Published on

EKS에서 Pod egress만 502? Envoy/NLB 추적기

Authors

서버가 502를 뿜으면 보통은 인그레스(ALB/Nginx)부터 의심합니다. 그런데 EKS에서 인그레스는 멀쩡한데 특정 Pod의 egress(아웃바운드)만 502가 간헐적으로 터지는 케이스가 있습니다. 특히 서비스 메시에 들어가 있거나(Envoy sidecar), egress gateway를 통해 외부로 나가는 구조, 혹은 사내/외부 API 앞단이 NLB/Envoy 기반일 때 이 현상이 자주 보입니다.

이 글은 “502가 어디서 생성되는지”를 먼저 확정하고, Pod → Envoy → (NAT/IGW/VPC Endpoint) → NLB → Upstream 경로를 따라가며 증거 기반으로 원인을 좁히는 방법을 다룹니다. 단순 체크리스트가 아니라, 실제로 로그/메트릭/패킷/타임아웃 값을 맞춰가며 재현과 검증을 반복하는 흐름입니다.

관련해서 EKS에서 502/504가 TLS 핸드셰이크에서 터지는 케이스도 많습니다. 인그레스 관점 진단은 아래 글을 함께 보면 교차검증에 도움이 됩니다.

문제 정의: “Pod egress만 502”의 전형적 증상

다음 중 2개 이상이면 이 글의 시나리오에 가깝습니다.

  • 같은 클러스터에서 다른 Pod는 정상인데 특정 Deployment/Node/네임스페이스에서만 502
  • 애플리케이션 로그에는 upstream 응답이 없고, 클라이언트(혹은 SDK)에서 502 Bad Gateway만 관측
  • 인그레스 트래픽(외부→서비스)은 정상이나, 서비스→외부 API 호출에서만 502
  • 재시도하면 대부분 성공(간헐적), 혹은 특정 시간대/부하에서만 급증
  • Envoy(istio/proxy/ambassador 등) 또는 NLB 앞단이 관여

여기서 중요한 포인트는 **502는 “게이트웨이/프록시가 만든 응답”**이라는 점입니다. 즉, 애플리케이션이 502를 직접 내보내는 경우도 있지만(프레임워크가 프록시 역할을 할 때), 대개는 Envoy/NLB/프록시 계층이 upstream 연결 실패를 502로 번역합니다.

1) 502의 생성 주체를 먼저 확정하기

가장 먼저 해야 할 일은 “누가 502를 만들었는지”를 확정하는 것입니다. 이게 안 되면 원인 추적이 계속 흔들립니다.

1-1. 응답 헤더로 프록시 식별

Pod 내부에서 직접 호출해 헤더를 확인합니다.

kubectl exec -it deploy/myapp -c app -- sh

# HTTP/1.1
curl -sv https://api.example.com/v1/foo -o /dev/null

# gRPC라면 grpcurl로 상태 확인(가능한 경우)
# grpcurl -vv api.example.com:443 list

관찰 포인트:

  • server: envoy / x-envoy-upstream-service-time / x-envoy-decorator-operation 등이 보이면 Envoy가 응답 생성 또는 중간에 개입
  • via, x-amzn-trace-id, x-amz-cf-id 등이 보이면 AWS 계층 또는 CDN/프록시 가능성

다만 egress에서 TLS 종단이 외부에 있고 중간 프록시가 L4라면 헤더만으로 부족합니다. 이때는 Envoy access log / stats가 결정적입니다.

1-2. Envoy access log에서 502의 이유를 확인

Istio/Envoy 계열이라면 사이드카 컨테이너 로그부터 봅니다.

# istio-proxy 예시
kubectl logs -n myns deploy/myapp -c istio-proxy --tail=200

Envoy는 502를 다양한 플래그로 남깁니다. 자주 보는 패턴:

  • UF (Upstream connection failure)
  • UO (Upstream overflow)
  • URX (Upstream retry limit exceeded)
  • UC (Upstream connection termination)
  • LH (Local healthcheck failed)

access log 포맷에 따라 다르지만, 핵심은 upstream_transport_failure_reason 혹은 response_flags를 보는 것입니다.

1-3. Envoy admin stats로 “어디서 실패하는지” 정량화

Envoy는 15000(또는 15090) 포트로 admin/stats를 노출하는 경우가 많습니다.

