Published on

EKS CrashLoopBackOff 원인 10분만에 찾기

Authors

서버리스나 매니지드 쿠버네티스(EKS)에서 장애 대응이 어려운 순간은 대개 비슷합니다. 배포 직후 트래픽이 안 붙고, kubectl get pods 를 보니 CrashLoopBackOff 가 떠 있습니다. 더 답답한 건 로그가 비어 있거나 컨테이너가 너무 빨리 죽어서 kubectl logs 가 따라잡지 못하는 경우죠.

이 글은 10분 안에 원인을 좁히는 것을 목표로 합니다. “원인 후보를 넓게 나열”하기보다, 가장 정보 밀도가 높은 신호부터 확인해서 빠르게 수렴하는 루틴으로 구성했습니다.

관련해서 로그가 아예 안 보이는 케이스는 별도 글에서 더 깊게 다뤘습니다. 필요하면 함께 보세요.

0분: CrashLoopBackOff의 의미부터 정리

CrashLoopBackOff 는 “쿠버네티스가 컨테이너를 재시작했는데, 계속 죽어서 재시작 간격을 점점 늘리는(backoff) 상태”입니다.

즉, 핵심은 두 가지입니다.

  1. 왜 프로세스가 종료되는가 (exit code, signal, OOMKilled 등)
  2. 왜 쿠버네티스가 재시작을 계속 시도하는가 (Deployment/ReplicaSet의 desired state)

원인은 애플리케이션 버그부터 설정/권한/리소스/프로브까지 다양하지만, 진단은 항상 이전 종료 원인을 먼저 잡으면 빨라집니다.

1분: 현재 상태를 한 줄로 요약하기

먼저 “어떤 Pod가 어떤 이유로 죽는지”를 한 번에 보겠습니다.

# 네임스페이스 포함해서 확인
kubectl get pods -n myns -o wide

# 특정 Pod의 컨테이너 상태(ExitCode, Reason, LastState 등)
kubectl describe pod -n myns mypod-xxxxx

kubectl describe pod 에서 특히 아래를 집중해서 봅니다.

  • State / Last State
  • Reason (예: Error, OOMKilled, ContainerCannotRun, CrashLoopBackOff)
  • Exit Code (예: 1, 137)
  • 하단 Events 섹션 (이미 답이 적혀있는 경우가 많습니다)

Exit Code 빠른 해석

  • Exit Code 137 인 경우가 많다면: 대개 OOMKilled 또는 SIGKILL 계열(메모리 압박) 의심
  • Exit Code 1 : 앱이 스스로 종료(환경변수 누락, 설정 파일 없음, 마이그레이션 실패 등) 가능성 큼
  • Reason: ContainerCannotRun / RunContainerError : 엔트리포인트, 권한, 이미지, 마운트 문제 가능성

2분: “이전 로그”를 먼저 본다

CrashLoop에서는 현재 컨테이너가 이미 죽어버려 로그가 끊기는 경우가 많습니다. 이럴 때는 --previous 가 가장 빠른 답입니다.

# 이전에 죽은 컨테이너 로그
kubectl logs -n myns mypod-xxxxx --previous

# 멀티 컨테이너 Pod라면 컨테이너 지정
kubectl logs -n myns mypod-xxxxx -c app --previous

여기서 바로 아래 같은 메시지가 나오면 거의 끝입니다.

  • 설정 파일 경로 오류, 환경변수 누락
  • DB/Redis 연결 실패
  • 마이그레이션 실패
  • TLS 인증서/키 파일 없음
  • 권한 부족(예: AWS API AccessDenied)

로그가 짧거나 아예 없다면 “프로세스가 뜨자마자 죽는” 케이스일 수 있으니 다음 단계로 넘어갑니다.

3분: 이벤트에서 “쿠버네티스가 본 실패”를 확인

애플리케이션 로그가 없을 때는 이벤트가 더 정직합니다.

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

# 특정 Pod에 붙은 이벤트는 describe 하단에서도 확인 가능
kubectl describe pod -n myns mypod-xxxxx

자주 나오는 이벤트 패턴은 다음과 같습니다.

  • Back-off restarting failed container : 반복 크래시
  • FailedMount : 볼륨/시크릿/컨피그맵 마운트 실패
  • Readiness probe failed / Liveness probe failed : 프로브 설정 문제 또는 앱 기동 지연
  • Failed to pull image / ImagePullBackOff : 이미지/레지스트리 인증 문제(이건 CrashLoop 이전에 걸리는 경우가 많음)

