Published on

Kubernetes CrashLoopBackOff 원인별 10분 진단

Authors

서버가 살아있는데 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 Code
  • Events:Back-off restarting failed container, OOMKilled, Error, FailedMount

여기서 Exit CodeEvents만으로도 절반은 끝납니다.

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: TerminatedReason: OOMKilled
  • Exit 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가지

  1. kubectl logs만 보고 끝내기: CrashLoop는 --previous가 본게임입니다.
  2. Readiness 실패를 CrashLoop 원인으로 착각: readiness는 트래픽 제외일 뿐, 재시작은 보통 liveness가 트리거입니다.
  3. 리소스 request/limit 미설정: 노드 압박 시 예측 불가능한 OOM/eviction이 납니다.
  4. 부팅 경로에 외부 의존성 호출: 외부 API 타임아웃 하나로 Pod가 계속 죽을 수 있습니다.
  5. 배치/마이그레이션을 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 헬스체크부터