Published on

EKS에서 Pod는 Running인데 503가 뜰 때 점검

Authors

서버가 503을 뿜는데 kubectl get pods는 전부 Running이라면, 운영자는 본능적으로 “애플리케이션은 살아있는데 라우팅이 죽었나?”를 의심하게 됩니다. 실제로 Kubernetes/EKS에서 503은 대개 로드밸런서/Ingress/Service가 보낼 ‘정상 백엔드’를 찾지 못할 때 발생합니다. 즉, 핵심은 Pod Running이 아니라 Service 관점에서 Endpoint가 “Ready”로 잡히는가입니다.

이 글은 EKS에서 흔한 503 케이스를 EndpointSlice → Readiness → Service selector → Ingress/ALB TargetGroup → 네트워크/보안그룹 순서로 빠르게 좁혀가는 방식으로 정리합니다.

503의 의미를 먼저 분리하기

503은 누가 반환했는지에 따라 디버깅 방향이 달라집니다.

  • ALB/NLB가 반환하는 503: 타겟 그룹에 healthy target이 없음, 혹은 연결 자체 실패
  • Ingress Controller(NGINX 등)가 반환하는 503: upstream endpoint가 비었거나 연결 실패
  • 애플리케이션이 반환하는 503: 앱 내부 과부하/의존성 장애

우선 클라이언트가 보는 503이 어디서 오는지부터 확인합니다.

빠른 확인: 응답 헤더/바디로 판별

  • ALB 503은 종종 Server: awselb/2.0 같은 흔적이 있고, 바디가 고정 HTML인 경우가 많습니다.
  • NGINX Ingress라면 Server: nginx 또는 default backend - 404/503 Service Temporarily Unavailable 패턴이 보입니다.
curl -sv https://api.example.com/health 2>&1 | sed -n '1,20p'

1) Service에 실제로 Endpoint가 잡히는지: EndpointSlice부터 본다

Pod가 Running이어도 Endpoint가 0개면 503은 정상입니다. Kubernetes 1.19+에서는 Endpoints 대신 EndpointSlice가 기본입니다.

Service와 EndpointSlice를 함께 확인

NS=prod
SVC=my-service

kubectl -n $NS get svc $SVC -o wide
kubectl -n $NS get endpointslice -l kubernetes.io/service-name=$SVC -o wide

여기서 봐야 할 것:

  • EndpointSlice가 존재하는가
  • ENDPOINTS0이 아닌가
  • endpoint의 conditions.ready=true 인가

좀 더 정확히는 아래처럼 JSONPath로 Ready 조건을 뽑아봅니다.

kubectl -n $NS get endpointslice \
  -l kubernetes.io/service-name=$SVC \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\t"}{.conditions.ready}{"\n"}{end}'
  • true가 안 보이고 false/<nil>만 보이면, Pod는 떠 있지만 Service가 트래픽을 안 보내는 상태입니다.

EndpointSlice가 비어있을 때 원인 Top 3

  1. Service selector가 Pod label과 불일치
  2. Pod가 Ready가 아님(ReadinessProbe 실패)
  3. Service가 headless/ExternalName 등 기대와 다른 타입

2) Service selector와 Pod label 불일치: 가장 흔한 실수

디플로이/롤백/헬름 값 변경 때 라벨이 미묘하게 바뀌면, Service는 “대상 Pod가 없다”고 판단합니다.

kubectl -n $NS get svc $SVC -o jsonpath='{.spec.selector}'
echo
kubectl -n $NS get pods -l app=my-app -o wide

Service selector가 예를 들어 app=my-app, tier=api인데 Pod에는 tier=backend로 찍혀 있으면 Endpoint는 0이 됩니다.

실전 팁: Service가 어떤 Pod를 고르는지 즉시 확인

kubectl -n $NS get pods \
  -l "$(kubectl -n $NS get svc $SVC -o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' | sed 's/,$//')" \
  -o wide

