- Published on
EKS에서 Pod는 Running인데 503가 뜰 때 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 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가 존재하는가
ENDPOINTS가 0이 아닌가- 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
- Service selector가 Pod label과 불일치
- Pod가 Ready가 아님(ReadinessProbe 실패)
- 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: False면Readiness 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>
여기서 unhealthy의 Reason이 힌트입니다.
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은 빠르게 잡힙니다.
- 503을 누가 반환? (ALB vs Ingress vs App)
- EndpointSlice에 endpoint가 있는가? Ready인가?
kubectl get endpointslice -l kubernetes.io/service-name=...
- Service selector ↔ Pod label 일치?
- Pod Ready=False면 ReadinessProbe 이벤트 확인
- Service port/targetPort 일치? 이름 기반으로 고정
- 클러스터 내부에서 Service로 curl → Pod IP로 curl
- ALB TargetGroup healthy 0인지 확인 + health check path/port/SG 점검
- (필요 시) 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가 보이는지부터 시작하면, 불필요하게 애플리케이션 로그만 파다가 시간을 날리는 일을 크게 줄일 수 있습니다.