콘텐츠로 이동

네트워크 디버깅

네트워크 디버깅: 장애를 체계적으로 찾아내는 기술

섹션 제목: “네트워크 디버깅: 장애를 체계적으로 찾아내는 기술”

네트워크 디버깅이란 “어느 레이어에서 무엇이 왜 깨졌는가”를 OSI 계층 구조를 기준으로 위→아래(또는 아래→위)로 좁혀가며 정확히 지목하는 과정이다.


  • 증상은 위에서, 원인은 아래에 있다: “API가 안 된다”는 L7 증상이지만 원인은 DNS 오설정(L3), Security Group 누락(방화벽), TCP 타임아웃(L4) 어디에든 있을 수 있다.
  • 추측 디버깅은 MTTR을 늘린다: 원인을 모른 채 서버 재시작, 배포 롤백을 반복하면 장애가 길어진다. 계층별 체크리스트가 있으면 10분 안에 좁혀진다.
  • 플랫폼 엔지니어의 핵심 역량: SRE/플랫폼 팀의 가치는 장애 시 “무엇을 봐야 하는지 바로 아는 것”이다.
  • 프론트엔드 경험이 자산이다: 브라우저 DevTools에서 보던 Network 탭 지식을 서버 사이드 도구와 연결하면 풀스택 디버깅이 가능해진다.

3. 멘탈 모델: OSI 레이어별 점검 순서

섹션 제목: “3. 멘탈 모델: OSI 레이어별 점검 순서”

3-1. 레이어별 담당 영역과 점검 도구

섹션 제목: “3-1. 레이어별 담당 영역과 점검 도구”
OSI Layer 담당 영역 점검 도구
─────────────────────────────────────────────────────────
L7 App HTTP 응답 코드, 헤더, 바디 curl -v, 브라우저 DevTools
L6 Pres TLS 인증서, 암호화 openssl s_client, Wireshark
L5 Session 세션 유지, 쿠키 curl --cookie, 브라우저
L4 Transport TCP 연결, 포트, 소켓 상태 ss, netstat, tcpdump
L3 Network IP 라우팅, ICMP ping, traceroute, mtr
L2 Data ARP, MAC, VLAN arp, ip neigh (컨테이너 브릿지)
L1 Physical 케이블, NIC, 링크 상태 ethtool, ip link

증상: “API 서버에 연결이 안 된다”

1단계 (L3) - 기본 연결 확인
ping api.example.com
→ 응답 없음 → DNS 문제 또는 방화벽(ICMP 차단)
2단계 (DNS) - 이름 해석 확인
dig api.example.com
→ NXDOMAIN → DNS 설정 오류
→ 올바른 IP 반환 → L4로 진행
3단계 (L4) - 포트 열려 있는지 확인
nc -zv api.example.com 443
telnet api.example.com 443
→ Connection refused → 서버 미기동 또는 방화벽
→ Connection timeout → 방화벽(패킷 DROP)
→ Connected → L7로 진행
4단계 (L7) - HTTP 레벨 확인
curl -v https://api.example.com/health
→ 연결은 되나 503 → 앱 또는 로드밸런서 문제
→ TLS 에러 → 인증서 문제

Terminal window
# 인터페이스 eth0에서 모든 패킷 캡처 (화면 출력)
sudo tcpdump -i eth0
# 특정 호스트의 패킷만
sudo tcpdump -i eth0 host 10.0.1.100
# 특정 포트만
sudo tcpdump -i eth0 port 443
# 파일로 저장 (Wireshark에서 열 수 있음)
sudo tcpdump -i eth0 -w /tmp/capture.pcap
# 저장된 파일 읽기 (-n: DNS 역조회 안 함, -v: 상세)
sudo tcpdump -r /tmp/capture.pcap -n -v

4-2. BPF(Berkeley Packet Filter) 필터 문법

섹션 제목: “4-2. BPF(Berkeley Packet Filter) 필터 문법”

BPF는 커널 레벨에서 필터링하므로 불필요한 패킷이 유저스페이스로 올라오지 않는다. 트래픽이 많은 환경에서 필수다.

BPF가 커널 레벨에서 동작하는 이유:

tcpdump 없이:
모든 패킷 → NIC → 커널 → 유저스페이스(tcpdump) → 필터 적용 → 출력
→ 1Gbps 링크에서 수십만 패킷/초를 전부 유저스페이스로 복사 → CPU 폭발
BPF 필터 사용:
모든 패킷 → NIC → 커널(BPF 가상머신에서 즉시 필터) → 매칭된 것만 → 유저스페이스
→ CPU 부하 최소화, 프로덕션에서도 안전하게 사용 가능
BPF 컴파일 과정:
"port 443 and host 10.0.1.5"
↓ libpcap이 BPF 바이트코드로 컴파일
ldh [12] # ethertype 로드
jeq #0x800 # IPv4인지 확인
ld [26] # src IP 로드
jeq #10.0.1.5 # IP 매칭
... # port 443 확인
↓ 커널 BPF 가상머신에서 실행
매칭된 패킷만 복사

📖 더 보기: BPF: The Forgotten Bytecode — Cloudflare — BPF 가상머신 내부 구조, JIT 컴파일, eBPF로의 발전. 이 섹션의 BPF 필터링 원리와 직접 연결됨

