- Published on
Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 죽었다가 살아나는 현상은 늘 비슷해 보이지만, Kubernetes의 CrashLoopBackOff는 특히 “증상은 하나, 원인은 여러 개”라서 진단 순서가 중요합니다. 무작정 이미지 재빌드나 노드 교체부터 시작하면 시간만 날리고, 같은 장애가 다시 반복됩니다.
이 글에서는 로그/이벤트 → Probe → 리소스(OOM/CPU) → 설정/의존성 순으로, 가장 흔한 CrashLoopBackOff 원인을 빠르게 좁혀가는 실전 체크리스트를 제공합니다. (EKS에서 DNS 계열 CrashLoopBackOff가 의심된다면 AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결도 함께 확인하는 것을 권장합니다.)
CrashLoopBackOff를 정확히 이해하기
CrashLoopBackOff는 컨테이너가 종료(exit) → kubelet이 재시작 → 또 종료가 반복될 때 붙는 상태입니다.- “왜 종료했는지”는 컨테이너 종료 코드, 이벤트(Events), 이전 로그(--previous), Probe 실패 기록에서 드러납니다.
핵심은 아래 3가지를 가장 먼저 확보하는 것입니다.
- 마지막 종료 사유(Exit Code / Reason)
- 직전 컨테이너 로그
- kubelet 이벤트(Probe 실패, OOMKilled, ImagePull, Mount 등)
1) 3분 안에 원인 범주를 좁히는 최소 명령 세트
아래 명령만으로도 대부분 “앱 크래시”, “Probe로 인한 재시작”, “OOMKilled”, “권한/마운트/시크릿 문제” 중 어디인지 갈립니다.
# 1) CrashLoopBackOff인 Pod 확인
kubectl get pod -n <ns> -o wide
# 2) Pod 이벤트/상태/종료코드 확인 (가장 중요)
kubectl describe pod -n <ns> <pod>
# 3) 직전(이전) 컨테이너 로그 확인
kubectl logs -n <ns> <pod> -c <container> --previous
# 4) 현재 컨테이너 로그(재시작 후 살아있다면)
kubectl logs -n <ns> <pod> -c <container> --tail=200
describe에서 특히 아래 필드를 눈여겨보세요.
Last State: Terminated→Reason,Exit CodeState: Waiting→Reason: CrashLoopBackOffEvents:→Back-off restarting failed container,OOMKilled,Liveness probe failed등
종료 코드 빠른 해석
Exit Code 0: 정상 종료인데 재시작 정책 때문에 반복(예: Job을 Deployment로 띄움)Exit Code 1/2: 앱 예외/설정 오류가 흔함Exit Code 137: OOMKilled(메모리 부족) 가능성이 매우 큼(SIGKILL)Exit Code 139: Segfault(네이티브 라이브러리/CGO/메모리 접근)
2) 로그로 잡히는 “앱 자체 크래시” 패턴
가장 흔한 케이스는 애플리케이션이 시작 직후 설정/의존성 문제로 죽는 것입니다.
(1) ConfigMap/Secret 누락 또는 환경변수 오류
kubectl logs --previous에서 다음이 보이면 거의 확정입니다.
KeyError,Missing env,failed to load config,permission denied(파일)- DB/Redis 접속 실패 후 즉시 종료(재시도 없이 종료)
대응
- 환경변수/마운트 경로가 맞는지 확인
- Secret 키 이름 오타, namespace 불일치 확인
- 앱이 의존 서비스 연결 실패 시 즉시 종료하지 말고 재시도/지연을 두는 것이 운영적으로 안전합니다.
# Pod에 실제로 주입된 env 확인
kubectl exec -n <ns> <pod> -c <container> -- printenv | sort
# 마운트된 파일 확인
kubectl exec -n <ns> <pod> -c <container> -- ls -al /etc/config
(2) 컨테이너 커맨드/엔트리포인트 오타
exec: "...": executable file not found in $PATH 같은 메시지는 이미지 내부에 실행 파일이 없거나, command/args가 잘못된 경우입니다.
# 잘못된 예: 바이너리 경로 오타
containers:
- name: app
image: myapp:1.0
command: ["/app/bin/starttt"]
대응: 로컬/CI에서 이미지 내부 파일을 확인하거나, 임시로 쉘 진입이 가능한 이미지로 검증합니다.
# 이미지에 쉘이 있다면
kubectl exec -n <ns> -it <pod> -c <container> -- sh
# 없으면 디버그용 ephemeral container를 고려
(3) 파이썬/노드 의존성 꼬임(런타임에서 ModuleNotFoundError)
빌드는 성공했는데 실행 시 ModuleNotFoundError가 뜨면, 가상환경/인터프리터 경로/site-packages가 어긋난 경우가 많습니다. 컨테이너에서도 동일하게 발생합니다.
- 관련 진단 체크리스트: pip install은 성공인데 실행하면 ModuleNotFoundError...
컨테이너 내부에서 아래를 확인하세요.
kubectl exec -n <ns> <pod> -c <container> -- python -c "import sys; print(sys.executable); print(sys.path)"
kubectl exec -n <ns> <pod> -c <container> -- python -c "import pkgutil; print('uvicorn' in [m.name for m in pkgutil.iter_modules()])"
3) Probe(liveness/readiness/startup) 때문에 죽는 경우
앱은 살아있는데 liveness probe 실패로 kubelet이 컨테이너를 죽여서 CrashLoopBackOff가 발생할 수 있습니다. 이때 로그에는 “정상 시작”이 찍히는데도 재시작합니다.
(1) 이벤트에서 Probe 실패를 먼저 확인
kubectl describe pod -n <ns> <pod>
# Events:
# Liveness probe failed: HTTP probe failed with statuscode: 500
# Killing container with id ...
(2) startupProbe가 필요한데 liveness만 있는 패턴
초기화가 긴 앱(마이그레이션, 캐시 워밍업, 모델 로딩 등)은 시작 중에 liveness가 먼저 때리면 죽습니다. 이때는 startupProbe로 “부팅 완료 전에는 죽이지 말라”를 명시해야 합니다.
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 60 # 60 * periodSeconds 동안 기동 유예
periodSeconds: 2
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
포인트
startupProbe가 성공한 이후에만livenessProbe가 의미를 갖습니다.- readiness는 “트래픽 받을 준비”이고, liveness는 “죽었는지”입니다. readiness 실패는 재시작 사유가 아닙니다.
(3) HTTP 500/timeout의 진짜 원인: 의존성/DNS/네트워크
Probe는 단지 “헬스 체크가 실패했다”만 알려줍니다. 실패 이유는 보통 아래입니다.
- 앱 내부에서 DB/캐시 연결이 안 되면
/healthz가 500을 반환 - DNS 타임아웃으로 외부 의존성 접근이 지연되어 Probe timeout
EKS에서 DNS 계열이면 CoreDNS 리소스/노드 네트워크/엔드포인트 문제로 번질 수 있어, 별도 가이드(AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결)를 참고해 빠르게 분리 진단하는 것이 좋습니다.
4) 리소스(OOMKilled/CPU Throttling)로 인한 CrashLoopBackOff
(1) OOMKilled: Exit Code 137 + 이벤트 확인
describe에서 아래가 보이면 거의 확정입니다.
Last State: Terminated→Reason: OOMKilled- 혹은
Exit Code: 137
kubectl describe pod -n <ns> <pod> | sed -n '/Last State:/,/Events:/p'
대응 전략
resources.limits.memory를 올리거나, 메모리 사용량을 줄입니다.- 특히 JVM/Node/Python에서도 “초기 로딩 때 피크”가 발생할 수 있어, 평상시 metrics만 보고 limits를 잡으면 터집니다.
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
메모리 피크를 보려면 metrics-server/Prometheus가 필요하지만, 최소한 현재 시점은 이렇게 확인합니다.
kubectl top pod -n <ns>
kubectl top node
(2) CPU Throttling으로 “헬스 체크 타임아웃 → 재시작”
CPU limit이 너무 낮으면 애플리케이션이 느려지고, 결과적으로 probe timeout이 발생해 재시작 루프로 들어갈 수 있습니다. 이 경우 이벤트에는 Liveness probe failed: context deadline exceeded가 찍히지만, 근본 원인은 CPU입니다.
대응
timeoutSeconds를 무작정 늘리기 전에 CPU limit/request를 현실화- 초기화 구간에는
startupProbe로 보호
5) 볼륨/권한/파일시스템 문제로 즉시 종료하는 경우
다음 이벤트가 보이면 앱 문제가 아니라 쿠버네티스/노드/스토리지 계층입니다.
MountVolume.SetUp failedpermission denied(특히 non-root + PV)Read-only file system
kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'
(1) securityContext로 인한 권한 문제
non-root로 실행하면서 PV 디렉터리 권한이 맞지 않으면, 앱이 로그/캐시 파일 쓰기에서 죽습니다.
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroup는 볼륨 파일 권한을 맞추는 데 자주 필요합니다.
6) “정상 종료”인데 CrashLoopBackOff처럼 보이는 배치/엔트리 설계 문제
컨테이너가 일을 끝내고 exit 0으로 종료하는 것이 정상인 워크로드를 Deployment로 띄우면, Kubernetes는 계속 재시작합니다.
체크 포인트
Exit Code 0인데 재시작이 반복된다면- Deployment가 아니라 Job/CronJob이 맞는지
restartPolicy가 의도와 맞는지
apiVersion: batch/v1
kind: Job
metadata:
name: one-shot-task
spec:
template:
spec:
restartPolicy: Never
containers:
- name: task
image: mytask:1.0
command: ["python", "run_once.py"]
7) 디버깅을 더 빠르게 만드는 실전 팁 5가지
(1) 재시작이 너무 빨라 로그를 못 볼 때: sleep로 붙잡기
임시로 entrypoint를 바꿔 컨테이너를 살려두고 내부를 조사합니다.
containers:
- name: app
image: myapp:1.0
command: ["sh", "-c", "sleep 3600"]
원인 파악 후 반드시 원복하세요.
(2) --previous는 습관처럼
CrashLoopBackOff에서는 “현재 로그”가 아니라 “직전 죽은 컨테이너 로그”가 핵심입니다.
kubectl logs -n <ns> <pod> -c <container> --previous --tail=300
(3) 이벤트 정렬로 원인 흐름 보기
kubectl get events -n <ns> --sort-by='.lastTimestamp'
Probe 실패 → Killing → Back-off 순서가 명확해집니다.
(4) readiness는 트래픽 차단용으로만 쓰기
readiness 실패를 “재시작 트리거”로 쓰면 장애가 증폭됩니다. 재시작은 liveness/startup에 맡기고, readiness는 의존성 준비/트래픽 보호에 집중하세요.
(5) 외부 헬스체크(ALB/NLB)와 Probe의 상호작용 점검
Ingress/ALB 헬스체크가 빡세면, readiness가 흔들릴 때 502/504로 관측될 수 있습니다. L7 헬스체크 튜닝은 AWS ALB 502·504 난사 - 원인별 해결 체크리스트도 참고하면 좋습니다.
8) 원인별 요약 테이블(현장에서 바로 쓰는 체크리스트)
| 증상/단서 | describe / 이벤트 | 로그 패턴 | 1차 조치 |
|---|---|---|---|
| 앱 시작 직후 즉시 종료 | Exit Code 1/2 | 설정/의존성 에러 | logs --previous, env/secret/command 점검 |
| OOMKilled | Reason: OOMKilled / Exit 137 | 메모리 부족 전조/없을 수도 | memory limit 상향, 메모리 누수/피크 분석 |
| Probe로 킬 | Liveness probe failed 후 Killing | 앱은 살아보이기도 함 | startupProbe 도입, timeout/period 조정, 헬스 엔드포인트 분리 |
| 마운트 실패 | MountVolume.SetUp failed | 앱 로그 거의 없음 | PV/Secret/ConfigMap, 권한(securityContext) 점검 |
| 정상 종료 반복 | Exit 0 | 작업 완료 로그 | Deployment → Job/CronJob 전환 |
| DNS/네트워크 이슈 | 타임아웃/연결 실패 이벤트 | i/o timeout, NXDOMAIN | CoreDNS/네트워크 분리 진단 |
마무리: “CrashLoopBackOff”를 상태가 아니라 흐름으로 보자
CrashLoopBackOff는 하나의 에러가 아니라 재시작 루프라는 결과입니다. 따라서 진단은 항상 “종료 코드/이벤트/직전 로그”로 시작해, Probe와 리소스로 확장하는 순서가 가장 빠릅니다.
describe pod로 Reason/Exit Code/Events를 먼저 확정logs --previous로 죽기 직전의 단서를 확보- Probe 설정은
startupProbe로 초기화 구간을 보호 - OOM/CPU는 limit이 아니라 실제 피크 기준으로 재설계
이 순서대로만 해도, 대부분의 CrashLoopBackOff는 10~20분 내에 “원인 범주”가 결정되고, 재발 방지까지 연결할 수 있습니다.