4분: 프로브(liveness/readiness/startup)가 앱을 죽이고 있지 않은가

CrashLoopBackOff의 흔한 함정은 “앱은 사실 정상인데, 프로브가 너무 공격적이라서 kubelet이 계속 죽이는” 상황입니다.

다음 조합이면 특히 위험합니다.

  • 기동이 느린데 livenessProbe 가 빠르게 시작됨
  • initialDelaySeconds 가 너무 짧음
  • timeoutSeconds 가 너무 짧음
  • failureThreshold 가 너무 작음

확인은 Deployment 스펙을 보면 됩니다.

kubectl get deploy -n myns myapp -o yaml | sed -n '1,200p'

빠른 해결 방향

  • 기동이 느리면 startupProbe 를 추가하고, startupProbe 가 성공하기 전까지 livenessProbe 를 유예
  • 최소한 기동 시간만큼 initialDelaySeconds 를 늘리기

예시(HTTP 앱 기준):

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

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

프로브 실패가 NLB/Ingress 헬스체크와 엮여 “타겟이 Unhealthy”로 보이는 경우도 많습니다. 이 연결고리는 아래 글이 도움이 됩니다.

5분: OOMKilled(메모리) 여부를 즉시 판별

describeOOMKilled 가 보이거나 Exit Code가 137 이면, 우선 “메모리가 부족했다”를 사실로 두고 다음을 확인합니다.

# 리소스 요청/제한 확인
kubectl get pod -n myns mypod-xxxxx -o jsonpath='{.spec.containers[*].resources}'

# metrics-server가 있다면 현재 사용량
kubectl top pod -n myns mypod-xxxxx
kubectl top node

자주 발생하는 원인

  • limits.memory 가 너무 낮음
  • JVM/Node/Python 등 런타임 메모리 튜닝이 limit을 초과
  • 캐시/버퍼가 기동 시 한꺼번에 잡힘
  • 큰 파일 다운로드/압축해제 등으로 순간 피크 발생

빠른 조치

  • 우선 limits.memory 를 올려서 “원인이 메모리인지”를 확정
  • JVM이면 -Xmxlimits.memory 보다 여유 있게 작게 설정
  • Node.js면 --max-old-space-size 조정

6분: ConfigMap/Secret/볼륨 마운트 실패 확인

앱 로그가 전혀 없이 바로 죽는 케이스 중 상당수가 “필수 파일이 없다”입니다. 특히 Secret 키 이름 오타, 경로 오타, subPath 실수는 빈번합니다.

# 마운트/환경변수로 주입된 시크릿/컨피그맵 확인
kubectl describe pod -n myns mypod-xxxxx

# 실제 리소스 존재 여부
kubectl get secret -n myns
kubectl get configmap -n myns

이벤트에 FailedMount 가 찍히면 거의 확정입니다. 예를 들어 아래처럼 나옵니다.

  • MountVolume.SetUp failed for volume ... secret ... not found
  • configmap ... not found

7분: IRSA/IAM 권한 문제(AccessDenied) 빠르게 잡기

EKS에서 CrashLoop의 “체감상” 가장 흔한 원인 중 하나가 AWS API 호출 권한 문제입니다. 앱이 기동 중 S3/SSM/Secrets Manager/KMS 등을 읽다가 AccessDenied 를 만나고 즉시 종료하는 패턴이 많습니다.

확인 순서는 다음이 빠릅니다.

  1. 이전 로그에서 AccessDenied 문자열 확인
  2. ServiceAccount에 IRSA annotation이 붙어 있는지 확인
  3. Pod가 실제로 그 ServiceAccount를 쓰는지 확인
  4. Trust policy와 OIDC issuer가 맞는지 확인

명령어:

# Pod가 사용하는 ServiceAccount
kubectl get pod -n myns mypod-xxxxx -o jsonpath='{.spec.serviceAccountName}'

# ServiceAccount annotation (role-arn)
kubectl get sa -n myns myserviceaccount -o yaml

IRSA 점검은 경우의 수가 많아서 체크리스트 형태로 보는 게 빠릅니다.

8분: 컨테이너 커맨드/엔트리포인트/권한 문제 확인