Terminal window
# 조합 연산자: and (&&), or (||), not (!)
# 특정 호스트와 포트 조합
sudo tcpdump -i eth0 host 10.0.1.5 and port 8080
# 출발지 IP 지정
sudo tcpdump -i eth0 src 10.0.1.5
# 목적지 포트 지정
sudo tcpdump -i eth0 dst port 80
# SYN 패킷만 (TCP 플래그 필터링)
sudo tcpdump -i eth0 'tcp[13] == 2'
# SYN+ACK 패킷만
sudo tcpdump -i eth0 'tcp[13] == 18'
# RST 패킷만 (연결 강제 종료 감지)
sudo tcpdump -i eth0 'tcp[13] & 4 != 0'
# HTTP GET 요청만 (페이로드 패턴 매칭)
sudo tcpdump -i eth0 -A 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420)'
# ICMP만
sudo tcpdump -i eth0 icmp
# 특정 서브넷
sudo tcpdump -i eth0 net 10.0.0.0/8
# 루프백 제외
sudo tcpdump -i eth0 not lo

시나리오 1: 연결이 갑자기 끊긴다 (RST 탐지)

Terminal window
# RST를 보내는 쪽 확인
sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -v
# 예상 출력
# 10.0.1.5.45231 > 10.0.1.10.8080: Flags [R.], seq 0, ack 1, win 0
# → 서버가 RST를 보내고 있음 → 앱 크래시 or 연결 수 초과

시나리오 2: 3-way handshake 확인

Terminal window
sudo tcpdump -i eth0 'port 8080 and (tcp[13] == 2 or tcp[13] == 18 or tcp[13] == 16)' -n
# SYN(2) → SYN-ACK(18) → ACK(16) 순서로 나와야 정상
# SYN만 계속 나오고 SYN-ACK가 없으면 → 서버가 응답 안 함

시나리오 3: 레이턴시 측정

Terminal window
# -tt: 타임스탬프 출력, 같은 seq 번호 찾아서 RTT 계산
sudo tcpdump -i eth0 -tt port 8080 | head -20
Terminal window
-i any # 모든 인터페이스
-n # DNS 역조회 하지 않음 (빠름)
-nn # IP도 이름으로 변환하지 않음
-v / -vv / -vvv # 상세도 증가
-c 100 # 100개만 캡처 후 종료
-s 0 # 전체 패킷 캡처 (기본은 65535 바이트로 이미 충분)
-A # ASCII로 페이로드 출력
-X # HEX + ASCII 출력
-q # 간략 출력

5-1. 캡처 필터 vs 디스플레이 필터

섹션 제목: “5-1. 캡처 필터 vs 디스플레이 필터”

두 가지를 혼동하면 안 된다. 문법도 다르다.

구분적용 시점문법예시
캡처 필터수집 전BPF 문법port 443
디스플레이 필터수집 후 분석Wireshark 전용http.response.code == 200

캡처 필터 (BPF 문법, tcpdump와 동일)

port 443
host 10.0.1.5
src port 80 and dst host 10.0.1.5
not port 22

디스플레이 필터 (Wireshark 전용)

# HTTP 관련
http.response.code == 500
http.request.method == "POST"
http.host contains "api.example.com"
# TCP 관련
tcp.flags.reset == 1 # RST 패킷
tcp.flags.syn == 1 # SYN 패킷
tcp.analysis.retransmission # 재전송 패킷
tcp.analysis.out_of_order # 순서 바뀐 패킷
tcp.port == 8080
# TLS 관련
tls.handshake.type == 1 # ClientHello
tls.handshake.type == 2 # ServerHello
tls.alert # TLS 에러
# DNS
dns.qry.name contains "example.com"
dns.flags.response == 1

5-2. TCP 스트림 추적 (Follow TCP Stream)

섹션 제목: “5-2. TCP 스트림 추적 (Follow TCP Stream)”

특정 TCP 연결의 전체 대화를 순서대로 보는 기능. 패킷 개별로 보는 것보다 훨씬 직관적이다.

방법:
1. 패킷 목록에서 원하는 패킷 우클릭
2. Follow → TCP Stream 선택
3. 빨간색 = 클라이언트 → 서버, 파란색 = 서버 → 클라이언트

실무 활용: HTTP/1.1 요청/응답을 평문으로 보거나, 잘못된 헤더를 정확히 확인할 때.

프로덕션에서는 불가능하지만 개발/스테이징에서 HTTPS 패킷을 복호화할 수 있다.

방법 1: 서버 Private Key 이용 (TLS 1.2까지, RSA 키 교환만)

Wireshark → Edit → Preferences → Protocols → TLS
→ RSA keys list에 서버 IP, 포트, 프로토콜, 키 파일 경로 입력

방법 2: SSLKEYLOGFILE (권장, TLS 1.3 포함)

Terminal window
# 브라우저나 curl이 세션 키를 파일에 기록
export SSLKEYLOGFILE=/tmp/sslkeys.log
curl https://api.example.com/data
# Wireshark → Edit → Preferences → Protocols → TLS
# → (Pre)-Master-Secret log filename에 /tmp/sslkeys.log 입력

Terminal window
curl -v https://api.example.com/health
# 출력 해석
# * Trying 54.12.34.56:443... → DNS 해석 결과, TCP 연결 시도
# * Connected to api.example.com → TCP 연결 성공
# * SSL connection using TLSv1.3 → TLS 버전
# > GET /health HTTP/2 → 송신 요청 헤더 (> 는 클라이언트 → 서버)
# < HTTP/2 200 → 수신 응답 (< 는 서버 → 클라이언트)
# < content-type: application/json
Terminal window
curl -o /dev/null -s -w "
DNS 조회: %{time_namelookup}s
TCP 연결: %{time_connect}s
TLS 핸드셰이크: %{time_appconnect}s
첫 바이트 수신: %{time_starttransfer}s
총 소요: %{time_total}s
HTTP 상태코드: %{http_code}
전송 크기: %{size_download} bytes
" https://api.example.com/health

출력 예시 및 해석

