Published on

EKS ALB Ingress 502 Target reset 원인과 해결

Authors

서버가 살아 있는데도 ALB Ingress가 502 Bad Gateway를 반환하면서 ALB 액세스 로그나 콘솔에서 Target reset(혹은 유사한 reset 계열 메시지)이 보이면, **“ALB가 타깃(노드/Pod)과 TCP 연결은 맺었지만 응답을 받기 전에 타깃이 연결을 끊었다”**는 신호인 경우가 많습니다.

문제는 여기서 끝나지 않습니다. EKS에서는 Ingress(=ALB), Service(NodePort/ClusterIP), Pod, CNI, 보안그룹, 타임아웃이 체인처럼 연결되어 있어 어느 한 지점의 불일치가 reset으로 표면화됩니다. 이 글은 “무조건 설정을 바꿔보자”가 아니라, 로그/상태 기반으로 원인을 좁혀가는 체크리스트 + 바로 적용 가능한 설정 예시를 제공합니다.

> 참고로 네트워크/보안그룹 관점에서 NodePort 경로가 꼬이는 케이스도 많습니다. 유사 증상 점검은 EKS에서 NodePort만 안 열릴 때 CNI·SG 점검도 함께 보면 진단 속도가 빨라집니다.

1) 증상 정의: “502 + Target reset”이 의미하는 것

ALB 기준으로 Target reset은 보통 아래 중 하나로 귀결됩니다.

  1. ALB → 타깃(TG) 연결이 성립했으나 타깃이 RST/FIN로 먼저 끊음
  2. 헬스체크는 통과/실패를 반복하지만 실제 트래픽 경로는 다른 포트/프로토콜로 들어감
  3. idle timeout/keep-alive/HTTP2 설정 불일치로 중간에 연결이 정리됨
  4. Pod/애플리케이션이 요청을 처리하다가 크래시/리스타트/커넥션 드롭

특히 EKS에서 흔한 패턴은 다음입니다.

  • Ingress는 80으로 받는데, Service는 8080으로 포워딩, 컨테이너는 3000 리슨… 이런 포트 체인 불일치
  • target-type: instance인데 Service가 ClusterIP만 있고 NodePort 경로가 열려 있지 않음
  • target-type: ip인데 Pod IP로 직접 붙으면서 보안그룹/네트워크 정책/리드니스가 엇갈림

2) 가장 먼저 확인할 5가지(시간 절약용)

2.1 ALB 타깃 그룹(Target Group) 상태

AWS 콘솔 또는 CLI로 TG 타깃 상태를 확인합니다.

  • healthy가 안정적으로 유지되는가?
  • unhealthy 사유가 Health checks failed인지, Target.Timeout인지, Target.ResponseCodeMismatch인지?

CLI 예시:

aws elbv2 describe-target-health \
  --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/xxx

2.2 Ingress 이벤트: AWS Load Balancer Controller가 뭘 만들었나

kubectl describe ingress -n <ns> <name>
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -n 50

Ingress 이벤트에 failed build model, security group, target group 관련 힌트가 자주 있습니다. 컨트롤러 권한/403류 이슈가 의심되면 EKS에서 AWS Load Balancer Controller 403 해결법도 함께 확인하세요.

2.3 Service 타입과 target-type 매칭

  • alb.ingress.kubernetes.io/target-type: instance

    • ALB → NodePort로 들어갑니다.
    • Service는 NodePort(또는 LoadBalancer지만 내부적으로 NodePort 경유)여야 하고, 노드 SG에서 NodePort가 열려야 합니다.
  • alb.ingress.kubernetes.io/target-type: ip

    • ALB → Pod IP로 직접 들어갑니다.
    • Pod SG(보안그룹 for pods) 또는 노드 SG/서브넷 라우팅/네트워크 정책이 맞아야 합니다.

2.4 Pod Readiness/Endpoint가 실제로 붙어 있나

kubectl get endpoints -n <ns> <svc>
kubectl get pod -n <ns> -o wide
kubectl describe pod -n <ns> <pod>

