- Published on
EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 살아 있고 Pod도 Running인데, 외부로 나가는 트래픽(egress)만 간헐적으로 끊기는 케이스는 EKS 운영에서 가장 추적이 까다로운 축에 속합니다. 특히 DNS는 가끔 실패, HTTPS 호출은 타임아웃, 재시도하면 또 성공 같은 패턴이 반복되면 애플리케이션/네트워크/클라우드 경계 어디에서 병목이 생기는지 감이 흐려집니다.
이 글은 “EKS에서 Pod egress만 간헐 끊김”을 SNAT(소스 NAT)·NAT Gateway·conntrack 관점에서 끝까지 추적하는 실전 절차를 제공합니다. 문제를 단순히 “NAT GW가 느린가?”로 단정하지 않고, 관측 가능한 증거(메트릭/로그/패킷/커널 상태) 로 원인을 좁혀가는 흐름으로 구성했습니다.
> DNS 증상부터 시작한다면 먼저 EKS에서 CoreDNS 정상인데 DNS가 간헐 실패할 때도 함께 보면, ‘DNS 문제처럼 보이지만 사실 egress SNAT 문제’인 전형적인 함정을 빠르게 걸러낼 수 있습니다.
증상 패턴: “Pod egress만” 끊긴다는 말의 의미
다음 조건이 동시에 성립하면, 노드/Pod 내부 문제라기보다 노드에서 VPC로 나가는 경로(특히 SNAT/NAT) 에 초점을 맞추는 게 효율적입니다.
- Ingress(예: ALB → Pod)는 정상인데, Pod → 외부 API 호출만 간헐 실패
- 같은 Pod가 같은 대상에 재시도하면 성공/실패가 섞임
- 특정 노드에 스케줄된 Pod들에서 더 자주 발생(노드 편향)
- 실패 시 에러가 다양함
i/o timeout,context deadline exceededconnection reset by peer(간헐)- DNS
SERVFAIL/timeout(간헐)
이때 흔한 오해는 “외부 API가 불안정하다”인데, 실제로는 클러스터 내부에서 외부로 나가는 출구가 순간적으로 포화되는 경우가 많습니다.
EKS egress 경로 요약: 어디서 SNAT이 일어나는가
EKS에서 Pod가 인터넷(또는 VPC 밖)으로 나갈 때 대표 경로는 두 가지입니다.
1) 프라이빗 서브넷 + NAT Gateway
- Pod IP(대개 VPC CIDR 대역의 secondary IP) → 노드에서 라우팅 → NAT Gateway에서 SNAT → 인터넷
- 이때 병목은 주로 NAT Gateway의 SNAT 포트/연결 처리 또는 노드의 conntrack 테이블에서 발생합니다.
2) 퍼블릭 서브넷 + 노드 Public IP (직접 egress)
- 노드가 퍼블릭 IP를 가지고 인터넷으로 직접 나감(보안/운영상 권장되지 않는 경우가 많음)
- 병목은 노드 자체의 conntrack/iptables에 더 민감하게 나타납니다.
대부분의 프로덕션 EKS는 1) 구조이므로, 이 글도 NAT Gateway 기반을 중심으로 설명합니다.
가장 흔한 원인 Top 4
간헐 끊김을 만드는 원인은 다양하지만, 현장에서 “Pod egress만”이라는 조건을 만족할 때 빈도가 높은 순으로 정리하면 다음과 같습니다.
- NAT Gateway SNAT 포트 고갈(또는 특정 목적지로 포트 집중)
- 노드 conntrack 테이블 포화(또는 conntrack GC 지연)
- 보안그룹/NACL/라우팅 비대칭 또는 특정 AZ 편향
- 애플리케이션의 연결 재사용/Keepalive 전략 부재로 인한 연결 폭증
이 중 1,2는 “간헐”을 만들기 쉬운 전형적인 메커니즘입니다. 순간 피크에서 포화 → 일부 세션 드랍/타임아웃 → 피크가 내려가면 정상으로 돌아옵니다.
1단계: 실패를 ‘네트워크 레벨’로 분해하기
먼저 애플리케이션 로그만 보면 원인이 뭉개집니다. 실패를 아래 3종으로 분해하세요.
- DNS 레벨 실패: 이름 해석이 안 됨 (
lookup ... i/o timeout) - TCP 핸드셰이크 실패: SYN 재전송 후 타임아웃
- TLS/HTTP 레벨 실패: 연결은 됐는데 중간에 끊김/리셋
Pod에서 즉시 확인하는 최소 커맨드
# 1) DNS만 분리해서 확인
kubectl exec -it <pod> -- nslookup api.example.com
# 2) TCP 핸드셰이크/레이턴시 확인
kubectl exec -it <pod> -- bash -lc 'time curl -sv --connect-timeout 2 https://api.example.com/healthz -o /dev/null'
# 3) 같은 목적지로 짧은 연결을 여러 번 만들어 실패율 확인
kubectl exec -it <pod> -- bash -lc 'for i in {1..50}; do curl -sS --connect-timeout 2 https://api.example.com/healthz >/dev/null || echo FAIL-$i; done'
nslookup이 흔들리면 CoreDNS 자체 문제도 가능하지만, egress가 불안정하면 CoreDNS가 upstream으로 나가는 UDP/TCP가 흔들려 “DNS 문제처럼” 보일 수 있습니다.curl -sv에서Connected to ...가 안 나오면 TCP 단계에서 막힌 겁니다(대개 SNAT/conntrack 의심).
2단계: NAT Gateway가 의심될 때 보는 지표(CloudWatch)
NAT Gateway는 “정상/비정상”이 아니라 포화/드랍/재전송 형태로 증상이 나타납니다. CloudWatch에서 다음 지표를 최소 세트로 보세요.
BytesOutToDestination,BytesInFromSource: 트래픽 피크 타이밍ActiveConnectionCount: 동시 연결 수ConnectionAttemptCount: 연결 시도량(스파이크 여부)ErrorPortAllocation: SNAT 포트 할당 실패(핵심)PacketsDropCount(있다면): 드랍 증가 여부
CLI로 최근 1시간의 ErrorPortAllocation 확인 예시
aws cloudwatch get-metric-statistics \
--namespace AWS/NATGateway \
--metric-name ErrorPortAllocation \
--dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
--statistics Sum \
--period 60 \
--start-time "$(date -u -d '1 hour ago' +%FT%TZ)" \
--end-time "$(date -u +%FT%TZ)"
- 여기서
ErrorPortAllocation이 0이 아니면, 거의 확실하게 SNAT 포트 부족이 egress 간헐 장애의 주범입니다.
왜 SNAT 포트가 고갈되는가
NAT Gateway는 내부적으로 (목적지 IP:Port) 단위로 SNAT 포트를 할당합니다. 다음 상황에서 고갈이 빨라집니다.
- Pod들이 같은 외부 API(같은 IP/포트) 로 짧은 연결을 대량 생성
- HTTP keep-alive/connection pooling이 없어서 매 요청마다 새 연결
- TIME_WAIT가 길거나 재시도로 연결 시도가 폭증
즉 “트래픽 총량”보다 연결 생성 패턴이 더 중요할 때가 많습니다.
3단계: 노드 conntrack 포화 확인(간헐 드랍의 또 다른 축)
SNAT이 NAT GW에서 일어나더라도, 노드 커널의 conntrack이 포화되면 노드에서 이미 패킷이 드랍될 수 있습니다. 특히 대량의 짧은 연결을 만들면 conntrack 엔트리가 급증합니다.
노드에 접속해서 conntrack 사용량 확인
노드에 SSM 또는 SSH로 들어갈 수 있다면:
# 현재 conntrack 엔트리 수
sudo conntrack -C
# conntrack 최대치
cat /proc/sys/net/netfilter/nf_conntrack_max
# 드랍/인서트 실패 카운터(커널 메시지)
dmesg | egrep -i 'conntrack|nf_conntrack' | tail -n 50
# conntrack 통계(드랍/에러)
sudo conntrack -S
의심 신호:
conntrack -C가nf_conntrack_max에 지속적으로 근접dmesg에nf_conntrack: table full, dropping packet등장
이 경우 NAT GW가 아무리 건강해도, 노드에서 이미 egress가 새 연결을 만들지 못합니다.
임시 완화(주의: 근본 해결 전까지)
# 예: conntrack max 상향(노드 재부팅 시 초기화될 수 있음)
sudo sysctl -w net.netfilter.nf_conntrack_max=524288
# TIME_WAIT 재사용 등은 부작용이 커서 신중(권장 X)
근본 해결은 아래 “해결책 섹션”의 연결 재사용/풀링, NAT GW 분산, Pod egress 설계 개선 쪽이 더 안전합니다.
4단계: AZ 편향과 라우팅/보안 설정 확인
간헐 장애는 종종 “특정 AZ의 NAT GW만 포화” 같은 형태로 나타납니다.
- 프라이빗 서브넷이 여러 AZ에 있고, 각 AZ 라우트 테이블이 자기 AZ의 NAT GW를 바라보는지
- 특정 AZ에만 워크로드가 몰려 NAT GW 한 대만 혹사되는지
- NACL이 ephemeral port 범위를 과도하게 제한하고 있지 않은지
여기서 보안그룹/네트워크 경계 점검이 필요하면, 인바운드 케이스지만 체크리스트 구성 자체가 유용한 EKS에서 NodePort만 안 열릴 때 CNI·SG 점검도 참고할 만합니다(보안그룹/NACL/라우팅을 체계적으로 확인하는 관점).
5단계: 패킷/흐름 로그로 “어디서 끊기는지” 증거 만들기
지표가 애매하면 VPC Flow Logs + 노드 tcpdump로 끊기는 위치를 고정하세요.
VPC Flow Logs에서 볼 것
- NAT GW가 붙은 서브넷/ENI 기준으로 REJECT가 늘어나는지
- 특정 목적지로의 트래픽이 ACCEPT인데도 응답이 없는지(외부 문제 가능)
노드에서 tcpdump로 SYN 재전송 확인
# 목적지 호스트로 나가는 SYN/ACK 흐름 확인(예: 443)
sudo tcpdump -ni any 'tcp port 443 and (tcp[tcpflags] & (tcp-syn) != 0)'
# 특정 목적지 IP로 한정
sudo tcpdump -ni any host 203.0.113.10 and tcp port 443
- SYN만 계속 나가고 SYN-ACK이 안 들어오면, NAT/SNAT/경로/외부 방화벽 이슈를 의심합니다.
- SYN-ACK까지 오는데 TLS에서 끊기면 MTU/프록시/중간장비 가능성을 추가로 봅니다.
해결책: 원인별로 “가장 덜 위험한” 순서
여기서부터는 원인에 따라 처방이 달라집니다. 운영 리스크가 낮은 것부터 적용 우선순위를 제안합니다.
1) 연결 재사용(keep-alive/풀링)으로 SNAT 압력 줄이기
가장 비용 대비 효과가 큰 해결책입니다. NAT GW 포트 고갈/conntrack 포화를 만드는 1등 공신이 “짧은 연결 폭증”이기 때문입니다.
예: Go HTTP 클라이언트 튜닝
tr := &http.Transport{
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Transport: tr,
Timeout: 10 * time.Second,
}
예: Python requests 세션 재사용
import requests
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100)
session.mount("https://", adapter)
for _ in range(1000):
r = session.get("https://api.example.com/healthz", timeout=2)
r.raise_for_status()
이렇게만 바꿔도 ConnectionAttemptCount가 크게 줄고, 간헐 타임아웃이 사라지는 경우가 많습니다.
2) NAT Gateway 수평 확장/분산
- AZ별 NAT GW를 두고 라우트 테이블을 올바르게 매핑
- 워크로드가 한 AZ에 몰리지 않도록 Pod anti-affinity / topology spread 적용
- 트래픽이 큰 워크로드는 전용 NAT GW를 고려(비용 증가)
핵심은 “NAT GW 한 대에 연결 시도가 몰리지 않게” 만드는 것입니다.
3) egress를 NAT GW에만 의존하지 않는 아키텍처
- 외부 SaaS가 아니라 AWS 서비스 호출이 대부분이면 NAT GW 대신 VPC Endpoint(Interface/Gateway) 로 우회하여 인터넷 egress 자체를 줄입니다.
- 예: Secrets Manager, ECR, S3, STS 등.
Secrets Manager 호출이 간헐 실패/403 등으로 보일 때도, 실제로는 egress 경로 불안정이 원인인 경우가 있어 EKS Pod에서 AWS Secrets Manager 403 해결 가이드와 함께 “네트워크 경로(Endpoint/NAT)” 관점으로 재점검하는 것이 좋습니다.
4) conntrack 한계에 대한 운영적 대응
- 노드 타입/커널 파라미터/conntrack max 상향
- 커넥션이 폭증하는 워크로드를 노드풀로 분리
- CNI/iptables 룰이 과도하게 증가하지 않는지 점검
다만 conntrack 튜닝은 부작용(메모리 사용 증가, 장애 시 복구 복잡성)이 있어 연결 패턴 개선/NAT 분산보다 후순위로 두는 편이 안전합니다.
재현과 검증: “고쳤다”를 수치로 증명하기
해결 후에는 다음을 전/후 비교로 남겨두면 재발 시 추적이 빨라집니다.
- NAT GW:
ErrorPortAllocation이 0으로 유지되는지 - NAT GW:
ConnectionAttemptCount가 감소했는지 - 노드:
conntrack -C피크가 낮아졌는지 - 애플리케이션: 외부 API 호출 p95/p99, 타임아웃 비율
간단한 부하 생성(주의: 프로덕션에서 조심)
# Pod 내부에서 짧은 연결을 의도적으로 많이 만들어 포화 조건을 재현
kubectl exec -it <pod> -- bash -lc '
for i in {1..200}; do
(curl -sS --connect-timeout 1 https://api.example.com/healthz >/dev/null || echo fail)&
done
wait
'
재현이 되면 원인 규명은 훨씬 쉬워집니다. 재현이 안 되면, 장애 시간대의 메트릭/로그를 기반으로 “그때만 터지는 조건(AZ 편향, 배치 작업, 스케일 이벤트)”을 찾아야 합니다.
마무리: 간헐 egress 장애는 ‘연결’과 ‘포트’의 문제인 경우가 많다
EKS에서 Pod egress만 간헐적으로 끊길 때는, 애플리케이션 오류처럼 보여도 실제로는 SNAT 포트(특히 NAT GW)와 conntrack 상태가 원인인 경우가 매우 많습니다.
정리하면 다음 순서가 가장 효율적입니다.
- 실패를 DNS/TCP/TLS로 분해해 증상을 고정
- NAT GW
ErrorPortAllocation과 연결 시도량 스파이크 확인 - 노드 conntrack 포화/드랍 로그 확인
- AZ/라우팅/분산 상태 점검
- 해결은 “연결 재사용 → NAT 분산 → Endpoint 도입 → conntrack 튜닝” 우선순위로
이 흐름대로 추적하면, “가끔 끊겨요” 같은 모호한 장애를 측정 가능한 네트워크 병목으로 바꿔서 재발 방지까지 연결할 수 있습니다.