DNS 조회: 0.002s ← 빠름 (캐시 히트)
TCP 연결: 0.050s ← 정상 (같은 리전 내 ~50ms)
TLS 핸드셰이크: 0.120s ← 약간 높음 (인증서 체인 검증)
첫 바이트 수신: 0.350s ← TTFB, 서버 처리 시간 포함
총 소요: 0.360s
HTTP 상태코드: 200
전송 크기: 1024 bytes

time_appconnect - time_connect = TLS 핸드셰이크 시간
time_starttransfer - time_appconnect = 서버 처리 시간 (TTFB)

6-3. —resolve: DNS 우회해서 특정 서버 직접 테스트

섹션 제목: “6-3. —resolve: DNS 우회해서 특정 서버 직접 테스트”
Terminal window
# 로드밸런서를 거치지 않고 특정 서버 인스턴스만 테스트
curl --resolve api.example.com:443:10.0.1.15 https://api.example.com/health
# 배포 전 새 서버 검증에 유용
# /etc/hosts 수정 없이 임시로 IP를 오버라이드
Terminal window
# TLS 인증서 검증 무시 (개발 환경 자체 서명 인증서)
curl -k https://localhost:8443/health
# 헤더만 출력 (-I: HEAD 요청)
curl -I https://api.example.com
# 응답 바디 버리고 헤더만 (GET 유지)
curl -s -o /dev/null -D - https://api.example.com/health
# POST + JSON 바디
curl -X POST -H "Content-Type: application/json" \
-d '{"key":"value"}' \
https://api.example.com/data
# Authorization 헤더
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me
# 프록시 경유 (Burp Suite 같은 디버깅 프록시)
curl -x http://localhost:8080 https://api.example.com/health
# 리다이렉트 따라가기
curl -L https://api.example.com/redirect
# 타임아웃 설정
curl --connect-timeout 5 --max-time 30 https://api.example.com/health
# 특정 TLS 버전 강제
curl --tlsv1.2 https://api.example.com/health
# HTTP/1.1 강제 (HTTP/2 비교 테스트)
curl --http1.1 https://api.example.com/health

Terminal window
# A 레코드 조회
dig api.example.com
# 특정 레코드 타입
dig api.example.com MX # 메일 서버
dig api.example.com AAAA # IPv6
dig api.example.com CNAME # 별칭
dig api.example.com TXT # 텍스트 (SPF, DKIM 등)
dig api.example.com NS # 네임서버
# 간결한 출력 (+short)
dig api.example.com +short
# 출력: 54.12.34.56
# 특정 DNS 서버에 질의 (@)
dig @8.8.8.8 api.example.com # Google DNS
dig @1.1.1.1 api.example.com # Cloudflare DNS
dig @169.254.169.253 api.example.com # AWS VPC DNS (메타데이터 서버)
# 전체 해석 경로 추적 (+trace)
dig api.example.com +trace

시나리오 1: 내부 DNS는 되는데 외부는 안 된다

Terminal window
# 로컬 DNS 서버 확인
cat /etc/resolv.conf
# nameserver 10.0.0.2 ← VPC DNS 서버
# VPC DNS로 질의
dig @10.0.0.2 api.example.com
# 외부 DNS로도 질의 비교
dig @8.8.8.8 api.example.com
# 결과 다르면 내부 DNS Zone 설정 문제
# 결과 같은데 앱만 안 되면 앱의 DNS 캐시 TTL 문제

시나리오 2: Kubernetes 내부 서비스 DNS

Terminal window
# Pod 안에서 서비스 이름으로 DNS 확인
dig my-service.default.svc.cluster.local
# CoreDNS 서버 확인
cat /etc/resolv.conf
# nameserver 10.96.0.10 ← CoreDNS 클러스터 IP
# CoreDNS가 응답 안 하면
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns

시나리오 3: TTL 확인 (캐시 만료 시간)

Terminal window
dig api.example.com | grep -A2 'ANSWER SECTION'
# api.example.com. 300 IN A 54.12.34.56
# ↑ TTL 300초 → 최대 5분간 이전 IP 캐시됨
# 배포 후 DNS 변경이 반영 안 될 때 TTL 확인 필수

7-3. nslookup (Windows 환경 및 빠른 확인용)

섹션 제목: “7-3. nslookup (Windows 환경 및 빠른 확인용)”
Terminal window
nslookup api.example.com
nslookup api.example.com 8.8.8.8 # 특정 DNS 서버 지정
# 역방향 조회 (IP → 도메인)
nslookup 54.12.34.56

8-1. ss (Socket Statistics) — netstat의 현대적 대안

섹션 제목: “8-1. ss (Socket Statistics) — netstat의 현대적 대안”
Terminal window
# 모든 TCP 연결
ss -t
# 모든 소켓 (TCP + UDP + Unix)
ss -a
# LISTEN 중인 포트만
ss -tlnp
# -t: TCP, -l: LISTEN, -n: 숫자 IP/포트, -p: 프로세스 정보
# 예상 출력
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=1234,fd=15))
# ESTAB 0 0 10.0.1.5:8080 10.0.0.1:54321 users:(("node",pid=1234,fd=25))
# UDP 소켓
ss -un
# 특정 포트 필터링
ss -tnp sport = :8080
ss -tnp dport = :443
# 연결 수 집계
ss -tn state established | wc -l

TIME_WAIT는 TCP 연결 종료 후 같은 소켓을 재사용하지 않기 위해 보통 60초간 대기하는 상태다. 고트래픽 환경에서 포트 고갈을 일으킬 수 있다.

