Published on

EKS CrashLoopBackOff 진단 - Pod 재시작 원인 추적

Authors

서버리스가 아닌 이상, Kubernetes에서 CrashLoopBackOff는 “앱이 죽는다”는 단순 현상만 의미하지 않습니다. 컨테이너 프로세스가 종료되고(Kubelet이 재시작), 재시작 간격이 점점 늘어나는(backoff) 상태를 뜻합니다. EKS에서는 여기에 노드(EC2), CNI, IAM, 스토리지(EBS/EFS), ALB/Ingress, Bottlerocket 등 다양한 레이어가 얽히기 때문에, 감으로 접근하면 시간이 크게 낭비됩니다.

이 글은 EKS에서 CrashLoopBackOff를 만났을 때 가장 먼저 확인할 것 → 빠르게 원인을 좁히는 순서 → 자주 나오는 패턴별 해결 포인트를 실무 관점으로 정리합니다.

CrashLoopBackOff의 본질: “죽는 이유”를 분해하기

CrashLoopBackOff는 대개 아래 중 하나로 귀결됩니다.

  1. 프로세스가 즉시 종료: 설정 오류, 필수 환경변수 누락, 파일/권한 문제, 라이브러리 로드 실패 등
  2. 리소스 부족으로 강제 종료: OOMKilled(메모리), CPU 스로틀링으로 타임아웃/프로브 실패
  3. 헬스체크(Probe)로 인한 킬: livenessProbe가 너무 빡세서 정상 앱도 죽임
  4. 의존성 준비 전 기동: DB/Redis/외부 API가 아직 준비 안 됨
  5. 노드/런타임 문제: 디스크 가득참, cgroup/컨테이너 런타임 이슈, 커널 OOM 등

핵심은 “CrashLoopBackOff”라는 라벨을 보는 게 아니라, **종료 코드(exit code), 마지막 상태(lastState), 이벤트(events), 로그(logs)**로 “왜 죽었는지”를 증명하는 것입니다.

1) 3분 컷: 가장 먼저 치는 kubectl 명령 세트

(1) Pod 상태/이벤트를 한 번에 보기

# 네임스페이스 포함해서 확인
kubectl -n <ns> get pod <pod-name> -o wide

# 이벤트 포함 상세
kubectl -n <ns> describe pod <pod-name>

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

  • State: Waiting / Reason: CrashLoopBackOff
  • Last State: TerminatedReason, Exit Code, Started, Finished
  • EventsBack-off restarting failed container, Killing container, Unhealthy(probe 실패), FailedMount, FailedPull, CreateContainerConfigError

(2) “마지막으로 죽기 직전 로그” 보기

CrashLoop은 컨테이너가 빨리 죽어서 일반 logs가 비는 경우가 많습니다. 이때는 --previous가 핵심입니다.

# 현재 컨테이너 로그
kubectl -n <ns> logs <pod-name> -c <container-name>

# 직전(죽기 전) 컨테이너 로그
kubectl -n <ns> logs <pod-name> -c <container-name> --previous

(3) 종료 코드로 방향 잡기

  • Exit Code 1/2: 애플리케이션 예외/설정 오류 가능성 큼
  • Exit Code 137: OOMKilled 또는 SIGKILL(리소스/노드 압박)
  • Exit Code 143: SIGTERM(롤링 업데이트/스케일링/프로브/프리엠션 등)

describeReason: OOMKilled가 찍히면 거의 확정입니다.

2) 원인별로 가장 흔한 패턴과 확인 포인트

2.1 설정/환경변수/시크릿 누락: CreateContainerConfigError vs CrashLoop

환경변수나 Secret/ConfigMap 참조가 깨지면 CrashLoop이 아니라 시작 자체가 안 되며 CreateContainerConfigError로 나오는 경우가 많습니다. 하지만 앱이 뜬 뒤 내부에서 설정을 읽다가 죽으면 CrashLoop로 보일 수 있습니다.

확인:

