Published on

EKS ALB Ingress 502/504 - TLS 핸드셰이크 실패 진단

Authors

서버가 살아있는데도 ALB Ingress가 502 Bad Gateway 또는 504 Gateway Timeout을 뱉는 상황은 EKS 운영에서 자주 만납니다. 특히 ALB가 Pod(타깃)로 연결하는 구간에서 TLS 핸드셰이크가 실패하면, 애플리케이션 로그에는 아무것도 남지 않거나(요청이 도달하지 않음) 헬스체크만 간헐적으로 실패하는 형태로 나타납니다.

이 글은 “클라이언트→ALB”가 아니라 “ALB→백엔드(Pod/NodePort)” TLS 관점에서, 502/504의 원인을 빠르게 좁히는 실전 진단 절차를 다룹니다.

> 참고: 네트워크/노드 상태가 먼저 의심된다면, 노드/보안그룹/CNI 기본 점검은 아래 글이 선행 지식으로 좋습니다. > - Terraform apply 후 EKS 노드 NotReady - CNI·IRSA·보안그룹 점검

1) 증상 패턴: 502 vs 504, 그리고 “TLS가 의심되는” 흔적

502가 많은 케이스

  • ALB가 타깃에 연결은 했지만 응답을 정상적으로 받지 못함
  • 타깃 그룹 상태가 unhealthy로 바뀌며, 이유가 Target.ResponseCodeMismatch가 아니라 handshake/connection 계열인 경우
  • 애플리케이션 컨테이너 로그에 요청 흔적이 없음(핸드셰이크 단계에서 종료)

504가 많은 케이스

  • ALB가 타깃에 연결/요청 후 응답을 기다리다 타임아웃
  • 간헐적일 때는 재시도/커넥션 재사용/헬스체크 주기와 맞물려 “가끔만” 터짐

TLS 핸드셰이크 실패일 때 자주 보이는 현상

  • Pod는 Ready인데 타깃 그룹 헬스체크가 계속 실패
  • Ingress 설정을 바꾸지 않았는데도 인증서 교체/사이드카 프록시 설정 변경 후 발생
  • 동일 서비스에 HTTP로 바꾸면 정상, HTTPS(백엔드 TLS)로 바꾸면 502/504

2) 먼저 구조를 명확히: 어떤 구간에서 TLS가 일어나는가

ALB Ingress(aws-load-balancer-controller)에서 TLS는 최소 두 구간에 존재할 수 있습니다.

  1. 클라이언트 → ALB (프론트 TLS)
  • ACM 인증서(대개)로 종료(terminate)
  • Ingress의 alb.ingress.kubernetes.io/certificate-arn
  1. ALB → 백엔드 타깃 (백엔드 TLS)
  • 타깃 그룹 프로토콜이 HTTPS인 경우
  • 이때 ALB는 백엔드로 TLS 핸드셰이크를 수행

이번 글의 핵심은 2)입니다. 즉, “외부에서 ALB까지는 HTTPS가 잘 되는데, ALB가 Pod로 붙을 때 TLS가 깨지는” 상황을 다룹니다.

3) 가장 흔한 원인 Top 6 (체크리스트)

원인 A) 타깃 그룹 프로토콜/포트 불일치 (HTTP로 열어놓고 ALB는 HTTPS로 접속)

  • Service/Pod는 8080 HTTP인데, Ingress annotation으로 backend-protocol을 HTTPS로 설정
  • 또는 반대로 Pod는 8443 HTTPS인데 타깃 그룹이 HTTP

확인 포인트

  • Ingress annotation: alb.ingress.kubernetes.io/backend-protocol
  • Service targetPort/containerPort
  • 타깃 그룹의 Port

원인 B) 헬스체크 프로토콜/경로가 TLS 요구사항과 충돌

  • 백엔드가 /healthz를 HTTP로만 제공하는데, 헬스체크를 HTTPS로 때림
  • 혹은 HTTPS인데 SNI/Host가 없으면 400/421/495 등으로 실패

확인 포인트

  • alb.ingress.kubernetes.io/healthcheck-protocol
  • alb.ingress.kubernetes.io/healthcheck-path
  • alb.ingress.kubernetes.io/success-codes

원인 C) 백엔드 인증서 체인/키 사용 문제(중간 인증서 누락, 키 불일치)

  • Pod가 제공하는 서버 인증서에 Intermediate CA가 누락되어 클라이언트(여기서는 ALB)가 검증 실패
  • 인증서 갱신 시 private key mismatch

특징

  • curl/openssl로 직접 붙으면 unable to get local issuer certificate
  • 일부 클라이언트는 통과하지만(ALPN/체인 캐시) ALB는 실패

원인 D) SNI(서버 이름 표시) 미스매치 또는 멀티 인증서 구성 오류

  • 백엔드가 SNI 기반으로 인증서를 선택하는데, ALB가 기대하는 서버 이름과 다름
  • Ingress host와 백엔드 TLS 서버 이름이 다르거나, Envoy/Nginx가 server_name에 매칭 실패