엔드포인트가 비어 있거나, readiness 실패로 엔드포인트가 빠졌다 들어왔다 하면 ALB는 간헐적으로 reset을 경험할 수 있습니다.

2.5 애플리케이션 로그에서 “connection reset”, “upstream prematurely closed”

ALB가 아니라 Pod가 먼저 연결을 끊는 경우가 많습니다.

  • 앱이 SIGTERM 받고 종료 중인데 프리스트롭(PreStop) 없이 바로 커넥션을 끊음
  • 워커 수 부족/GC/메모리 압박으로 요청 처리 중 리스타트
kubectl logs -n <ns> deploy/<app> --since=10m
kubectl get pod -n <ns> -w

3) 대표 원인별 해결 패턴

3.1 원인 A: target-type=instance인데 NodePort 경로가 막힘

증상

  • TG 타깃이 노드로 잡히지만 unhealthy가 지속
  • 노드 보안그룹에서 NodePort 범위(기본 30000-32767) 인바운드가 없음
  • 특정 노드만 unhealthy (스케줄링/노드 교체 시 증상 변동)

해결

  1. Service가 NodePort로 노출되는지 확인
apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: default
spec:
  type: NodePort
  selector:
    app: myapp
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 31080
  1. Ingress에서 instance 타깃을 명시하고, 헬스체크 포트를 일치시킵니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
  namespace: default
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: instance
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/healthcheck-port: "traffic-port"
spec:
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp
                port:
                  number: 80
  1. 노드 SG 인바운드에 nodePort(예: 31080) 또는 범위(30000-32767)를 ALB SG 소스로 허용합니다.

> NodePort만 유독 안 열리는 전형적인 점검 흐름은 EKS에서 NodePort만 안 열릴 때 CNI·SG 점검에 더 자세히 정리되어 있습니다.

3.2 원인 B: target-type=ip인데 Pod로의 인바운드가 막힘(보안그룹/네트워크)

증상

  • TG 타깃이 Pod IP로 등록되지만 unhealthy
  • 같은 클러스터라도 특정 네임스페이스/노드에서만 실패
  • 보안그룹 for pods 사용 시, Pod SG 규칙 누락

해결

  • ip 타깃이면 ALB SG → Pod(혹은 노드)로의 인바운드가 허용되어야 합니다.
  • EKS 보안그룹 for pods를 쓴다면 Pod ENI에 붙은 SG에 인바운드를 열어야 합니다.

Ingress 예시(핵심은 target-type=ip):

metadata:
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/backend-protocol: HTTP
    alb.ingress.kubernetes.io/healthcheck-path: /healthz

추가로 Pod IP가 부족/불안정하면 타깃 등록이 들쑥날쑥해지고, 그 결과 reset/502가 간헐적으로 발생하기도 합니다. VPC CNI 문제 가능성이 있으면 EKS VPC CNI IP 누수로 Pod IP 고갈 해결하기도 같이 확인하세요.

3.3 원인 C: 헬스체크 경로/포트/성공코드 불일치

증상

  • 실제 서비스는 정상인데 TG만 unhealthy
  • /는 301/302 리다이렉트, /healthz는 200인데 헬스체크가 /로 되어 있음
  • 앱이 204를 반환하는데 성공코드가 200으로 고정

해결

헬스체크를 명시적으로 고정합니다.

metadata:
  annotations:
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/healthcheck-port: "traffic-port"
    alb.ingress.kubernetes.io/success-codes: "200-399"

또한 Service의 targetPort가 컨테이너 리슨 포트와 같은지 재확인합니다.

kubectl get svc -n default myapp -o yaml | yq '.spec.ports'
kubectl get deploy -n default myapp -o yaml | yq '.spec.template.spec.containers[].ports'

3.4 원인 D: 타임아웃/keep-alive 불일치로 인한 연결 리셋

증상

  • 짧은 요청은 OK, 긴 요청에서만 502
  • 간헐적으로만 발생(피크 타임/배포 직후)
  • 앱/프록시(예: NGINX, Envoy) 로그에 upstream timeout/close

