Skip to content
Published on

Ingress 트러블슈팅 플레이북 — 502/504/404부터 TLS까지

Authors

들어가며

Ingress는 클러스터로 들어오는 트래픽의 첫 관문입니다. 그래서 무언가 잘못되면 사용자가 가장 먼저 마주치는 곳도 Ingress입니다. 502, 504, 404, TLS 경고 — 이런 에러는 원인이 컨트롤러 설정, 서비스 셀렉터, 백엔드 헬스, 인증서, DNS 등 여러 계층에 흩어져 있어, 추측으로 접근하면 시간만 잡아먹습니다.

이 글은 추측을 없애기 위한 플레이북입니다. 증상에서 출발해 진단 트리를 따라 내려가면, 몇 개의 명령만으로 원인 계층을 좁힐 수 있게 구성했습니다. ingress-nginx를 기준 구현체로 삼지만, 진단 절차 자체는 Traefik, HAProxy, Contour 등 다른 컨트롤러에도 그대로 적용됩니다.

2026년 현재 Ingress API는 frozen 상태이고 Gateway API가 후계 표준이지만, 운영 중인 클러스터 대부분은 여전히 Ingress 위에서 돌아갑니다. 따라서 이 플레이북은 당분간 현역으로 유용합니다. 마지막에 Gateway API에서 동일 증상을 어떻게 진단하는지도 간단히 짚습니다.

일반 디버깅 절차

증상별 트리로 들어가기 전에, 거의 모든 Ingress 문제에 공통으로 적용되는 점검 순서가 있습니다. 트래픽 경로를 따라 바깥에서 안쪽으로 좁혀가는 것이 핵심입니다.

[클라이언트] → [DNS] → [LoadBalancer/노드] → [Ingress Controller] → [Service] → [Endpoints/Pod]
     1            2              3                      4                5              6
  1. DNS가 올바른 LB IP로 해석되는가 (dig/nslookup)
  2. LB/노드 포트까지 TCP 도달이 되는가 (curl -v, telnet)
  3. Ingress 리소스가 컨트롤러에 인식됐는가 (kubectl describe ingress)
  4. 컨트롤러가 의도한 백엔드로 라우팅하는가 (컨트롤러 로그)
  5. Service가 올바른 Pod를 선택하는가 (kubectl get endpoints)
  6. Pod가 실제로 응답하는가 (kubectl exec로 직접 curl)

핵심 명령 모음.

# Ingress 상태와 이벤트
kubectl describe ingress my-ingress -n my-namespace

# 컨트롤러 로그 실시간
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller -f

# 서비스가 가리키는 엔드포인트 확인 (가장 중요한 명령 중 하나)
kubectl get endpoints my-service -n my-namespace

# 컨트롤러 Pod 안에서 백엔드로 직접 요청
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
  curl -s -o /dev/null -w "%{http_code}\n" http://my-service.my-namespace.svc:80/

# 외부에서 Host 헤더를 지정해 직접 테스트
curl -v -H "Host: app.example.com" http://<LB_IP>/

kubectl get endpoints가 비어 있으면(주소 없음 표시), 거의 모든 502/503의 근본 원인입니다. 여기서부터 시작하면 시간을 크게 아낍니다.

증상 1: 404 Not Found

404는 "컨트롤러는 살아 있는데 이 요청을 어디로 보낼지 모른다"는 신호입니다.

404 수신
1) ingressClassName이 설정/일치하는가?
   ├─ 없음/오타  → 컨트롤러가 이 Ingress를 무시함. spec.ingressClassName 추가
   └─ 일치       → 다음
2) host와 path가 요청과 매칭되는가?
   ├─ Host 헤더 불일치  → 와일드카드/정확 호스트 확인
   ├─ pathType 문제     → Prefix vs Exact 확인
   └─ 매칭됨            → 다음
3) describe ingress의 backend가 올바른 서비스/포트인가?
   ├─ 서비스명/포트 오타 → 수정
   └─ 정상              → 백엔드 앱 자체의 404 (라우팅은 정상)

확인 명령.

# 이 Ingress가 어떤 클래스에 속하는지, 규칙이 어떻게 보이는지
kubectl get ingress my-ingress -o yaml | grep -A2 ingressClassName
kubectl describe ingress my-ingress