# 포트포워딩
kubectl port-forward -n myns pod/myapp-xxxx 15000:15000

# 클러스터/업스트림 에러 카운터 확인
curl -s http://127.0.0.1:15000/stats | egrep 'upstream_cx_connect_fail|upstream_rq_5xx|upstream_cx_destroy|tls'

upstream_cx_connect_fail이 증가하면 “연결 자체가 안 됨”, upstream_rq_5xx가 증가하면 “연결은 됐는데 upstream이 5xx”일 가능성이 큽니다.

2) 네트워크 경로를 “L3/L4/L7”로 분해하기

“Pod egress만 502”는 다음 3계층 중 어디가 깨졌는지로 분류하면 빠릅니다.

  • L3 (IP 라우팅/보안그룹/NACL/라우트): 아예 못 나감 → 보통은 타임아웃/연결 실패가 많고 502는 프록시가 번역한 결과
  • L4 (TCP/TLS): SYN/ACK, TLS handshake, keepalive, idle timeout
  • L7 (HTTP/gRPC): 헤더/프레임 크기, 업스트림 리셋, H2 설정 불일치

특히 Envoy가 개입하면 L4 문제도 L7에서 502로 보일 수 있습니다(예: TLS handshake 실패 → Envoy가 502로 반환).

3) NLB 구간에서 자주 터지는 원인 6가지

NLB는 L4 로드밸런서지만, 실제 장애는 타임아웃/커넥션 재사용/대상 그룹 상태/Proxy Protocol/TLS 종단의 조합에서 많이 납니다.

3-1. NLB Idle timeout/Keepalive 불일치

증상:

  • 일정 시간 유휴 후 첫 요청이 502/연결 리셋
  • 재시도하면 바로 성공

설명:

  • 클라이언트(Envoy)가 keepalive로 커넥션을 재사용하려고 하는데, 중간(NLB/방화벽/업스트림)이 idle timeout으로 커넥션을 끊어버린 상태면 첫 요청에서 RST가 날 수 있습니다.

대응:

  • Envoy의 upstream keepalive/connection pool 설정을 조정하거나, 애플리케이션 HTTP 클라이언트의 keepalive를 조정합니다.
  • 가능하면 upstream(서버) 측 keepalive timeout과도 맞춥니다.

3-2. Target 그룹 헬스체크는 통과하지만 실제 트래픽은 실패

증상:

  • NLB target은 healthy인데 실제 요청은 간헐적으로 502

원인 후보:

  • 헬스체크 경로/포트는 열려 있으나, 실제 서비스 포트는 과부하/큐 포화
  • 서버가 SYN은 받지만 accept backlog가 꽉 차서 연결이 실패

검증:

  • 업스트림 서버의 netstat -s, ss -s, accept queue, CPU steal, conntrack, 애플리케이션 스레드풀/이벤트루프 지표 확인

3-3. Proxy Protocol 설정 불일치

NLB에서 Proxy Protocol v2를 켰는데 업스트림(Envoy/서버)이 이를 기대하지 않거나 반대인 경우, 초기 바이트가 깨져서 연결이 실패할 수 있습니다.

검증:

  • NLB 리스너/타깃 그룹의 Proxy Protocol 설정 확인
  • 업스트림 Envoy listener에 use_proxy_proto 설정 여부 확인

3-4. TLS 종단 위치 혼동(SNI/ALPN/H2)

Pod에서 HTTPS/gRPC로 나가는데, 중간에 Envoy가 TLS를 종단하거나 재암호화할 때 설정이 어긋나면 502로 보입니다.

체크 포인트:

  • SNI가 올바르게 전달되는지
  • gRPC라면 ALPN h2 협상이 되는지
  • 업스트림이 HTTP/2를 기대하는데 HTTP/1.1로 나가거나 그 반대

참고로 gRPC에서 “413 없이 502” 같은 현상은 프록시의 메시지 크기 제한이 502로 번역되는 경우도 있습니다.

3-5. 소스 NAT/conntrack 고갈로 인한 간헐 실패

EKS Pod egress가 NAT Gateway를 타는 경우, 특정 시간대에 동시 연결이 폭증하면 conntrack/NAT 포트가 부족해져서 연결 실패가 증가할 수 있습니다. 이 실패가 Envoy에서 502로 보일 수 있습니다.