해결

  1. ALB idle timeout 조정(기본 60초)
metadata:
  annotations:
    alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=120
  1. 애플리케이션/사이드카 keep-alive를 ALB와 맞추고, 장시간 요청은 비동기화(큐/웹훅)도 고려합니다.

  2. HTTP/2(gRPC 포함)라면 reset이 더 자주 관측될 수 있습니다. gRPC/HTTP2 레벨의 RST_STREAM 튜닝 포인트는 Kubernetes gRPC UNAVAILABLE·RST_STREAM 원인과 Envoy·NGINX 대응에서 다룹니다.

3.5 원인 E: Pod 종료/배포 시 연결 드롭(Graceful shutdown 미흡)

증상

  • 롤링 업데이트/오토스케일링 때만 502
  • kubectl rollout restart 직후 스파이크
  • Pod가 SIGTERM 후 즉시 종료하면서 기존 커넥션이 reset

해결

  1. terminationGracePeriodSeconds + preStop으로 드레이닝 시간을 확보
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: myapp:latest
          ports:
            - containerPort: 8080
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5
  1. readinessProbe가 실제 트래픽 수용 가능 시점에만 통과하도록 구성(예: DB 연결 완료 후 200)

  2. HPA/Cluster Autoscaler 환경에서는 노드 드레인 이벤트에도 동일한 문제가 발생할 수 있으니 PDB(DisruptionBudget)도 함께 검토합니다.

4) 실전 디버깅: “ALB에서 Pod까지” 패킷 흐름을 끊어보기

문제를 빠르게 좁히려면, 같은 요청을 각 홉에서 직접 호출해 봅니다.

4.1 Pod 내부에서 로컬 리슨 확인

kubectl exec -n default -it deploy/myapp -- sh -c 'netstat -lntp || ss -lntp'

4.2 클러스터 내부에서 Service로 호출

kubectl run -n default -it --rm curl --image=curlimages/curl -- \
  curl -sv http://myapp.default.svc.cluster.local/healthz

4.3 노드에서 NodePort로 호출(instance 타깃일 때)

노드에 SSM/SSH로 접속 가능하다면:

curl -sv http://127.0.0.1:31080/healthz

여기서 성공하면 앱/Service 체인은 정상이고, 실패하면 Kubernetes 내부(서비스/엔드포인트/리슨 포트) 문제로 좁혀집니다.

5) 자주 하는 실수 모음(체크리스트)

  • Ingress backend servicePort는 80인데 Service가 8080만 열어둠
  • readinessProbe 없이 livenessProbe만 둬서 “준비 안 됐는데 트래픽 유입”
  • target-type: instance인데 Service가 ClusterIP
  • ALB SG는 열었는데 노드 SG/Pod SG 인바운드를 안 열음
  • 헬스체크 경로가 인증 필요(/가 401/302)
  • 배포 시 SIGTERM 처리 없이 즉시 종료(특히 Node.js/Go에서 흔함)

6) 결론: 502 Target reset은 “불일치”의 신호다

EKS의 ALB Ingress에서 502 + Target reset은 ALB 자체 장애라기보다, 대개 아래 축 중 하나의 불일치에서 시작합니다.

  • 타깃 타입(instance/ip) ↔ Service 타입(NodePort/ClusterIP) ↔ 보안그룹
  • 헬스체크 경로/포트/성공코드 ↔ 실제 앱 동작
  • 타임아웃/keep-alive ↔ 앱/프록시 설정
  • 배포/스케일링 시 graceful shutdown ↔ readiness/draining

위의 순서대로 “TG 상태 → Ingress 이벤트 → Service/Endpoints → 앱 로그 → 타임아웃/종료 처리”를 밟으면, 대부분은 30분 내로 원인이 좁혀집니다.

원하시면 현재 사용 중인 Ingress/Service YAML, TG 상태(healthy/unhealthy reason), 그리고 ALB 액세스 로그의 해당 라인(개인정보 제거)을 주시면 케이스에 맞춰 정확히 어떤 불일치가 reset을 만드는지를 짚어서 수정안을 제안해 드릴게요.