# 설치된 IngressClass 목록
kubectl get ingressclass

# Host 헤더를 정확히 지정해 매칭 확인
curl -v -H "Host: app.example.com" http://<LB_IP>/api/users

가장 흔한 원인은 ingressClassName 누락입니다. 컨트롤러는 자신의 클래스에 해당하는 Ingress만 처리하므로, 클래스가 없거나 다른 클래스면 그 Ingress를 통째로 무시하고 기본 404를 반환합니다. pathType도 자주 발목을 잡습니다. Prefix는 경로 세그먼트 단위 접두사 매칭이고 Exact는 완전 일치라, 의도와 다르게 매칭/미스가 납니다.

증상 2: 502 Bad Gateway

502는 "컨트롤러가 백엔드에 연결은 시도했으나 정상 응답을 받지 못했다"는 신호입니다.

502 수신
1) endpoints가 존재하는가?
   ├─ 비어 있음  → Pod 미준비/셀렉터 불일치 (사실상 503에 가까움)
   └─ 존재       → 다음
2) 백엔드 포트/프로토콜이 맞는가?
   ├─ targetPort 불일치       → Service.targetPort 점검
   ├─ HTTPS 백엔드인데 HTTP로 → backend-protocol 어노테이션 HTTPS
   └─ 정상                    → 다음
3) 백엔드가 정상 응답하는가? (컨트롤러에서 직접 curl)
   ├─ 연결 거부/리셋  → 앱 크래시/리스닝 포트 다름
   ├─ 타임아웃        → 504 트리로 이동
   └─ 200            → 헤더/바디 크기, upstream keepalive 의심

확인 명령.

# 백엔드 직접 호출 (포트/프로토콜 검증)
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
  curl -sv http://my-service.my-namespace.svc:8080/healthz

# 컨트롤러 로그에서 502 라인과 upstream 정보
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | grep " 502 "

전형적인 502 원인은 포트/프로토콜 불일치입니다. Service의 targetPort가 컨테이너가 실제 리스닝하는 포트와 다르거나, 백엔드가 HTTPS인데 컨트롤러가 HTTP로 붙는 경우입니다. 후자는 backend-protocol 어노테이션으로 해결합니다.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"

응답 헤더가 지나치게 크거나(예: 거대한 Set-Cookie) upstream keepalive 설정이 안 맞아도 502가 납니다. 이 경우 proxy-buffer-size 조정이 필요할 수 있습니다.

증상 3: 503 Service Unavailable (no endpoints)

503의 대표 원인은 "보낼 백엔드가 없음"입니다.

503 수신
kubectl get endpoints my-service
   ├─ <none>  → 1) Pod가 Ready인가? 2) Service selector가 Pod label과 일치하는가?
   │           3) readinessProbe가 실패해 트래픽에서 빠졌는가?
   └─ 주소 있음 → 컨트롤러-서비스 간 일시적 동기화 지연 또는 maxUnavailable 중

확인 명령.

# 엔드포인트(가장 중요)
kubectl get endpoints my-service -n my-namespace

# Pod 상태와 readiness
kubectl get pods -n my-namespace -l app=my-app
kubectl describe pod <pod> -n my-namespace | grep -A5 Conditions

# 서비스 셀렉터와 Pod 라벨 비교
kubectl get service my-service -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels -n my-namespace

endpoints가 비어 있으면(주소 없음) 원인은 셋 중 하나입니다. (1) Pod가 아직 Ready가 아님, (2) Service selector와 Pod label 불일치, (3) readinessProbe 실패로 엔드포인트에서 제외됨. 배포 직후라면 일시적이지만, 지속되면 셀렉터 오타나 probe 설정을 의심합니다.

증상 4: 504 Gateway Timeout

504는 "백엔드가 제한 시간 안에 응답하지 않았다"는 신호입니다.

504 수신
1) 백엔드 자체가 느린가? (직접 curl로 시간 측정)
   ├─ 느림  → 앱 성능 문제. DB/외부 호출 의심
   └─ 빠름  → 다음