kubectl -n <ns> describe pod <pod-name>
kubectl -n <ns> get deploy <deploy-name> -o yaml | sed -n '1,200p'
kubectl -n <ns> get cm,secret | grep <name>

해결 팁:

  • envFrom로 통째로 주입할 때 키가 비어 있거나 타입이 꼬이면 앱이 바로 죽는 패턴이 많습니다.
  • Secret이 바뀌어도 프로세스가 자동 리로드되지 않는 앱은 재기동 타이밍에만 터집니다.

2.2 이미지/엔트리포인트 문제: “컨테이너가 실행되자마자 종료”

대표 로그:

  • exec format error (ARM/AMD64 아키텍처 불일치)
  • no such file or directory (ENTRYPOINT 경로, 쉘 존재 여부)
  • 권한 문제(실행 비트)

확인:

kubectl -n <ns> logs <pod-name> --previous
kubectl -n <ns> get pod <pod-name> -o jsonpath='{.spec.containers[0].image}'

EKS는 Graviton(ARM)과 x86 노드가 섞일 수 있어, 멀티아키 이미지가 아닌데 스케줄링이 엇갈리면 특정 노드에서만 CrashLoop가 납니다.

2.3 OOMKilled(Exit 137): 메모리 제한/누수/스파이크

EKS에서 CrashLoopBackOff의 상위 원인 중 하나가 OOM입니다. describeOOMKilled가 보이면 컨테이너 메모리 limit 또는 노드 메모리 압박을 의심하세요.

확인:

kubectl -n <ns> top pod <pod-name>
kubectl -n <ns> get pod <pod-name> -o jsonpath='{.spec.containers[0].resources}'

# 노드 리소스도 함께
kubectl top node

추가로 노드 커널 OOM을 확인하려면 노드 로그(예: dmesg)까지 봐야 합니다. 리눅스 OOM 진단 방법은 아래 글이 도움이 됩니다.

대응 전략:

  • limit가 너무 낮으면 올리고, requests/limits 비율도 조정
  • JVM/Node/Python 런타임은 힙 상한을 컨테이너 limit에 맞추지 않으면 쉽게 OOM
  • 메모리 스파이크가 초기 로딩 시점에만 발생하면 startupProbe 도입이 효과적

2.4 Probe(liveness/readiness/startup) 설정이 앱을 죽이는 경우

CrashLoop인데 로그를 보면 앱은 정상 기동처럼 보이는데, 이벤트에 Unhealthy와 함께 Killing container가 반복되면 livenessProbe가 오탐일 확률이 큽니다.

확인(이벤트):

kubectl -n <ns> describe pod <pod-name> | sed -n '/Events/,$p'

자주 하는 실수:

  • 기동에 40초 걸리는데 liveness가 10초부터 때림
  • /health가 DB 연결을 포함해 느린데 timeout이 1초
  • readiness 실패를 liveness로도 동일하게 설정

권장 패턴(예시 YAML):

livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

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

startupProbe:
  httpGet:
    path: /startup
    port: 8080
  periodSeconds: 5
  failureThreshold: 24 # 최대 2분까지 기동 허용

핵심은 startupProbe로 “기동 중”을 보호하고, readiness는 트래픽 유입 제어, liveness는 진짜로 “멈춤”만 잡도록 단순화하는 것입니다.

2.5 의존성(DB/Redis/외부 API) 준비 전 기동: 즉시 종료 루프

앱이 시작하면서 DB 마이그레이션/캐시 워밍업을 하고 실패 시 프로세스를 종료하는 설계라면, 의존성이 잠깐만 흔들려도 CrashLoop가 됩니다.

해결 방식은 보통 3가지입니다.

  1. 재시도(backoff) 로직을 앱에 넣고 프로세스는 살아있기
  2. initContainer로 의존성 체크 후 본 컨테이너 시작
  3. readinessProbe로 트래픽만 막고, 프로세스는 유지

initContainer 예시:

initContainers:
  - name: wait-for-db
    image: public.ecr.aws/docker/library/busybox:1.36
    command: ['sh', '-c', 'until nc -z db.example.local 5432; do echo waiting; sleep 2; done']