Terminal window
# TIME_WAIT 개수 확인
ss -tn state time-wait | wc -l
# TIME_WAIT 상세 목록
ss -tn state time-wait
# 상태별 집계
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
# 출력 예시:
# 1250 ESTAB
# 342 TIME-WAIT
# 12 CLOSE-WAIT
# 1 State
# CLOSE_WAIT 폭증 → 서버가 연결을 제때 닫지 않음 → 코드 버그
# TIME_WAIT 폭증 → 짧은 수명의 연결이 많음 → Connection Pooling 검토

TIME_WAIT 줄이는 커널 파라미터

Terminal window
# 현재 설정 확인
sysctl net.ipv4.tcp_tw_reuse
sysctl net.ipv4.ip_local_port_range
# 로컬 포트 범위 확대 (기본 32768-60999)
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# TIME_WAIT 소켓 재사용 허용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 영구 적용
echo "net.ipv4.tcp_tw_reuse=1" >> /etc/sysctl.conf
Terminal window
# LISTEN 포트
netstat -tlnp
# 모든 연결
netstat -an
# 상태별 집계
netstat -an | awk '/^tcp/ {print $6}' | sort | uniq -c
# 특정 포트 연결만
netstat -an | grep :8080

9. traceroute / mtr: 경로 추적과 패킷 로스 진단

섹션 제목: “9. traceroute / mtr: 경로 추적과 패킷 로스 진단”
Terminal window
# 기본 사용 (UDP 프로브)
traceroute api.example.com
# ICMP 사용 (방화벽에서 UDP 막을 때)
traceroute -I api.example.com
# TCP SYN 사용 (80 포트, 방화벽 통과율 높음)
traceroute -T -p 443 api.example.com
# IPv6
traceroute6 api.example.com
# 출력 해석
# 1 10.0.0.1 (10.0.0.1) 0.5 ms 0.4 ms 0.4 ms ← 게이트웨이 (1홉)
# 2 * * * ← ICMP 차단 (방화벽)
# 3 52.93.0.1 (52.93.0.1) 1.2 ms 1.1 ms 1.3 ms ← AWS 백본
# 4 api.example.com 2.0 ms 1.9 ms 2.1 ms ← 목적지

* * * 해석: ICMP TTL exceeded를 해당 홉이 차단한 것. 다음 홉이 응답한다면 경로는 살아있다. 마지막 홉이 * * *이면 방화벽 또는 서버 문제.

9-2. mtr (My TraceRoute): 실시간 경로 모니터링

섹션 제목: “9-2. mtr (My TraceRoute): 실시간 경로 모니터링”

traceroute를 지속적으로 실행하며 패킷 로스율과 레이턴시를 집계한다.

Terminal window
# 인터랙티브 모드
mtr api.example.com
# 보고서 모드 (100회 후 종료)
mtr -r -c 100 api.example.com
# TCP 모드 (443 포트)
mtr -T -P 443 api.example.com
# 출력 해석
# Host Loss% Snt Last Avg Best Wrst StDev
# 1. 10.0.0.1 0.0% 100 0.4 0.4 0.3 1.1 0.1
# 2. ??? ← ICMP 차단
# 3. 52.93.0.1 0.0% 100 1.2 1.1 1.0 2.3 0.1
# 4. api.example.com 5.0% 100 2.1 2.3 1.9 12.4 0.8 ← 5% 로스!

패킷 로스 해석: 중간 홉의 로스는 ICMP rate limiting일 수 있다. 이후 홉들의 로스가 없으면 무시해도 된다. 목적지 홉의 로스는 실제 문제다.


VPC에서 오가는 모든 IP 트래픽을 기록한다. tcpdump는 서버 안에서만 보지만, Flow Logs는 VPC 경계에서 본다 — Security Group이 패킷을 드랍한 것도 확인 가능.

Flow Logs 활성화

AWS Console → VPC → Flow Logs → Create
대상: CloudWatch Logs 또는 S3
필터: All (Accept + Reject 모두)

Flow Log 레코드 형식

version account-id interface-id src-addr dst-addr srcport dstport protocol packets bytes start end action log-status
예시:
2 123456789012 eni-abc12345 10.0.1.5 10.0.2.10 54321 8080 6 10 5000 1617000000 1617000060 ACCEPT OK
2 123456789012 eni-abc12345 10.0.1.5 10.0.2.10 54322 8080 6 0 0 1617000000 1617000060 REJECT OK

CloudWatch Insights로 분석

-- 거부된 트래픽 찾기
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 100
-- 특정 IP의 트래픽 볼륨
fields @timestamp, srcAddr, dstAddr, bytes
| filter srcAddr = "10.0.1.5"
| stats sum(bytes) by dstAddr

가장 흔한 연결 실패 원인. “Security Group이 막고 있다”는 Flow Log에서 REJECT로 확인한다.

Terminal window
# AWS CLI로 인바운드 규칙 확인
aws ec2 describe-security-groups \
--group-ids sg-12345678 \
--query 'SecurityGroups[0].IpPermissions' \
--output table
# 특정 포트의 인바운드 허용 여부 확인
aws ec2 describe-security-groups \
--filters "Name=group-id,Values=sg-12345678" \
--query 'SecurityGroups[0].IpPermissions[?ToPort==`8080`]'

Security Group vs NACL

항목Security GroupNetwork ACL
적용 범위인스턴스 레벨서브넷 레벨
상태 추적Stateful (응답 자동 허용)Stateless (인/아웃 별도)
기본 정책인바운드 거부, 아웃바운드 허용허용
로그Flow Logs에서 REJECTFlow Logs에서 REJECT

ALB에서 L7 레벨의 요청/응답을 기록한다. 서버에서 로그가 안 보이면 ALB 단에서 막힌 것.

ALB 액세스 로그 활성화

AWS Console → EC2 → Load Balancers → 선택 → Attributes
→ Access logs → Enable → S3 bucket 지정

로그 형식 주요 필드

