- Published on
EKS CrashLoopBackOff 진단 - Pod 재시작 원인 추적
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스가 아닌 이상, Kubernetes에서 CrashLoopBackOff는 “앱이 죽는다”는 단순 현상만 의미하지 않습니다. 컨테이너 프로세스가 종료되고(Kubelet이 재시작), 재시작 간격이 점점 늘어나는(backoff) 상태를 뜻합니다. EKS에서는 여기에 노드(EC2), CNI, IAM, 스토리지(EBS/EFS), ALB/Ingress, Bottlerocket 등 다양한 레이어가 얽히기 때문에, 감으로 접근하면 시간이 크게 낭비됩니다.
이 글은 EKS에서 CrashLoopBackOff를 만났을 때 가장 먼저 확인할 것 → 빠르게 원인을 좁히는 순서 → 자주 나오는 패턴별 해결 포인트를 실무 관점으로 정리합니다.
CrashLoopBackOff의 본질: “죽는 이유”를 분해하기
CrashLoopBackOff는 대개 아래 중 하나로 귀결됩니다.
- 프로세스가 즉시 종료: 설정 오류, 필수 환경변수 누락, 파일/권한 문제, 라이브러리 로드 실패 등
- 리소스 부족으로 강제 종료: OOMKilled(메모리), CPU 스로틀링으로 타임아웃/프로브 실패
- 헬스체크(Probe)로 인한 킬: livenessProbe가 너무 빡세서 정상 앱도 죽임
- 의존성 준비 전 기동: DB/Redis/외부 API가 아직 준비 안 됨
- 노드/런타임 문제: 디스크 가득참, 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: CrashLoopBackOffLast State: Terminated의Reason,Exit Code,Started,FinishedEvents의Back-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(롤링 업데이트/스케일링/프로브/프리엠션 등)
describe에 Reason: 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입니다. describe에 OOMKilled가 보이면 컨테이너 메모리 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가지입니다.
- 재시도(backoff) 로직을 앱에 넣고 프로세스는 살아있기
- initContainer로 의존성 체크 후 본 컨테이너 시작
- 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) 재현 가능한 진단 루틴(체크리스트)
현장에서 가장 효율이 좋았던 순서를 체크리스트로 정리합니다.
kubectl describe pod에서 Last State / Exit Code / Events 확인kubectl logs --previous로 죽기 직전 로그 확보- 이벤트에
Unhealthy가 있으면 probe 설정부터 의심 Exit 137또는OOMKilled면 resources + 노드 OOM 확인- 특정 노드에서만 발생하면 node condition/pressure 확인
- 스토리지 이벤트(
FailedMount)가 보이면 PVC/CSI로 전환 - 외부 의존성 실패면 initContainer / readiness / 재시도로 설계 변경
5) 실전 예시: CrashLoopBackOff를 “원인”으로 바꾸는 출력 읽기
예시 1) OOMKilled
describe에서:
Last State: TerminatedReason: OOMKilledExit 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 디버깅 가이드를 함께 참고하면 진단 속도가 훨씬 빨라집니다.