2.6 볼륨 마운트/권한 문제: FailedMount와 섞여 보이는 CrashLoop

EBS/EFS, projected volume, CSI 드라이버 문제 등으로 마운트가 실패하면 컨테이너가 기동 전에 막히거나, 앱이 파일을 못 읽어 죽어서 CrashLoop처럼 보일 수 있습니다.

확인:

kubectl -n <ns> describe pod <pod-name>
kubectl -n <ns> get pvc
kubectl -n <ns> describe pvc <pvc-name>

이벤트에 FailedMount, MountVolume.SetUp failed가 있으면 스토리지/권한 쪽으로 바로 파고드는 게 빠릅니다.

3) “Pod만 보지 말고 Node도 봐야” 하는 순간

특정 노드에 스케줄된 Pod만 CrashLoop가 나면, 앱 문제보다 노드 환경 차이를 먼저 의심하세요.

  • 노드 디스크 압박(DiskPressure) → 이미지 풀/로그 기록 실패
  • CNI 문제 → 네트워크 불안정으로 의존성 연결 실패
  • 노드 커널 OOM → 컨테이너가 연쇄적으로 죽음

노드에 어떤 Pod가 붙는지 확인

kubectl -n <ns> get pod <pod-name> -o wide
kubectl describe node <node-name> | sed -n '/Conditions/,$p'

Bottlerocket 노드에서 디버깅(SSH 없이)

운영 EKS에서 Bottlerocket을 쓰면 SSH가 막혀 있는 경우가 흔합니다. 이때 SSM으로 들어가 로그/상태를 확인하는 흐름을 알고 있으면 진단 시간이 크게 줄어듭니다.

4) 재현 가능한 진단 루틴(체크리스트)

현장에서 가장 효율이 좋았던 순서를 체크리스트로 정리합니다.

  1. kubectl describe pod에서 Last State / Exit Code / Events 확인
  2. kubectl logs --previous죽기 직전 로그 확보
  3. 이벤트에 Unhealthy가 있으면 probe 설정부터 의심
  4. Exit 137 또는 OOMKilledresources + 노드 OOM 확인
  5. 특정 노드에서만 발생하면 node condition/pressure 확인
  6. 스토리지 이벤트(FailedMount)가 보이면 PVC/CSI로 전환
  7. 외부 의존성 실패면 initContainer / readiness / 재시도로 설계 변경

5) 실전 예시: CrashLoopBackOff를 “원인”으로 바꾸는 출력 읽기

예시 1) OOMKilled

describe에서:

  • Last State: Terminated
  • Reason: OOMKilled
  • Exit Code: 137

이면 결론은 “앱이 메모리를 초과했다”입니다. 이때 필요한 질문은 딱 2개입니다.

  • limit이 너무 낮은가?
  • 정상 트래픽/정상 기동에서도 메모리가 튀는가(누수/캐시/힙 설정)?

예시 2) livenessProbe 오탐

이벤트에서:

  • Unhealthy: Liveness probe failed: HTTP probe failed with statuscode: 500
  • 이어서 Killing container

이면 앱이 죽는 게 아니라 Kubernetes가 죽이는 것입니다. /live는 DB 의존성을 제거하고, 기동이 느리면 startupProbe를 추가하세요.

6) 마무리: CrashLoopBackOff는 “증상”, 해답은 증거 수집

EKS에서 CrashLoopBackOff를 빠르게 끝내는 요령은 단순합니다.

  • 종료 코드와 이벤트로 큰 갈래를 먼저 나누고
  • --previous 로그로 죽기 직전 단서를 확보한 뒤
  • probe/리소스/노드/스토리지/의존성을 순서대로 배제합니다.

특히 Exit 137(OOM)Unhealthy(Killing container) 두 패턴만 확실히 잡아도, CrashLoop의 절반 이상은 빠르게 해결됩니다. 노드 레벨까지 내려가야 한다면 OOM 및 Bottlerocket 디버깅 가이드를 함께 참고하면 진단 속도가 훨씬 빨라집니다.