type time elb client:port target:port request_processing_time
target_processing_time response_processing_time elb_status_code
target_status_code received_bytes sent_bytes request user_agent ssl_cipher ssl_protocol

Athena로 ALB 로그 분석

-- 5xx 에러가 많은 타겟 찾기
SELECT target_ip, target_port, target_status_code, COUNT(*) as count
FROM alb_logs
WHERE target_status_code >= 500
AND time > '2024-01-01T00:00:00Z'
GROUP BY target_ip, target_port, target_status_code
ORDER BY count DESC
LIMIT 20;
-- 느린 요청 찾기 (TTFB > 1초)
SELECT request_url, target_processing_time, client_ip
FROM alb_logs
WHERE target_processing_time > 1.0
ORDER BY target_processing_time DESC
LIMIT 50;

11-1. nsenter: 컨테이너 네트워크 네임스페이스 진입

섹션 제목: “11-1. nsenter: 컨테이너 네트워크 네임스페이스 진입”

컨테이너는 별도 네트워크 네임스페이스를 갖는다. 호스트에서 tcpdump를 실행하면 컨테이너 가상 인터페이스(veth)까지 보인다.

Terminal window
# 컨테이너 PID 확인
docker inspect my-container --format '{{.State.Pid}}'
# 출력: 12345
# 컨테이너 네트워크 네임스페이스에서 명령 실행
sudo nsenter -t 12345 -n ip addr
sudo nsenter -t 12345 -n ss -tlnp
sudo nsenter -t 12345 -n tcpdump -i eth0 port 8080
# 컨테이너 안에서 tcpdump 바이너리가 없을 때 유용
# 호스트의 도구를 컨테이너 네임스페이스 안에서 실행 가능

11-2. kubectl exec: Pod 안에서 직접 디버깅

섹션 제목: “11-2. kubectl exec: Pod 안에서 직접 디버깅”
Terminal window
# Pod 안에서 셸 실행
kubectl exec -it my-pod -- /bin/sh
# 특정 컨테이너 지정 (멀티 컨테이너 Pod)
kubectl exec -it my-pod -c sidecar -- /bin/sh
# 원라이너로 DNS 확인
kubectl exec -it my-pod -- nslookup my-service.default.svc.cluster.local
# curl 실행 (이미지에 있다면)
kubectl exec -it my-pod -- curl -v http://my-service:8080/health
# 네트워크 정책 확인용 nc
kubectl exec -it my-pod -- nc -zv my-service 8080

11-3. Ephemeral Container: 최소 이미지 Pod 디버깅

섹션 제목: “11-3. Ephemeral Container: 최소 이미지 Pod 디버깅”

프로덕션 이미지에는 curl, tcpdump, bash가 없는 경우가 많다. Kubernetes 1.23+의 Ephemeral Container를 사용하면 실행 중인 Pod에 임시 디버그 컨테이너를 주입할 수 있다.

Terminal window
# nicolaka/netshoot: 네트워크 디버깅 도구 올인원 이미지
kubectl debug -it my-pod --image=nicolaka/netshoot --target=my-container
# 진입 후 사용 가능한 도구들
tcpdump -i eth0
curl, wget, dig, nslookup, ss, netstat, mtr, traceroute, iperf3, ...
# 종료하면 ephemeral container도 사라짐

컨테이너 간 트래픽 tcpdump

Terminal window
# veth 인터페이스 찾기 (호스트에서)
# 컨테이너 PID의 네트워크 네임스페이스와 연결된 veth 확인
ip link | grep veth
# 또는 컨테이너 IP로 필터
sudo tcpdump -i any host <pod-ip>
Terminal window
# Pod의 DNS 설정 확인
kubectl exec my-pod -- cat /etc/resolv.conf
# nameserver 10.96.0.10 ← CoreDNS
# search default.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5
# ndots:5 의미: 도트가 5개 미만이면 search 도메인을 붙여서 먼저 시도
# "my-service" → my-service.default.svc.cluster.local 시도 → 성공하면 바로 사용
# CoreDNS 로그 확인
kubectl logs -n kube-system -l k8s-app=kube-dns -f

12-1. “사이트가 느려요” 신고 → 원인 파악

섹션 제목: “12-1. “사이트가 느려요” 신고 → 원인 파악”

Step 1: 영향 범위 확인 (1분)

Terminal window
# 모든 사용자? 특정 지역? 특정 기능?
# 내부에서도 느린가?
curl -w "\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
-o /dev/null -s https://api.example.com/health

Step 2: 인프라 레이어부터 빠르게 순환 (2분)

Terminal window
# DNS 정상?
dig api.example.com +short
# ALB/서버 연결 정상?
nc -zv api.example.com 443
# 서버 응답 타이밍
curl -w "connect:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer}\n" \
-o /dev/null -s https://api.example.com/health

Step 3: TTFB가 느리면 → 서버 처리 시간 문제

Terminal window
# 서버 CPU/메모리
kubectl top pods
kubectl top nodes
# 애플리케이션 로그에서 슬로우 쿼리
kubectl logs my-api-pod --since=5m | grep -E "slow|timeout|ERROR"
# DB 연결 확인
kubectl exec my-api-pod -- ss -tn | grep :5432

Step 4: TCP Connect가 느리면 → 네트워크/LB 문제

Terminal window
# ALB 타겟 헬스 확인
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:...
# Security Group 변경 이력
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AuthorizeSecurityGroupIngress \
--start-time 2024-01-01T00:00:00Z

Step 5: DNS가 느리거나 틀리면 → DNS 문제

Terminal window
# 여러 DNS 서버에서 결과 비교
for dns in 8.8.8.8 1.1.1.1 169.254.169.253; do
echo -n "$dns: "
dig @$dns api.example.com +short +time=2
done