검증:

  • 노드에서 conntrack 테이블 사용량 확인(노드 접근 가능 시)
  • NAT Gateway CloudWatch metrics(ActiveConnectionCount, ErrorPortAllocation 등) 확인

대응:

  • NAT GW 확장(서브넷 분산), egress 분산, 연결 재사용 최적화, 불필요한 단기 커넥션 줄이기

3-6. DNS/엔드포인트 변경과 커넥션 캐시

외부 API가 DNS 기반으로 엔드포인트를 바꾸거나(블루/그린), NLB의 IP가 바뀌는 상황에서 DNS TTL과 클라이언트의 캐시 정책이 충돌하면 특정 Pod만 오래된 IP로 붙어서 실패할 수 있습니다.

검증:

kubectl exec -it deploy/myapp -c app -- sh

# Pod 내부에서 반복 조회
for i in $(seq 1 5); do
  date
  nslookup api.example.com
  sleep 2
done

Envoy/애플리케이션의 DNS refresh 주기도 함께 확인해야 합니다.

4) “Pod만” 문제일 때: 노드/서브넷/보안 경계 확인

같은 Deployment라도 특정 노드에서만 502가 난다면, 애플리케이션보다 노드/네트워크 경계가 원인일 확률이 큽니다.

4-1. 실패 Pod가 특정 노드에 몰리는지 확인

kubectl get pod -n myns -o wide

# 노드별로 실패율을 대략 매핑(로그 기반)
kubectl logs -n myns deploy/myapp -c istio-proxy --since=10m | grep ' 502 ' | head

노드 편향이 보이면 다음을 확인합니다.

  • 노드 보안그룹 egress 규칙(특정 목적지/포트 차단 여부)
  • 서브넷 라우트 테이블(특정 AZ만 NAT 경로가 다르거나 누락)
  • NACL(특정 ephemeral port 범위 차단)

4-2. MTU/조각화 이슈(특히 TLS)

VPC CNI, 터널링, 보안 장비가 얽히면 MTU가 애매해지고, 큰 TLS 레코드/큰 패킷에서만 문제가 터질 수 있습니다. 이 경우도 상위에서는 “간헐 502”로 보일 수 있습니다.

간단한 힌트:

  • 작은 요청은 성공, 큰 페이로드/응답에서만 실패
  • gRPC 스트리밍/큰 JSON에서만 실패

검증은 tcpdump가 가장 빠르지만, 권한이 없으면 애플리케이션 레벨에서 페이로드 크기별 성공률을 비교해도 단서가 됩니다.

5) Envoy 관점에서 가장 효과 좋은 3가지 증거 수집

5-1. Envoy 로그 레벨 일시 상향

Istio라면 istioctl proxy-configproxy 로그 레벨을 올릴 수 있습니다(운영에서는 짧게).

# 예시: istioctl이 있는 환경에서
istioctl proxy-config log pod/myapp-xxxx -n myns --level http:debug,router:debug,upstream:debug

이후 동일 요청을 재현하고 upstream connect error, TLS error, reset reason 같은 문구를 확보합니다.

5-2. 특정 클러스터(업스트림) 단위로 실패율 확인

Envoy는 upstream cluster 단위로 지표가 나뉩니다. 외부 API가 여러 개라면 “어느 목적지에서만” 502가 나는지 분리합니다.

curl -s http://127.0.0.1:15000/stats | egrep 'cluster\..*upstream_rq_5xx|cluster\..*upstream_cx_connect_fail' | head -n 50

5-3. 재시도 정책이 문제를 숨기거나 증폭시키는지 확인

재시도는 관측을 왜곡합니다.

  • 재시도 ON: 사용자는 성공으로 보지만, 내부적으로 502가 다량 발생 가능
  • 재시도 OFF: 실패가 그대로 드러나 원인 파악은 쉬워짐

재시도 횟수/조건을 확인하고, 장애 분석 시간에는 일시적으로 재시도를 줄여 신호를 선명하게 만드는 것도 방법입니다(단, 사용자 영향 고려).

6) 재현이 어려울 때: 최소 재현용 egress 테스트 Pod 만들기

애플리케이션이 복잡하면 원인 분리가 늦어집니다. 동일 네임스페이스/사이드카 조건에서 “curl만 하는 Pod”를 띄워 비교합니다.