원인 E) 보안그룹/네트워크 정책으로 인해 TLS 핸드셰이크 패킷이 드롭

  • TCP 3-way handshake는 되는데, 이후 TLS 레코드/MTU/재전송이 막히는 형태
  • SG/NACL, 노드 방화벽, CNI 정책, eBPF 정책 등

원인 F) HTTP/2(gRPC) 등 프로토콜 기대치 불일치

  • 백엔드는 gRPC(HTTP/2 over TLS)만 받는데, ALB가 HTTP/1.1로 프록시하거나 반대
  • ALB 리스너/타깃 그룹 프로토콜 버전 설정 불일치

4) 증거 수집: “ALB가 TLS에서 죽는다”를 어떻게 확정할까

4.1 ALB 타깃 그룹 상태/이벤트 확인

AWS 콘솔에서 Target Group → Targets → 상태 메시지를 봅니다. 메시지가 직접적으로 “TLS handshake”를 말해주진 않지만, 아래처럼 연결 계열 실패가 지속되면 강하게 의심할 수 있습니다.

  • Health checks failing
  • Unhealthy reason이 Target.Timeout, Target.FailedHealthChecks 등으로 반복

CloudWatch Access Log(활성화 시)에서도 502/504와 타깃 응답 부재가 관찰됩니다.

4.2 Pod로 직접 붙어서 TLS 핸드셰이크 재현

ALB를 거치지 말고, 클러스터 내부에서 Pod/Service로 직접 붙어 TLS가 성립하는지 확인합니다.

(1) 임시 디버그 Pod 생성

kubectl run -it --rm netshoot \
  --image=nicolaka/netshoot \
  --restart=Never -- bash

(2) Service DNS로 openssl 테스트

# HTTPS 백엔드라고 가정
openssl s_client -connect my-svc.my-namespace.svc.cluster.local:443 \
  -servername my.example.com \
  -showcerts -verify 5
  • -servername는 SNI 확인에 중요합니다.
  • 출력에서 Verify return code: 0 (ok)가 아니면 인증서 체인/이름 문제가 있을 확률이 큽니다.

(3) curl로 ALPN/HTTP 버전까지 확인

curl -vk https://my-svc.my-namespace.svc.cluster.local/healthz \
  --resolve my.example.com:443:10.0.0.123 \
  -H 'Host: my.example.com'
  • --resolve는 특정 IP로 강제 라우팅할 때 유용합니다.
  • -H 'Host: ...'는 SNI/가상호스트 기반 서버에서 중요합니다.

4.3 Envoy/Nginx/애플리케이션 TLS 로그 확인

백엔드가 Nginx/Envoy/Ingress-Nginx가 아니라 “앱 자체 TLS”일 수도 있습니다. 이때는 앱 로그보다 TLS 종료 지점의 로그 레벨을 올려야 합니다.

  • Nginx: error_log ... debug;
  • Envoy: --log-level debug, TLS handshake failure 카운트

애플리케이션이 Java/Spring이라면 JVM TLS 디버그(-Djavax.net.debug=ssl:handshake)가 결정적일 수 있지만, 운영에서는 노이즈가 크니 단기 재현 환경에서 권장합니다.

5) 설정 점검: aws-load-balancer-controller Ingress에서 자주 틀리는 부분

아래는 백엔드 TLS를 쓰는 전형적인 Ingress 예시입니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
  namespace: app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip

    # (중요) ALB -> 백엔드로 HTTPS
    alb.ingress.kubernetes.io/backend-protocol: HTTPS

    # 헬스체크도 HTTPS로 할지, HTTP로 할지 백엔드 구현에 맞춰야 함
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTPS
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/success-codes: "200"

spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 443

여기서 실패가 난다면 다음 질문으로 좁힙니다.

  • api-svc:443은 정말 TLS를 제공하는가?
  • 인증서 CN/SAN이 api.example.com에 매칭되는가?
  • intermediate chain이 포함되어 있는가?
  • 헬스체크 /healthz가 TLS/SNI/Host 요구사항을 만족하는가?

6) 원인별 처방전(수정 방향)

6.1 프로토콜/포트 불일치 해결

  • 백엔드가 HTTP라면:
    • alb.ingress.kubernetes.io/backend-protocol: HTTP
    • Service port를 80/8080 등으로 맞춤
  • 백엔드가 HTTPS라면:
    • Service/Pod가 실제로 TLS 리스닝(예: 8443)
    • Ingress의 backend-protocol, Service port를 일치

6.2 헬스체크만 HTTP로 분리하기

백엔드는 외부 트래픽은 HTTPS로 받고, 헬스체크 엔드포인트는 HTTP로 별도 제공하는 구성이 운영에서 흔합니다.

  • 앱에서 :8080/healthz는 HTTP
  • 메인 트래픽은 :8443 HTTPS