12-2. “특정 API만 502 에러가 난다”

섹션 제목: “12-2. “특정 API만 502 에러가 난다””
Terminal window
# 1. ALB가 502를 주는 건지, 앱이 502를 주는 건지 구분
# ALB Access Log에서 elb_status_code vs target_status_code 비교
# elb=502, target=- → ALB가 타겟과 연결 실패
# elb=502, target=502 → 앱이 502를 응답
# 2. 타겟 헬스 확인
aws elbv2 describe-target-health --target-group-arn ...
# 3. 해당 Pod의 응답 직접 확인 (ALB 우회)
kubectl exec debug-pod -- curl http://<pod-ip>:8080/api/problematic
# 4. Pod 로그 확인
kubectl logs <pod-name> --previous # 이전 크래시 로그
kubectl logs <pod-name> --since=5m

12-3. “간헐적으로 Connection Reset 된다”

섹션 제목: “12-3. “간헐적으로 Connection Reset 된다””
Terminal window
# tcpdump로 RST 패킷 잡기
sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -w /tmp/rst.pcap
# Wireshark에서 열어 Follow TCP Stream으로 컨텍스트 확인
# 원인 후보:
# - keepalive 타임아웃 (ALB 기본 60초, nginx 75초 등 불일치)
# - 서버 측 Connection Pool 고갈
# - OOM Killer가 프로세스 종료
# - iptables 규칙 (conntrack 테이블 가득 참)
# conntrack 테이블 상태 확인
sudo conntrack -C # 현재 엔트리 수
sudo sysctl net.netfilter.nf_conntrack_max # 최대값

12.5 트러블슈팅 (실무 에러 패턴)

섹션 제목: “12.5 트러블슈팅 (실무 에러 패턴)”

🔧 VPC Flow Logs에서 REJECT인데 Security Group은 열려 있음 — NACL 차단

섹션 제목: “🔧 VPC Flow Logs에서 REJECT인데 Security Group은 열려 있음 — NACL 차단”

증상:

Terminal window
# Flow Logs 분석 (CloudWatch Insights)
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, action
| filter action = "REJECT" and dstPort = 8080
| sort @timestamp desc
# 출력: 10.0.1.5 → 10.0.2.10 포트 8080 REJECT가 계속 기록됨
# Security Group 확인하면 8080 허용되어 있음
aws ec2 describe-security-groups --group-ids sg-xxxxx \
--query 'SecurityGroups[0].IpPermissions[?ToPort==`8080`]'
# 결과: 허용 규칙 있음 → 그런데 왜 REJECT?

원인: Security Group은 Stateful(응답 자동 허용)이지만 NACL(Network ACL)은 Stateless다. NACL 아웃바운드 규칙에서 임시 포트(ephemeral port: 1024-65535)가 차단되어 있으면 응답 패킷이 반환되지 못한다. 또는 소스 서브넷의 NACL 아웃바운드 규칙이 제한되어 있을 수 있다.

해결:

Terminal window
# 1. 해당 서브넷의 NACL 확인
aws ec2 describe-network-acls \
--filters "Name=association.subnet-id,Values=subnet-xxxxxx" \
--query 'NetworkAcls[*].Entries' \
--output table
# 2. NACL 임시 포트 아웃바운드 허용 추가 (응답 트래픽)
aws ec2 create-network-acl-entry \
--network-acl-id acl-xxxxxx \
--rule-number 100 \
--protocol tcp \
--rule-action allow \
--egress \
--cidr-block 10.0.0.0/16 \
--port-range From=1024,To=65535
# 3. Security Group vs NACL 우선순위 기억:
# 패킷 → ENI → Security Group → NACL → 서브넷
# 둘 다 통과해야 함

🔧 kubectl exec 내부에서는 되는데 클러스터 외부에서 접속 실패

섹션 제목: “🔧 kubectl exec 내부에서는 되는데 클러스터 외부에서 접속 실패”

증상:

Terminal window
# 클러스터 내부 (정상):
kubectl exec my-pod -- curl -s http://my-service:8080/health
# {"status": "ok"}
# 클러스터 외부 (실패):
curl https://api.example.com/health
# curl: (7) Failed to connect to api.example.com port 443: Connection refused
# nc로 포트 확인:
nc -zv api.example.com 443
# nc: connect to api.example.com port 443 (tcp) failed: Connection refused

원인: 여러 가능성:

  1. Ingress가 설정되지 않았거나 Ingress Controller가 실행되지 않음
  2. ALB/NLB Security Group에서 인바운드 443 미허용
  3. Route 53 레코드가 없거나 잘못된 IP 가리킴
  4. TLS 인증서 오류 (443은 열려 있지만 핸드셰이크 실패)

해결:

Terminal window
# 1. 레이어별 순차 점검
dig api.example.com +short # DNS → IP 반환되는지
nc -zv <ip-address> 443 # TCP 연결 가능한지
openssl s_client -connect api.example.com:443 # TLS 핸드셰이크
# 2. Ingress 상태 확인
kubectl get ingress -n production
# NAME CLASS HOSTS ADDRESS PORTS AGE
# api-ingress alb api.example.com 54.1.2.3 80,443 2d
# ADDRESS가 없으면 ALB 생성 실패 → Ingress Controller 로그 확인:
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
# 3. ALB Security Group 인바운드 확인
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[?DNSName==`my-alb.elb.amazonaws.com`].SecurityGroups'
aws ec2 describe-security-groups --group-ids sg-xxxxxx \
--query 'SecurityGroups[0].IpPermissions[?ToPort==`443`]'

🔧 tcpdump로 패킷이 보이는데 앱이 요청을 못 받음 — iptables DROP