3) Pod는 Running인데 Ready가 아니다: ReadinessProbe와 readinessGates

Running은 컨테이너 프로세스가 떠 있다는 뜻이지, 트래픽을 받을 준비가 됐다는 뜻이 아닙니다. Service는 기본적으로 Ready=True인 Pod만 endpoint로 넣습니다.

Pod Ready 상태 확인

POD=$(kubectl -n $NS get pod -l app=my-app -o jsonpath='{.items[0].metadata.name}')

kubectl -n $NS get pod $POD
kubectl -n $NS describe pod $POD | sed -n '/Conditions:/,/Events:/p'
  • Ready: FalseReadiness probe failed 이벤트가 있는지 확인합니다.

ReadinessProbe가 503을 유발하는 전형적 패턴

  • 앱이 /health는 200인데 /ready는 DB 연결 전까지 500
  • readiness가 너무 공격적(초기 기동에 비해 initialDelaySeconds가 짧음)
  • 타임아웃이 짧아 간헐 실패 → endpoint에서 계속 빠졌다 들어왔다(flapping)

예시 설정:

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3

기동이 느린 서비스라면 initialDelaySeconds를 늘리거나, /ready가 의존성(예: DB)까지 강제하지 않도록 정책을 정해야 합니다.

또 하나: EKS + AWS Load Balancer Controller 환경에서는 Pod readiness gate가 붙어 target-health가 Ready 조건에 영향을 주기도 합니다. 이 경우 Pod는 프로세스/프로브는 정상인데도 Ready=False로 남을 수 있습니다.

4) Ingress/ALB/NLB 레벨: TargetGroup에 healthy가 0인지 확인

ALB를 쓰는 경우(aws-load-balancer-controller), 503은 대부분 “타겟 그룹 healthy 0”입니다.

kubectl에서 Ingress 이벤트 확인

kubectl -n $NS describe ingress my-ingress
kubectl -n $NS get events --sort-by=.lastTimestamp | tail -n 30

AWS 콘솔/CLI에서 TargetGroup 상태 확인(핵심)

  • Health check path/port가 Service/Pod와 맞는지
  • Target type이 ip인지 instance인지(환경에 따라 다름)
  • Security Group/NACL로 health check 트래픽이 막히지 않는지

CLI 예시:

aws elbv2 describe-target-health --target-group-arn <TG_ARN>

여기서 unhealthyReason이 힌트입니다.

  • Health checks failed → 경로/포트/응답코드 문제
  • Target.Timeout → 네트워크/보안그룹/애플리케이션 응답 지연
  • Target.NotInUse → 등록 자체가 안 됨(Selector/Endpoint 문제로 거슬러 올라가야 함)

5) Service port/targetPort 불일치: 컨테이너는 8080인데 Service는 80?

Pod가 8080에서 떠 있는데 Service가 targetPort: 80으로 잡혀 있으면, endpoint는 잡혀도 연결은 실패합니다(503/502).

kubectl -n $NS get svc $SVC -o yaml | yq '.spec.ports'
kubectl -n $NS get deploy my-app -o yaml | yq '.spec.template.spec.containers[].ports'

가장 안전한 방법은 targetPort이름으로 맞추는 겁니다.

# deployment
ports:
  - name: http
    containerPort: 8080

# service
ports:
  - name: http
    port: 80
    targetPort: http

6) 네트워크 경로 점검: Pod까지 실제로 붙는지

Endpoint가 있고 Ready도 true인데 503이면, 이제는 연결 경로를 봐야 합니다.

클러스터 내부에서 Service로 curl

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

# inside pod
curl -sv http://my-service.prod.svc.cluster.local:80/health
  • 내부에서 OK면: Ingress/ALB/보안그룹 쪽 문제일 확률이 큼
  • 내부에서도 실패면: Service port, kube-proxy, NetworkPolicy, Pod listening 문제

Pod IP로 직접 접근(서비스 우회)

