Published on

EKS에서 Readiness 실패인데 로그는 정상일 때

Authors

서버 로그는 정상이고 애플리케이션도 "기동 완료"를 찍는데, Pod가 Readiness probe failed로 계속 NotReady 상태라면 운영자 입장에선 가장 답답한 케이스입니다. 특히 EKS에서는 Pod 내부(컨테이너) 관점의 정상kubelet이 수행하는 readiness 체크 관점의 정상이 쉽게 엇갈립니다.

이 글에서는 "로그는 정상인데 readiness만 실패"할 때 실제로 많이 발생하는 원인들을 재현 가능한 진단 방법과 함께 정리합니다. 결론부터 말하면, 대부분은 다음 중 하나입니다.

  • 프로브가 때리는 포트/경로/Host 헤더가 실제 서비스와 다름
  • 앱이 0.0.0.0이 아닌 127.0.0.1에만 바인딩(또는 IPv6/dual-stack 이슈)
  • readiness가 의존하는 외부 의존성(DB/STS/Redis) 이 간헐적으로 느림/차단됨
  • NetworkPolicy/보안그룹/iptables로 kubelet→Pod 트래픽이 막힘
  • ALB/NLB 헬스체크와 readiness가 서로 다른 기준으로 동작해 “겉보기 정상”이 깨짐

관련해서 Ingress/ALB 레벨에서 5xx가 섞여 보인다면 아래 글도 함께 보면 원인 분리가 빨라집니다.


1) 먼저: "Readiness 실패"의 증거를 이벤트로 확정하기

로그가 정상이라는 건 애플리케이션 프로세스가 살아있다는 의미일 뿐, readiness probe가 무엇을, 어떤 응답으로 실패하는지와는 별개입니다. 우선 이벤트에서 실패 메시지를 정확히 봅니다.

# Pod 상태/프로브 설정 요약
kubectl -n <ns> describe pod <pod>

# 이벤트만 빠르게
kubectl -n <ns> get events --sort-by=.metadata.creationTimestamp | tail -n 50

describe pod에서 아래를 체크하세요.

  • Readiness: 섹션의 프로브 타입(http-get, tcp-socket, exec)
  • 실패 메시지 예: context deadline exceeded, connection refused, HTTP probe failed with statuscode: 404
  • Container Ports와 실제 앱 리스닝 포트 일치 여부

이 한 줄만으로도 원인의 절반이 갈립니다.

  • connection refused → 포트/바인딩/네트워크
  • timeout (context deadline exceeded) → 느림, 방화벽, DNS, 외부 의존성
  • HTTP 404/503 → 경로/Host/라우팅/앱 readiness 로직

2) 흔한 원인 1: 프로브 포트/경로가 실제와 다름

(1) 숫자 포트 불일치

컨테이너는 8080을 듣는데 readiness는 80을 때리는 식의 실수는 여전히 최상위 빈도입니다.

readinessProbe:
  httpGet:
    path: /health
    port: 80   # <- 실제는 8080

해결은 단순히 포트를 맞추는 것인데, Service 포트Container 포트를 혼동하는 경우가 많습니다. readiness는 기본적으로 컨테이너 네임스페이스 내부에서 Pod IP로 직접 접근하므로, Service 포트가 아니라 컨테이너가 실제로 리슨하는 포트를 써야 합니다.

(2) 경로 불일치(특히 프레임워크 기본 경로)

  • Spring Boot Actuator: /actuator/health (보안 설정에 따라 401/403 가능)
  • Next.js/Node: /healthz를 따로 안 만들면 404
  • gRPC 서버인데 HTTP GET 프로브를 사용

프로브 경로는 항상 앱에서 200을 보장하는 엔드포인트로 분리하는 게 좋습니다.


3) 흔한 원인 2: 앱이 127.0.0.1에만 바인딩됨

로그에선 "Server started"가 찍히지만, 실제로는 127.0.0.1:8080에만 떠 있는 케이스가 많습니다. 이 경우 Pod IP로 접근하는 readiness는 무조건 실패합니다.

컨테이너 내부에서 리슨 주소 확인

kubectl -n <ns> exec -it <pod> -- sh -lc "ss -lntp || netstat -lntp"