섹션 제목: “🔧 tcpdump로 패킷이 보이는데 앱이 요청을 못 받음 — iptables DROP”

증상:

Terminal window
# tcpdump에서는 패킷이 서버에 도달:
sudo tcpdump -i eth0 port 8080 -n
# 10.0.1.5.54321 > 10.0.2.10.8080: Flags [S] seq 0, ...
# (그런데 앱 로그에는 아무것도 없음, 응답도 없음)
# ss -tlnp로 포트는 정상적으로 LISTEN 중:
ss -tlnp | grep 8080
# LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=1234))

원인: 패킷이 NIC 레벨(tcpdump가 보는 레벨)에서는 수신되지만, iptables가 커널 레벨에서 DROP하는 경우. Docker, Kubernetes, 또는 직접 iptables 규칙이 트래픽을 차단할 수 있다. conntrack 테이블이 가득 찬 경우에도 동일 증상이 발생한다.

해결:

Terminal window
# 1. iptables 규칙 확인
sudo iptables -L -n -v | grep -E "DROP|REJECT" | head -20
sudo iptables -t nat -L -n -v | head -30 # NAT 규칙 (Docker/K8s)
# 2. conntrack 테이블 상태 확인
sudo conntrack -C # 현재 엔트리 수
sudo sysctl net.netfilter.nf_conntrack_max # 최대값
# 현재값 ≈ 최대값이면 conntrack 고갈 → 새 연결 DROP
# 3. conntrack 최대값 증가
sudo sysctl -w net.netfilter.nf_conntrack_max=1048576
# 또는 /etc/sysctl.conf에 영구 적용
# 4. 특정 연결이 conntrack에 있는지 확인
sudo conntrack -L | grep "10.0.1.5"
# 5. 패킷 추적 (iptables TRACE target)
sudo iptables -t raw -A PREROUTING -p tcp --dport 8080 -j TRACE
sudo dmesg | grep TRACE | tail -20

13. 프론트엔드 → 플랫폼 브릿지

섹션 제목: “13. 프론트엔드 → 플랫폼 브릿지”

13-1. 브라우저 DevTools Network 탭에서 보이는 것

섹션 제목: “13-1. 브라우저 DevTools Network 탭에서 보이는 것”

4년차 프론트엔드 개발자라면 이미 이 화면이 익숙하다:

Name Status Type Initiator Size Time
─────────────────────────────────────────────────────
api/users 200 fetch app.js:42 2.3 KB 243 ms
↓ Request Headers
:method: GET
:authority: api.example.com
Authorization: Bearer eyJ...
Content-Type: application/json
↓ Response Headers
content-type: application/json
x-request-id: abc-123-def
cache-control: no-cache
↓ Timing
Stalled: 12 ms ← 브라우저 큐잉 (6개 동시 제한)
DNS Lookup: 0 ms ← 캐시 히트
Initial connection: 45 ms ← TCP + TLS
Request sent: 0 ms
Waiting (TTFB): 180 ms ← 서버 처리 시간!
Content Download: 6 ms

13-2. 서버 사이드에서 같은 요청을 보면

섹션 제목: “13-2. 서버 사이드에서 같은 요청을 보면”
Terminal window
# curl로 동일 재현 (브라우저 DevTools의 "Copy as cURL" 사용)
curl -v -H "Authorization: Bearer eyJ..." \
https://api.example.com/api/users
# tcpdump로 패킷 레벨 확인
sudo tcpdump -i eth0 host api.example.com and port 443 -w /tmp/api.pcap
# ALB Access Log에서
# target_processing_time: 서버가 처리한 시간 (DevTools TTFB와 비교)
# request_processing_time: ALB → 타겟 연결 시간

13-3. 브라우저에서만 보이는 것 vs 서버에서만 보이는 것

섹션 제목: “13-3. 브라우저에서만 보이는 것 vs 서버에서만 보이는 것”
항목브라우저 DevTools서버 사이드 도구
CORS 에러O (preflight 요청 보임)X (서버는 모든 요청 수신)
브라우저 캐시O (from cache, 0ms)X
서비스 워커O (SW intercept)X
TLS 세부 정보간략히 (Security 탭)O (openssl, tcpdump)
서버 내부 처리X (TTFB로 추정만)O (앱 로그, 프로파일러)
네트워크 드랍XO (tcpdump, Flow Logs)
방화벽 차단Connection 에러로만O (Flow Logs REJECT)
Pod간 통신XO (nsenter, kubectl)

브라우저에서 CORS 에러가 나도 서버에서는 멀쩡해 보이는 이유:

Terminal window
# 브라우저는 먼저 OPTIONS 요청 (preflight) 보냄
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization" \
-v 2>&1 | grep -i "access-control"
# 올바른 응답이면 이 헤더들이 있어야 함:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Access-Control-Allow-Headers: Authorization

13-5. HTTP/2 멀티플렉싱 — 브라우저와 서버 사이드의 차이

섹션 제목: “13-5. HTTP/2 멀티플렉싱 — 브라우저와 서버 사이드의 차이”
Terminal window
# 브라우저: 같은 도메인 요청을 HTTP/2 스트림으로 다중화 → Stalled 거의 없음
# curl로 HTTP/2 확인
curl -v --http2 https://api.example.com/health 2>&1 | grep "HTTP/"
# 서버에서 h2 지원 확인
openssl s_client -connect api.example.com:443 -alpn h2 2>&1 | grep "ALPN"
# ALPN protocol: h2 → HTTP/2 지원 확인됨