2) 타임아웃 어노테이션이 너무 짧은가?
   └─ proxy-read-timeout/proxy-send-timeout 상향 검토
3) keepalive/커넥션 풀 고갈인가?
   └─ upstream-keepalive 설정, 동시성 점검

확인 명령.

# 백엔드 응답 시간 측정
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
  curl -s -o /dev/null -w "time_total=%{time_total}\n" http://my-service.my-namespace.svc:80/slow

# 현재 타임아웃 어노테이션 확인
kubectl get ingress my-ingress -o yaml | grep timeout

타임아웃을 무작정 늘리는 것은 증상만 가리는 것입니다. 먼저 백엔드가 왜 느린지(DB 쿼리, 외부 API, GC)를 봐야 합니다. 진짜로 오래 걸리는 작업(리포트 생성 등)이라면 타임아웃을 명시적으로 상향합니다.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "120"

증상 5: TLS 오류 (인증서/SNI)

TLS 문제는 브라우저 경고, handshake 실패, 잘못된 인증서 제공 등으로 나타납니다.

TLS 오류
1) 어떤 인증서가 제공되는가? (openssl s_client)
   ├─ 기본/가짜 인증서  → Ingress의 tls.secretName 또는 SNI 매칭 문제
   ├─ 만료된 인증서      → cert-manager 갱신 실패 확인
   └─ 올바른 인증서      → 클라이언트 신뢰 체인 문제
2) Secret이 존재하고 형식이 맞는가?
   └─ kubernetes.io/tls 타입, tls.crt/tls.key 키 확인
3) host와 인증서 SAN이 일치하는가?
   └─ SNI/와일드카드 범위 점검

확인 명령.

# 실제로 제공되는 인증서를 SNI 지정해 확인
openssl s_client -connect <LB_IP>:443 -servername app.example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

# TLS Secret 존재/형식
kubectl get secret my-tls -n my-namespace -o yaml | grep -E "tls.crt|tls.key|type"

# cert-manager Certificate 상태
kubectl describe certificate my-cert -n my-namespace

가장 흔한 함정은 SNI 기반 인증서 선택입니다. 여러 호스트가 한 컨트롤러를 공유할 때, 클라이언트가 SNI를 보내지 않거나 host와 매칭되는 tls 항목이 없으면 컨트롤러가 기본(fake) 인증서를 제공합니다. cert-manager를 쓴다면 Certificate 리소스의 Ready 조건과 갱신 이벤트를 먼저 봅니다. 만료는 거의 항상 갱신 실패의 결과입니다.

증상 6: 리다이렉트 루프

브라우저가 "너무 많은 리다이렉트"를 표시하면 무한 루프입니다.

리다이렉트 루프
1) Ingress가 HTTPS 리다이렉트를 거는데 백엔드도 HTTP로 다시 리다이렉트?
   └─ 이중 리다이렉트. 한 곳에서만 처리
2) TLS 종료가 LB에서 일어나는데 백엔드가 X-Forwarded-Proto를 무시?
   └─ use-forwarded-headers 활성화, 앱이 헤더 신뢰하도록
3) force-ssl-redirect와 앱의 자체 리다이렉트가 충돌?
   └─ 한쪽 비활성화

전형적인 시나리오는 TLS가 외부 LB에서 종료되고, 컨트롤러/백엔드는 평문 HTTP를 받는데, 앱이 "HTTP니까 HTTPS로 리다이렉트"를 반복하는 것입니다. 해결은 X-Forwarded-Proto 헤더를 신뢰하도록 설정하는 것입니다.

# ConfigMap (컨트롤러 전역)
data:
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"

증상 7: 413 Request Entity Too Large (대용량 업로드)

파일 업로드가 일정 크기에서 실패하면 413입니다. 컨트롤러의 바디 크기 제한이 원인입니다.

확인과 해결.

# 컨트롤러 로그에서 413 / "client intended to send too large body"
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | grep "too large"

해당 Ingress에 어노테이션으로 한계를 올립니다.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"

전역으로 바꾸려면 ConfigMap의 proxy-body-size를 조정합니다. 단, 무제한(0)은 메모리/디스크 버퍼링 측면에서 위험하므로, 실제 업로드 상한에 맞춘 합리적 값을 권장합니다. 또한 대용량 업로드는 proxy-request-buffering 설정과 함께 검토해야 메모리 폭증을 막을 수 있습니다.