이미지 자체는 정상인데, 실행 커맨드가 잘못되면 즉시 종료합니다.

  • command / args 오타
  • 쉘 스크립트에 실행 권한 없음
  • #!/bin/sh 경로 불일치(알파인/디비안 차이)
  • 워킹 디렉터리/파일 경로가 이미지와 불일치

확인은 Pod spec과 이미지 메타를 함께 봅니다.

kubectl get pod -n myns mypod-xxxxx -o yaml | sed -n '1,220p'

그리고 로컬에서 동일 이미지로 커맨드를 재현해보면 바로 드러납니다.

# 로컬 재현(가능한 경우)
docker run --rm -it myrepo/myimage:tag sh -lc 'ls -al && ./start.sh'

EKS 노드가 containerd를 쓰더라도 “문제 재현” 관점에선 Docker가 가장 빠른 도구인 경우가 많습니다.

9분: 네트워크/의존성 장애를 “기동 단계”에서 분리하기

앱이 기동하자마자 외부 의존성(DB, Redis, Kafka, 외부 API)에 붙다가 실패하고 종료하는 설계라면, 작은 네트워크 이슈도 CrashLoop로 증폭됩니다.

빠르게 분리 진단하는 방법은 다음입니다.

  • 앱을 “의존성 없이 기동” 가능하도록 플래그 제공(가능하면)
  • 같은 네임스페이스에 임시 디버그 Pod를 띄워 DNS/연결만 확인
# 임시 디버그 Pod (curl/dig 등)
kubectl run -n myns net-debug --rm -it --image=curlimages/curl -- sh

# Pod 내부에서
# DNS 확인
nslookup mydb.myns.svc.cluster.local

# HTTP 연결 확인
curl -sv http://myservice.myns.svc.cluster.local:8080/healthz

여기서 DNS가 깨졌다면 CoreDNS, 네트워크 폴리시, VPC CNI, 보안그룹/라우팅 등으로 범위가 좁혀집니다.

10분: “원인 확정”을 위한 최소 재현과 롤백 전략

10분 내 목표는 “완벽한 근본 원인 분석”이 아니라 원인 후보를 하나로 수렴하고, 서비스 복구를 위한 선택지를 확보하는 것입니다.

최소 재현 체크

  • --previous 로그에 같은 에러가 반복되는가
  • Events 의 실패 원인이 일관적인가
  • 프로브/리소스/권한 중 하나를 바꿨을 때 증상이 즉시 변하는가

가장 빠른 복구 옵션

  • 직전 정상 버전으로 롤백
kubectl rollout history deploy -n myns myapp
kubectl rollout undo deploy -n myns myapp
kubectl rollout status deploy -n myns myapp
  • 프로브 완화(특히 기동이 느린 서비스)
  • 메모리 limit 상향으로 OOM 여부 확정
  • IRSA/Secret 누락은 즉시 수정 후 재배포

자주 만나는 원인 10가지 요약(체크리스트)

아래 10개는 현장에서 실제로 CrashLoopBackOff로 가장 자주 이어지는 항목들입니다.

  1. 앱 설정/환경변수 누락으로 기동 실패
  2. DB/Redis 등 의존성 연결 실패를 “즉시 종료”로 처리
  3. livenessProbe 가 너무 빨라서 kubelet이 앱을 죽임
  4. startupProbe 부재로 기동 지연을 장애로 오판
  5. OOMKilled (limit 부족, 런타임 메모리 튜닝 미스)
  6. Secret/ConfigMap 키 오타 또는 리소스 미존재
  7. 볼륨 마운트 경로/권한 문제(subPath 포함)
  8. IRSA 설정 불일치로 AWS API AccessDenied
  9. 엔트리포인트/커맨드/실행 권한 문제
  10. 이미지 태그/아키텍처 불일치로 실행 불가(특히 ARM/AMD 혼재)

결론: 가장 빠른 루트는 describe--previous

CrashLoopBackOff 대응에서 시간을 아끼는 핵심은 “추측”을 줄이고, 쿠버네티스가 이미 수집한 신호를 먼저 소비하는 것입니다.

  • kubectl describe podLast StateEvents 확인
  • kubectl logs --previous 로 직전 크래시 로그 확인
  • 그 다음에 프로브, OOM, 마운트, IRSA 순으로 범위를 좁히기

이 루틴만 습관화해도, 대부분의 CrashLoopBackOff는 10분 안에 원인 후보가 1개로 수렴합니다.