이 경우 타깃 그룹을 분리하거나(권장), 최소한 헬스체크 프로토콜/포트를 백엔드에 맞춰야 합니다. ALB 컨트롤러에서 헬스체크 포트도 지정 가능합니다.

6.3 인증서 체인 문제 해결(중간 인증서 포함)

서버가 제공하는 인증서 번들(fullchain)을 올바르게 구성합니다.

  • Let’s Encrypt: fullchain.pem + privkey.pem
  • ACM을 백엔드에 쓰는 게 아니라면(대부분 아님), Pod의 TLS 시크릿이 “leaf만” 들어있지 않은지 확인

Kubernetes TLS secret 점검:

kubectl -n app get secret api-tls -o yaml

그리고 실제 서빙 체인은 openssl s_client -showcerts로 확인합니다.

6.4 SNI/Host 요구사항 맞추기

백엔드가 SNI 없으면 기본 인증서를 내보내거나 연결을 끊는다면, ALB가 어떤 SNI를 보내는지/백엔드가 무엇을 기대하는지 맞춰야 합니다.

  • 단일 도메인만 처리하도록 단순화(가능하면)
  • Nginx/Envoy의 server_name/filter chain을 Ingress host와 일치

6.5 네트워크/보안그룹 점검(특히 target-type=ip)

target-type: ip면 ALB가 Pod IP로 직접 붙습니다. 이때 보안그룹은 보통 노드 SG가 아니라 Pod ENI/노드 경로를 따라가며, 설정에 따라 인바운드가 막힐 수 있습니다.

  • ALB SG → 노드/Pod로의 인바운드 허용(해당 포트)
  • NACL, 보안그룹 참조(SG referencing) 확인

또한 IP가 부족하거나 CNI가 불안정하면 “간헐적” 실패가 섞여 보일 수 있습니다. IP 고갈 이슈가 의심되면 아래 글도 같이 보세요.

7) 빠른 트러블슈팅 플로우(10~20분 내 결론)

Step 1. 타깃 그룹 상태 확인

  • 타깃이 전부 unhealthy인가, 일부만 unhealthy인가
  • unhealthy가 일부면 특정 노드/서브넷/파드에 국한되는지

Step 2. 클러스터 내부에서 백엔드 TLS 재현

  • openssl s_client로 체인/이름/SNI 확인
  • curl -vk로 실제 응답 확인

Step 3. Ingress annotation과 Service port 정합성 확인

  • backend-protocol/healthcheck-protocol/healthcheck-path/port

Step 4. 인증서 체인/갱신 이력 확인

  • 최근 cert-manager/시크릿 교체가 있었는지
  • fullchain 제공 여부

Step 5. 네트워크 경로 확인

  • SG/NACL/네트워크 정책/노드 방화벽

8) 재발 방지: 운영 가드레일

8.1 배포 파이프라인에서 “백엔드 TLS 검증” 자동화

배포 직후, 클러스터 내부에서 백엔드 서비스에 대해 openssl s_client 검증을 자동화하면 인증서 체인 누락 같은 실수를 빠르게 잡습니다.

예: Kubernetes Job로 검증(개념 예시)

apiVersion: batch/v1
kind: Job
metadata:
  name: tls-smoke-test
  namespace: app
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: test
          image: alpine:3.20
          command: ["/bin/sh", "-c"]
          args:
            - |
              apk add --no-cache openssl curl;
              echo | openssl s_client -connect api-svc.app.svc.cluster.local:443 -servername api.example.com -verify 5;
              curl -skf https://api-svc.app.svc.cluster.local/healthz || exit 1;

8.2 타임아웃/재시도 설계(클라이언트 관점)

ALB 504가 사용자 장애로 번지지 않게 하려면, 클라이언트 재시도/타임아웃도 중요합니다. 애플리케이션에서 외부 호출이 섞여 있다면 아래 글의 재시도 설계가 도움이 됩니다.

마무리

EKS에서 ALB Ingress의 502/504는 “앱이 죽어서”가 아니라 ALB가 백엔드에 붙는 과정(특히 TLS 핸드셰이크)에서 실패해서 발생하는 경우가 많습니다. 핵심은 감으로 추측하지 않고,

  1. 타깃 그룹/헬스체크 상태로 증상을 분류하고,
  2. 클러스터 내부에서 openssl/curl로 TLS를 재현해 증거를 만들고,
  3. Ingress의 backend-protocol/헬스체크/인증서 체인/SNI/보안그룹을 정합성 있게 맞추는 것

입니다.

위 플로우대로 하면 대부분 10~20분 안에 “TLS가 맞냐/아니냐”, “체인이냐/포트냐/SNI냐”까지는 결론을 낼 수 있고, 그 다음 수정은 비교적 단순해집니다.