출력 예시:

  • 정상: LISTEN 0 4096 0.0.0.0:8080 ...
  • 문제: LISTEN 0 4096 127.0.0.1:8080 ...

해결은 프레임워크/서버 설정에서 바인딩을 0.0.0.0로 바꾸는 것입니다.

  • Node/Express: app.listen(port, '0.0.0.0')
  • Spring Boot: server.address=0.0.0.0
  • uvicorn: --host 0.0.0.0

4) 흔한 원인 3: readiness가 외부 의존성에 묶여 있음(그리고 그게 느림)

readiness는 "트래픽을 받을 준비"를 의미하지만, 이를 DB 연결/외부 API 호출 성공 같은 조건으로 강하게 묶으면 EKS 환경에서 실패가 잦아집니다.

  • STS, RDS, Redis, Kafka, 외부 SaaS 호출
  • DNS 지연
  • NAT 게이트웨이/라우팅 문제

특히 IRSA/ST S 호출이 readiness 경로에서 발생하면, 권한/스로틀링/시간 오차로 readiness가 깨질 수 있습니다. IRSA 계열 문제는 아래 체크리스트도 도움이 됩니다.

권장 패턴: readiness는 “빠르고 로컬한 조건”으로

  • 프로세스/스레드풀/큐/필수 포트 바인딩 확인
  • DB 등 외부 의존성은 별도 지표로 관측하되 readiness에 강결합하지 않기

예시: Node.js에서 로컬 상태 기반 readiness

import express from 'express';

const app = express();
let ready = false;

// 예: 내부 초기화(캐시 로딩 등)만 완료되면 ready
async function init() {
  // ... 로컬 초기화
  ready = true;
}

app.get('/healthz', (req, res) => res.status(200).send('ok'));
app.get('/readyz', (req, res) => {
  if (!ready) return res.status(503).send('not ready');
  res.status(200).send('ready');
});

app.listen(8080, '0.0.0.0', () => {
  console.log('listening');
  init();
});

Kubernetes에서는 /readyz만 readiness로 사용하고, /healthz는 liveness로 쓰는 식으로 분리하는 것이 안전합니다.


5) 흔한 원인 4: 프로브 타임아웃/임계값이 너무 빡빡함

기본값은 생각보다 공격적입니다.

  • timeoutSeconds 기본 1초
  • periodSeconds 기본 10초
  • failureThreshold 기본 3

앱이 순간적으로 GC/스레드 정체/콜드 스타트가 있으면 1초 타임아웃은 쉽게 터집니다.

개선 예시

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 6
  successThreshold: 1

핵심은 무작정 크게 잡는 게 아니라, 실제 p95/p99 응답시간을 기준으로 타임아웃을 설정하는 것입니다.

만약 GC/메모리 압박이 의심되면 OOMKilled/메모리 튜닝 관점도 같이 봐야 합니다.


6) kubelet이 때리는 readiness를 “Pod 내부에서” 재현하기

readiness가 HTTP GET이라면, 같은 Pod에서 직접 호출해보면 원인 분리가 빨라집니다.

# 같은 컨테이너에서 로컬 호출
kubectl -n <ns> exec -it <pod> -- sh -lc "curl -svm 2 http://127.0.0.1:8080/readyz"

# Pod IP로 호출(바인딩 문제/iptables 문제 확인)
POD_IP=$(kubectl -n <ns> get pod <pod> -o jsonpath='{.status.podIP}')
kubectl -n <ns> exec -it <pod> -- sh -lc "curl -svm 2 http://$POD_IP:8080/readyz"
  • 127.0.0.1은 되는데 POD_IP는 실패 → 바인딩이 localhost이거나, 네트워크 레벨 차단/라우팅 문제
  • 둘 다 느리거나 타임아웃 → 앱 자체가 느리거나 readiness 로직이 무거움
  • POD_IP는 되는데 kubelet만 실패 → 노드→Pod 경로(iptables/CNI/보안정책) 의심

7) EKS 특유의 네트워크 이슈: CNI/보안그룹/NetworkPolicy

EKS에서 readiness 실패가 "간헐적"이라면, 앱보다는 네트워크 계층이 원인인 경우가 많습니다.