상황 도구
──────────────────────────────────────────────────────
DNS 안 된다 dig, nslookup
포트 열려 있는지 nc -zv, telnet
HTTP 응답 상세 보기 curl -v
HTTP 타이밍 분석 curl -w
패킷 레벨 디버깅 tcpdump, Wireshark
소켓 상태 / 포트 확인 ss -tlnp
연결 수 / TIME_WAIT ss -tan | awk 집계
경로 추적 traceroute, mtr
패킷 로스 모니터링 mtr -r
TLS 인증서 확인 openssl s_client
AWS Security Group 차단 VPC Flow Logs
ALB 레벨 에러 ALB Access Logs
컨테이너 네트워크 nsenter, kubectl exec
도구 없는 컨테이너 디버깅 kubectl debug (netshoot)
Terminal window
# 인증서 만료일 확인
echo | openssl s_client -connect api.example.com:443 2>/dev/null \
| openssl x509 -noout -dates
# SAN(Subject Alternative Names) 확인
echo | openssl s_client -connect api.example.com:443 2>/dev/null \
| openssl x509 -noout -text | grep -A2 "Subject Alternative"
# 인증서 체인 전체 확인
openssl s_client -connect api.example.com:443 -showcerts
# 특정 TLS 버전 지원 여부
openssl s_client -connect api.example.com:443 -tls1_2
openssl s_client -connect api.example.com:443 -tls1_3

14-3. iperf3: 네트워크 대역폭 측정

섹션 제목: “14-3. iperf3: 네트워크 대역폭 측정”
Terminal window
# 서버에서 (대기)
iperf3 -s -p 5201
# 클라이언트에서 (10초간 측정)
iperf3 -c server-ip -p 5201 -t 10
# UDP 패킷 로스 측정
iperf3 -c server-ip -u -b 100M # 100Mbps로 UDP 전송

  1. 레이어별로 좁혀라: L3(ping) → DNS(dig) → L4(nc) → L7(curl) 순서로 점검하면 원인 레이어가 좁혀진다.
  2. tcpdump는 패킷을 거짓말하지 않는다: 앱 로그에 안 보여도 tcpdump에서 RST가 보이면 그게 사실이다.
  3. 브라우저 DevTools와 서버 도구를 연결하라: TTFB = target_processing_time (ALB 로그) = 서버 처리 시간이다.
  4. TIME_WAIT vs CLOSE_WAIT 구분: TIME_WAIT는 정상 종료 후 대기, CLOSE_WAIT는 서버가 연결을 안 닫는 코드 버그다.
  5. 컨테이너에 도구가 없으면 nsenter 또는 kubectl debug: 프로덕션 이미지를 바꾸지 않고도 디버깅할 수 있다.
  6. VPC Flow Logs는 방화벽 디버깅의 결정판: Security Group이 차단하면 Flow Logs에 REJECT로 남는다.


Terminal window
# 1. OSI 계층별 순차 점검 스크립트
TARGET="api.example.com"
echo "=== L3: DNS ==="
dig $TARGET +short
echo "=== L4: TCP 포트 ==="
nc -zv $TARGET 443 2>&1
echo "=== L6: TLS ==="
openssl s_client -connect $TARGET:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher|Verify"
echo "=== L7: HTTP ==="
curl -s -o /dev/null -w "HTTP %{http_code} | TTFB: %{time_starttransfer}s\n" https://$TARGET/health

예상 출력:

=== L3: DNS ===
54.12.34.56
=== L4: TCP 포트 ===
Connection to api.example.com port 443 [tcp/https] succeeded!
=== L6: TLS ===
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
=== L7: HTTP ===
HTTP 200 | TTFB: 0.187s
Terminal window
# 2. RST 패킷 실시간 감지
sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -c 10

예상 출력 (RST 발생 시):

14:23:05.123456 IP 10.0.1.5.45231 > 10.0.2.10.8080: Flags [R.], seq 0, ack 1, win 0, length 0
14:23:05.234567 IP 10.0.2.10.8080 > 10.0.1.5.45231: Flags [R], seq 2048, win 0, length 0
# 서버가 RST 보내는 경우 → 앱 크래시 or 포트 미청취
# 클라이언트가 RST 보내는 경우 → 타임아웃 or keepalive 만료
Terminal window
# 3. 소켓 상태 집계
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

예상 출력 (정상):

245 ESTAB
42 TIME-WAIT
5 LISTEN
1 State

예상 출력 (비정상 — CLOSE_WAIT 폭증):

150 ESTAB
1240 CLOSE-WAIT ← 서버가 연결을 닫지 않는 코드 버그
12 TIME-WAIT
5 LISTEN
Terminal window
# 4. curl 타이밍 종합 분석
curl -o /dev/null -s -w \
"DNS: %{time_namelookup}s | TCP: %{time_connect}s | TLS: %{time_appconnect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n" \
https://google.com

예상 출력:

DNS: 0.002s | TCP: 0.025s | TLS: 0.071s | TTFB: 0.098s | Total: 0.112s
# TLS 핸드셰이크: 0.071 - 0.025 = 46ms
# 서버 처리 (TTFB): 0.098 - 0.071 = 27ms
Terminal window
# 5. VPC Flow Logs에서 REJECT 트래픽 찾기 (CloudWatch Insights)
# aws logs start-query --log-group-name VPCFlowLogs \
# --start-time $(date -d "1 hour ago" +%s) \
# --end-time $(date +%s) \
# --query-string 'fields @timestamp, srcAddr, dstAddr, dstPort, action
# | filter action = "REJECT"
# | stats count(*) by srcAddr, dstAddr, dstPort
# | sort count desc | limit 10'
echo "CloudWatch Insights 쿼리를 콘솔에서 실행하면:"
echo "srcAddr dstAddr dstPort count"
echo "10.0.1.5 10.0.2.10 8080 342 ← Security Group 확인 필요"
echo "10.0.1.5 10.0.3.20 5432 156 ← DB 접근 차단"