- Published on
Kubernetes CrashLoopBackOff 원인별 10분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 살아있는데 Pod만 계속 죽고 다시 뜨는 상황, 즉 CrashLoopBackOff는 Kubernetes에서 가장 흔하면서도 시간 잡아먹는 장애 유형입니다. 중요한 포인트는 **CrashLoopBackOff 자체가 에러 원인이 아니라 “컨테이너가 반복적으로 비정상 종료되어 kubelet이 재시도(back-off)하는 상태”**라는 점입니다.
이 글은 “로그 좀 보고 감으로 때려 맞추기”가 아니라, 10분 안에 원인을 분류하고 바로 조치할 수 있도록 **원인별 진단 루트(명령어 + 관찰 포인트 + 해결책)**를 제공합니다.
10분 진단 플로우(가장 빠른 루트)
아래 순서대로 진행하면 대부분의 CrashLoopBackOff는 10분 내에 원인 범위를 좁힐 수 있습니다.
1) 상태/이벤트로 “왜 재시도 중인지” 먼저 확인 (1분)
kubectl -n <ns> get pod <pod> -o wide
kubectl -n <ns> describe pod <pod>
describe의 핵심은 아래 두 구역입니다.
Containers:→State: Waiting/Last State: Terminated/Exit CodeEvents:→Back-off restarting failed container,OOMKilled,Error,FailedMount등
여기서 Exit Code와 Events만으로도 절반은 끝납니다.
2) “이전(직전) 크래시 로그”를 반드시 본다 (1분)
CrashLoopBackOff는 컨테이너가 이미 재시작했을 가능성이 높습니다. 그래서 --previous가 핵심입니다.
kubectl -n <ns> logs <pod> -c <container> --previous
kubectl -n <ns> logs <pod> -c <container> --tail=200
--previous로그가 비어 있으면: 프로세스가 너무 빨리 죽거나, 애초에 컨테이너가 실행되지 않았거나(이미지/마운트 실패 등), 로그가 stdout/stderr로 안 나갈 수 있습니다.
3) 종료 코드로 원인 분기 (2분)
Exit Code 1/2→ 앱 설정/인자/환경변수/권한/의존성 문제 가능성 큼Exit Code 137→ OOM(메모리 부족) 의심Exit Code 139→ 세그폴트(네이티브 크래시)Exit Code 126/127→ 실행 권한/엔트리포인트/명령어 없음Completed(0)인데 CrashLoop? →restartPolicy: Always+ 단발성 작업(배치) 설계 오류
4) “프로브 때문에 죽는지”를 확인 (2분)
앱이 실제로는 살아있는데, livenessProbe 실패로 kubelet이 죽이는 케이스가 매우 많습니다.
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[0].livenessProbe}'
kubectl -n <ns> describe pod <pod> | sed -n '/Events:/,$p'
Events에 Liveness probe failed / Readiness probe failed가 반복되면 프로브가 원인입니다.
5) 마지막으로 “노드/리소스/마운트” 확인 (4분)
kubectl -n <ns> top pod <pod>
kubectl -n <ns> get events --sort-by=.lastTimestamp | tail -n 30
kubectl get node -o wide
FailedMount,MountVolume.SetUp failed→ 볼륨/시크릿/권한ImagePullBackOff는 CrashLoop과 다르지만, 재시작처럼 보일 수 있음
원인별 체크리스트(가장 흔한 8가지)
이제부터는 원인별로 “무엇을 보면 확정인지”와 “어떻게 고치는지”를 빠르게 정리합니다.
1) 앱이 즉시 종료됨(프로세스가 포그라운드가 아님)
증상
- 로그가 짧고 곧바로 종료
Last State: Terminated/Exit Code: 0또는1- Dockerfile/ENTRYPOINT가 데몬을 백그라운드로 띄우고 끝나는 형태
진단 포인트
컨테이너는 PID 1 프로세스가 살아있어야 유지됩니다. 예를 들어 nginx를 daemon on;으로 띄우면 PID 1이 바로 종료될 수 있습니다.
해결
- 포그라운드 실행으로 변경
- 필요하면
tini같은 init 사용
# 예: nginx는 포그라운드로
CMD ["nginx", "-g", "daemon off;"]
2) command/args 또는 ENTRYPOINT 오버라이드 실수 (Exit 126/127)
증상
Exit Code: 127(command not found)Exit Code: 126(permission denied)- 로그에
exec: "...": executable file not found in $PATH
진단
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[0].command}'
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[0].args}'
해결
- 이미지 내부 경로 확인
- 실행 권한 부여
command/args를 제거해 이미지의 기본 ENTRYPOINT를 사용
containers:
- name: app
image: my/app:1.0
# command/args를 잘못 지정했다면 우선 제거해 기본 동작 확인
3) 환경변수/설정 누락(Secret/ConfigMap)로 앱이 크래시
증상
- 로그에
missing env,cannot read config,invalid DSN,JWT secret not set등 Exit Code: 1
진단
kubectl -n <ns> describe pod <pod> | sed -n '/Environment:/,/Mounts:/p'
kubectl -n <ns> get configmap
kubectl -n <ns> get secret
해결
envFrom/valueFrom.secretKeyRef키 이름 오타 확인- Secret이 base64로 잘 들어갔는지 확인
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secret
key: database_url
참고로 인증/서명 관련 설정 누락은 앱이 부팅 단계에서 종료되는 대표 케이스입니다. JWT 서명 검증 실패 원인 정리도 함께 보면 설정 검증에 도움이 됩니다: JWT invalid signature 서명검증 실패 원인 7가지
4) OOMKilled (Exit 137): 메모리 제한이 너무 낮음
증상
Last State: Terminated에Reason: OOMKilledExit Code: 137
진단
kubectl -n <ns> describe pod <pod> | sed -n '/Last State:/,/Events:/p'
kubectl -n <ns> top pod <pod>
해결
resources.limits.memory상향- JVM/Node/Python 등 런타임 메모리 옵션 조정
- 캐시/버퍼/동시성 제한
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
추가 팁: OOM은 재시작 후 메모리 사용량이 초기화되므로 top pod만 보면 놓칠 수 있습니다. describe의 OOMKilled가 더 결정적입니다.
5) livenessProbe가 너무 공격적이라 kubelet이 계속 죽임
증상
- 앱은 뜨지만 일정 시간 후 반복 재시작
- Events에
Liveness probe failed반복 - 부팅이 느린 앱(마이그레이션/캐시 워밍업)에서 흔함
진단
kubectl -n <ns> describe pod <pod> | grep -n "Liveness\|Readiness\|Startup" -n
kubectl -n <ns> describe pod <pod> | sed -n '/Events:/,$p'
해결
- startupProbe를 도입해 초기 구간은 관대하게
- livenessProbe의
initialDelaySeconds,failureThreshold,timeoutSeconds조정
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
6) 의존 서비스(DB/Redis/외부 API) 연결 실패로 부팅 중 크래시
증상
- 로그에
connection refused,timeout,ENOTFOUND,TLS handshake등 - readiness가 아니라 앱 자체가 부팅 실패로 종료
진단
- 서비스 DNS/엔드포인트 확인
kubectl -n <ns> get svc
kubectl -n <ns> get endpoints
- 임시 디버그 Pod로 네트워크 확인
kubectl -n <ns> run net-debug --rm -it --image=busybox:1.36 -- sh
# inside
nslookup mydb.default.svc.cluster.local
wget -qSO- http://myservice:8080/health || true
해결
- 앱을 “의존 서비스 연결 실패=즉시 종료”로 만들지 말고, 재시도/백오프/서킷브레이커 적용
- 외부 API 호출이 부팅 경로에 있다면 특히 위험
재시도/백오프 설계는 외부 의존성 불안정으로 인한 CrashLoop를 줄이는 데 직접적으로 도움이 됩니다: OpenAI 429/Rate Limit 대응 - 재시도·백오프·큐잉
7) 볼륨 마운트/권한 문제(FailedMount, permission denied)
증상
- Events에
FailedMount,MountVolume.SetUp failed - 앱 로그에
permission denied(특히 non-root + hostPath/PVC 조합)
진단
kubectl -n <ns> describe pod <pod> | sed -n '/Volumes:/,/Events:/p'
kubectl -n <ns> get pvc
kubectl -n <ns> describe pvc <pvc>
해결
- PVC 바인딩 상태 확인
securityContext.fsGroup로 볼륨 권한 맞추기
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
8) “배치 Job을 Deployment로 돌림” (Exit 0인데 계속 재시작)
증상
- 로그상 정상 처리 후 종료
Exit Code: 0인데도 Pod가 다시 뜸
원인
Deployment는 기본 restartPolicy: Always라서 정상 종료해도 재시작합니다.
해결
- 단발성 작업은
Job/CronJob으로 옮기기
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-once
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: my/app:1.0
command: ["sh","-lc","python manage.py migrate"]
실전: CrashLoopBackOff를 “재현 가능한 증거”로 남기는 방법
장애 대응에서 가장 중요한 건 “추측”이 아니라 **재현 가능한 증거(로그/이벤트/설정 diff)**입니다.
이벤트/로그/스펙을 한 번에 수집
NS=<ns>
POD=<pod>
kubectl -n $NS describe pod $POD > describe.txt
kubectl -n $NS logs $POD --all-containers --tail=300 > logs.txt
kubectl -n $NS logs $POD --all-containers --previous --tail=300 > logs-previous.txt
kubectl -n $NS get pod $POD -o yaml > pod.yaml
kubectl -n $NS get events --sort-by=.lastTimestamp | tail -n 80 > events.txt
이 5개 파일만 있어도 원인 분석 속도가 급격히 올라갑니다.
자주 놓치는 함정 5가지
kubectl logs만 보고 끝내기: CrashLoop는--previous가 본게임입니다.- Readiness 실패를 CrashLoop 원인으로 착각: readiness는 트래픽 제외일 뿐, 재시작은 보통 liveness가 트리거입니다.
- 리소스 request/limit 미설정: 노드 압박 시 예측 불가능한 OOM/eviction이 납니다.
- 부팅 경로에 외부 의존성 호출: 외부 API 타임아웃 하나로 Pod가 계속 죽을 수 있습니다.
- 배치/마이그레이션을 Deployment에 넣기: 정상 종료가 곧 CrashLoop가 됩니다.
10분 진단 요약(체크박스)
-
kubectl describe pod에서Exit Code/Reason/Events확인 -
kubectl logs --previous로 직전 크래시 로그 확보 -
Exit 137이면 OOMKilled부터 처리 - Events에 probe 실패가 있으면
startupProbe/임계값 조정 - Secret/ConfigMap 키 오타 및 누락 확인
- 의존 서비스 DNS/엔드포인트/네트워크 확인(디버그 Pod)
- 볼륨/PVC 바인딩 및 권한(fsGroup) 확인
- 정상 종료(Exit 0)인데 재시작이면 Job/CronJob로 전환
마무리
CrashLoopBackOff는 “쿠버네티스가 이상함”이 아니라, 컨테이너가 죽는 이유를 kubelet이 반복해서 노출해주는 상태입니다. 따라서 가장 빠른 해결법은 관찰 지점을 표준화하는 것입니다.
이 글의 10분 플로우대로 describe → previous logs → exit code 분기 → probe/리소스/마운트 확인만 습관화해도, 대부분의 CrashLoop는 재현 가능한 근거와 함께 짧은 시간 안에 정리할 수 있습니다.
추가로 인그레스/로드밸런서 레벨에서 헬스체크가 얽히면 “Pod는 살아있는데 서비스는 죽은 것처럼 보이는” 역전 현상도 자주 생깁니다. 그런 케이스는 다음 글도 함께 참고하세요: EKS Ingress 502인데 Pod 로그가 비면? ALB/NLB 헬스체크부터