(1) Pod IP/CNI 상태 확인

Pod가 정상 IP를 받았는지, 노드 ENI/IP가 고갈 직전인지 확인합니다.

kubectl -n kube-system get ds aws-node
kubectl -n kube-system logs -l k8s-app=aws-node --tail=200

kubectl -n <ns> get pod -o wide

Pod가 아예 IP를 못 받는 Pending 케이스는 아래 글이 직접적이지만, IP가 간신히 할당되는 경계 상황에서도 네트워크가 불안정해질 수 있습니다.

(2) NetworkPolicy로 kubelet→Pod 트래픽 차단

일부 CNI(예: Calico) 환경에서 NetworkPolicy를 적용하면, readiness/liveness 트래픽이 예상치 못하게 막힐 수 있습니다. 프로브는 “클러스터 내부 트래픽”이므로, 인바운드 허용 규칙이 필요합니다.

점검:

kubectl -n <ns> get networkpolicy
kubectl -n <ns> describe networkpolicy <name>

(3) Security Group / NACL

보통 kubelet→Pod는 노드 내부/ENI 경로로 통신하므로 SG 이슈는 덜하지만, 커스텀 네트워크 구성(예: SG for Pods, 서브넷 분리, NACL 강화)에서는 프로브 트래픽이 영향을 받을 수 있습니다.


8) ALB/NLB 헬스체크와 readiness의 “이중 기준” 정리

현상:

  • Pod readiness는 실패(NotReady)인데
  • ALB Target Group health check는 healthy처럼 보이거나(혹은 반대)
  • 사용자 트래픽은 간헐적으로 5xx/504

이는 체크 주체와 경로가 다르기 때문입니다.

  • readiness: kubelet이 Pod IP:port로 체크(컨테이너 기준)
  • ALB: NodePort/PodIP(TargetType에 따라)로 체크 + Host/path 다름

해결 전략:

  1. readiness와 ALB health check를 가능한 한 동일한 엔드포인트로 통일
  2. 앱에서 /readyz는 내부 준비 상태만, /healthz는 프로세스 생존만
  3. Ingress/Service/TargetGroup의 포트/경로/성공 코드 범위를 문서화

ALB에서 504/5xx가 섞이면 readiness만 볼 게 아니라, 타겟 등록/드레인/헬스체크 타이밍까지 함께 봐야 합니다.


9) 실전 체크리스트(10분 컷)

아래 순서대로 하면 대부분 10~20분 내 원인 범위를 좁힐 수 있습니다.

  1. kubectl describe pod에서 readiness 실패 메시지(거절/타임아웃/HTTP 코드) 확인
  2. readiness 포트/경로가 컨테이너 실제 리슨과 일치하는지 확인
  3. ss -lntp0.0.0.0 바인딩 여부 확인
  4. 컨테이너 내부에서 curl 127.0.0.1curl $POD_IP 순으로 재현
  5. 타임아웃이 1초면 현실적으로 늘려보고, readiness 로직이 무거운지 점검
  6. readiness가 외부 의존성에 묶였는지 확인(특히 STS/DB/DNS)
  7. NetworkPolicy/CNI 로그/aws-node 상태/IP 여유 확인
  8. ALB/NLB 헬스체크 경로/포트/성공코드와 readiness 기준을 비교

10) 마무리: “로그 정상”에 속지 않기

EKS에서 readiness 실패는 애플리케이션 로그만으로는 잘 드러나지 않습니다. readiness는 네트워크/바인딩/프로브 정의/의존성/정책이 모두 맞아야 통과합니다.

가장 효과적인 접근은 (1) 이벤트에서 실패 형태를 확정하고, (2) 같은 요청을 컨테이너 내부에서 재현하고, (3) EKS 네트워크 계층(CNI/정책/ALB 헬스체크)을 순서대로 분리하는 것입니다.

원하시면 사용 중인 Deployment/Service/Ingress YAML과 describe pod의 readiness 실패 메시지(몇 줄)만 공유해도, 위 체크리스트 기준으로 어디부터 보는 게 가장 빠를지 구체적으로 짚어드릴게요.