apiVersion: v1
kind: Pod
metadata:
  name: egress-debug
  namespace: myns
  labels:
    app: egress-debug
spec:
  containers:
    - name: curl
      image: curlimages/curl:8.6.0
      command: ["sh", "-c", "sleep 36000"]
  restartPolicy: Never
kubectl apply -f egress-debug.yaml
kubectl exec -it -n myns pod/egress-debug -- sh

# 반복 호출로 실패율 측정
for i in $(seq 1 200); do
  code=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health)
  echo "$i $code"
  sleep 0.2
done

여기서도 502가 나면 앱 문제가 아니라 네임스페이스/사이드카/네트워크 경로 문제로 좁혀집니다.

7) 실전 해결 패턴: 원인별 처방전

아래는 현장에서 자주 “정답”이었던 조합들입니다.

7-1. (가장 흔함) TLS handshake/ALPN 문제 → Envoy 클러스터 TLS 설정 정합

  • SNI 누락/오류, SAN 불일치, 업스트림이 h2만 받는데 h1로 연결
  • 해결: Envoy cluster의 sni, alpn_protocols, transport_socket 설정을 목적지에 맞게 수정

TLS 계열 진단은 인그레스뿐 아니라 egress에서도 동일한 원리로 접근할 수 있습니다.

7-2. NAT/conntrack 병목 → 연결 재사용 + egress 분산

  • 짧은 커넥션을 남발하는 SDK/HTTP 클라이언트가 있으면 keepalive를 적극 활용
  • 워커 노드/서브넷/AZ에 egress가 한쪽으로 쏠리지 않게 구성

7-3. NLB idle timeout/중간 장비 세션 만료 → keepalive 튜닝

  • Envoy/클라이언트 keepalive 주기를 더 짧게(중간 만료보다 먼저 ping)
  • 혹은 커넥션 풀의 max connection age/idle을 줄여 “죽은 커넥션 재사용”을 방지

7-4. DNS 변화 대응 → DNS refresh/TTL/캐시 정책 통일

  • Envoy의 DNS refresh rate 확인
  • 애플리케이션 런타임(예: Java DNS 캐시, Go net.Resolver, Python aiohttp 커넥터 캐시) 정책 확인

8) 체크리스트: 30분 안에 범인 좁히기

운영 중 빠르게 수습할 때는 아래 순서가 효율적입니다.

  1. Pod 내부 curl로 502 재현 (가능하면 실패율/시간대 기록)
  2. 응답 헤더로 server: envoy 등 확인
  3. Envoy access log에서 response_flags 확보 (UF/UC/URX 등)
  4. Envoy stats에서 upstream_cx_connect_fail vs upstream_rq_5xx연결 실패 vs 업스트림 5xx 분기
  5. 노드 편향 확인(kubectl get pod -o wide)
  6. NLB 설정(Proxy Protocol/TLS/타깃 상태) 및 NAT/conntrack 지표 확인
  7. DNS/TTL/캐시 확인

이 흐름대로 하면 “애플리케이션 버그”로 몰고 가는 시간을 크게 줄일 수 있습니다. 특히 502는 결과일 뿐 원인이 아니다라는 점을 계속 상기해야 합니다. Envoy/NLB가 502를 만들었다면, 그 직전의 실패(연결/TLS/리셋/타임아웃)를 로그와 지표로 잡아내는 것이 핵심입니다.

마무리

EKS에서 Pod egress만 502가 나는 문제는 대개 프록시(Envoy)가 upstream 연결 실패를 502로 번역하면서 시작됩니다. 따라서 “왜 502냐”보다 “어떤 실패가 502로 번역됐냐”를 찾아야 합니다.

  • Envoy access log의 response_flags와 stats의 connect_fail/rq_5xx는 가장 빠른 분기점
  • NLB는 L4라서 단순해 보이지만, idle timeout/Proxy Protocol/TLS/대상 과부하 같은 현실적인 함정이 많음
  • 노드/AZ 편향이 보이면 보안그룹/NACL/라우트/NAT 병목을 우선 의심

다음 글에서는 실제 Envoy 설정 예시(egress cluster, SNI/ALPN, retry/timeout)와 함께, 502를 재현하는 테스트 매트릭스(요청 크기/프로토콜/keepalive/동시성)까지 묶어 더 체계적으로 정리해보겠습니다.