흔한 설정 실수 카탈로그

진단 트리와 별개로, 반복해서 사람을 무는 설정 실수를 모았습니다.

실수증상해결
ingressClassName 누락404, 컨트롤러가 무시spec.ingressClassName 명시
Service targetPort 오타502컨테이너 리스닝 포트와 일치
selector와 Pod label 불일치503 no endpointsselector 수정
backend-protocol 미설정 (HTTPS 백엔드)502backend-protocol HTTPS 어노테이션
pathType 혼동 (Prefix vs Exact)404 또는 과매칭의도에 맞게 지정
rewrite-target과 path 캡처 그룹 불일치잘못된 경로 전달정규식 캡처와 rewrite 정렬
use-forwarded-headers 미설정리다이렉트 루프, 잘못된 클라이언트 IPConfigMap에서 활성화
proxy-body-size 기본값413 업로드 실패어노테이션으로 상향
TLS Secret 잘못된 타입인증서 미적용kubernetes.io/tls 타입 확인

특히 rewrite-target은 캡처 그룹 번호가 어긋나면 조용히 잘못된 경로를 백엔드로 보냅니다. 이런 종류는 404나 500이 아니라 "엉뚱한 페이지가 뜨는" 형태라 발견이 늦습니다.

사전 점검 체크리스트

배포 전, 혹은 트러블슈팅의 마무리로 훑을 체크리스트입니다.

  • spec.ingressClassName이 명시되어 있고 설치된 클래스와 일치하는가
  • host와 pathType가 의도와 맞는가
  • Service의 targetPort가 컨테이너 포트와 일치하는가
  • kubectl get endpoints에 주소가 채워져 있는가
  • TLS Secret이 kubernetes.io/tls 타입이고 host와 SAN이 일치하는가
  • cert-manager Certificate가 Ready인가
  • proxy-body-size가 실제 업로드 상한을 커버하는가
  • 타임아웃 어노테이션이 백엔드 최악 응답 시간을 커버하는가
  • use-forwarded-headers가 LB 토폴로지에 맞게 설정됐는가
  • 컨트롤러 로그에 reload 실패나 config 경고가 없는가

Gateway API에서의 동일 증상

2026년 현재 Gateway API가 후계 표준입니다. 증상은 비슷하지만 진단 대상이 바뀝니다. 404는 HTTPRoute의 매칭 규칙과 Gateway의 listener/hostname, 그리고 ReferenceGrant(네임스페이스 간 참조 허용)를 봅니다. 503은 backendRef가 가리키는 서비스의 엔드포인트를, TLS는 Gateway listener의 certificateRefs를 점검합니다.

좋은 점은 Gateway API가 status 조건을 훨씬 풍부하게 노출한다는 것입니다. HTTPRoute와 Gateway 모두 kubectl describe로 Accepted/Programmed/ResolvedRefs 같은 조건과 그 이유(reason)를 보여주므로, "왜 라우팅이 안 되는가"가 어노테이션 추측이 아니라 명시적 status로 드러납니다. 이 플레이북의 증상→계층 좁히기 사고방식은 그대로 가져가되, 확인 대상만 Ingress 리소스에서 Gateway/HTTPRoute의 status 조건으로 옮기면 됩니다.

마치며

Ingress 트러블슈팅의 핵심은 추측을 끊는 것입니다. 증상에서 출발해 트래픽 경로를 바깥에서 안으로 좁히고, 각 계층에서 한두 개의 명령으로 사실을 확인하면, 대부분의 문제는 몇 분 안에 원인 계층이 드러납니다. 특히 kubectl get endpoints와 컨트롤러에서의 직접 curl, 두 가지만 습관화해도 502/503의 절반 이상이 즉시 해결됩니다.

이 플레이북을 팀 위키나 런북에 붙여두고, 새 증상을 만날 때마다 트리와 카탈로그를 갱신하세요. 시간이 지나면 이 문서가 팀의 집단 기억이 되어, 같은 함정에 두 번 빠지지 않게 해줍니다.

참고 자료