POD_IP=$(kubectl -n $NS get pod -l app=my-app -o jsonpath='{.items[0].status.podIP}')
curl -sv http://$POD_IP:8080/health

여기서도 실패하면 애플리케이션/컨테이너 레벨 이슈일 가능성이 큽니다.

7) 흔하지만 놓치기 쉬운 케이스들

(1) readiness는 OK인데 실제 요청만 느려서 503

헬스체크는 가볍고 실제 API는 무거우면, ALB idle timeout/NGINX proxy timeout에서 잘릴 수 있습니다. 이때는 504/502가 더 흔하지만, 구성에 따라 503으로 보이기도 합니다. 장기 스트리밍/SSE라면 프록시 버퍼링/타임아웃 튜닝이 중요합니다.

관련해서는 네트워크 타임아웃·프록시 버퍼링 관점의 체크리스트도 함께 보면 좋습니다: LLM SSE 스트리밍 499 502 급증과 응답 끊김을 잡는 프록시 튜닝 체크리스트

(2) CoreDNS/서비스 디스커버리 문제로 upstream을 못 찾음

내부 호출이 my-service.prod.svc를 못 찾거나 지연되면 “백엔드 없음”처럼 보일 수 있습니다. 특히 EKS에서 CoreDNS가 불안정하면 광범위하게 503/타임아웃이 연쇄 발생합니다.

DNS 의심 시에는 이 글의 증상/해결 흐름이 그대로 도움이 됩니다: AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결

(3) Pod는 Running이지만 실제로는 불안정(CrashLoop 직전/자원 부족)

Running은 “지금 이 순간”일 뿐이고, 직전까지 재시작을 반복했거나 OOM 직전이면 readiness가 흔들립니다. 이벤트/로그/프로브를 원인별로 정리한 글도 참고할 만합니다: Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅

8) 10분 안에 끝내는 실전 체크리스트(요약)

아래 순서로 보면 대부분 503은 빠르게 잡힙니다.

  1. 503을 누가 반환? (ALB vs Ingress vs App)
  2. EndpointSlice에 endpoint가 있는가? Ready인가?
    • kubectl get endpointslice -l kubernetes.io/service-name=...
  3. Service selector ↔ Pod label 일치?
  4. Pod Ready=False면 ReadinessProbe 이벤트 확인
  5. Service port/targetPort 일치? 이름 기반으로 고정
  6. 클러스터 내부에서 Service로 curl → Pod IP로 curl
  7. ALB TargetGroup healthy 0인지 확인 + health check path/port/SG 점검
  8. (필요 시) DNS/CoreDNS, NetworkPolicy, 노드/자원 이벤트

부록: 자주 쓰는 원샷 명령 모음

# 1) 서비스와 엔드포인트(EndpointSlice)
NS=prod; SVC=my-service
kubectl -n $NS get svc $SVC -o wide
kubectl -n $NS get endpointslice -l kubernetes.io/service-name=$SVC -o wide
kubectl -n $NS get endpointslice -l kubernetes.io/service-name=$SVC \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\t"}{.conditions.ready}{"\n"}{end}'

# 2) 서비스 셀렉터 확인
kubectl -n $NS get svc $SVC -o jsonpath='{.spec.selector}'; echo

# 3) Pod Ready/이벤트
kubectl -n $NS get pod -l app=my-app
kubectl -n $NS describe pod -l app=my-app | sed -n '/Conditions:/,/Events:/p'

# 4) 내부에서 서비스 호출
kubectl -n $NS run -it --rm netshoot --image=nicolaka/netshoot --restart=Never -- bash

Pod가 Running인데 503이 뜨는 상황의 본질은 “Pod의 생존”이 아니라 Service/Ingress가 선택한 backend가 ‘Ready endpoint’로 존재하느냐입니다. EndpointSlice에서 Ready가 보이는지부터 시작하면, 불필요하게 애플리케이션 로그만 파다가 시간을 날리는 일을 크게 줄일 수 있습니다.