Published on

K8s CrashLoopBackOff 10분 원인별 진단법

Authors

서버가 죽고 다시 뜨기를 반복하는 CrashLoopBackOff는 “쿠버네티스가 문제”라기보다, 컨테이너 프로세스가 정상적으로 유지되지 못하는 상황을 의미합니다. 중요한 건 원인을 길게 추측하지 않고, 10분 안에 범주를 좁혀 확정하는 것입니다.

이 글은 다음 전략으로 접근합니다.

  • 1분: 어떤 컨테이너가, 왜, 얼마나 자주 죽는지 “사실”부터 수집
  • 3분: 종료 코드와 이벤트로 원인 범주 분류
  • 6분: 범주별로 재현 가능한 확인 명령과 즉시 조치

아래 루틴을 그대로 따라 하면, 대부분의 CrashLoopBackOff는 10분 안에 원인 후보를 1~2개로 줄일 수 있습니다.

0) CrashLoopBackOff의 의미를 한 줄로 정리

CrashLoopBackOff컨테이너가 반복적으로 종료되어 kubelet이 재시작을 지수 백오프(backoff)로 늦추는 상태입니다.

  • “왜 죽었는가”는 Pod 상태가 아니라 컨테이너 종료 원인(exit code, signal, OOM, probe 실패 등)에 있습니다.
  • 따라서 핵심 데이터는 describe 이벤트와 logs --previous, 그리고 종료 코드입니다.

1) 10분 진단 타임라인 (복붙용)

1-1. 1분: 대상 확정 (어떤 Pod/컨테이너가 문제인가)

# 네임스페이스에서 크래시 나는 Pod 찾기
kubectl get pod -n <NAMESPACE> -o wide

# 특정 Pod의 컨테이너 상태 요약(재시작 횟수, 상태)
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.state.waiting.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
  • restartCount가 빠르게 증가하면 “즉시 크래시” 성격
  • lastState.terminated.exitCode가 있으면 종료 코드 기반으로 분류 가능

1-2. 3분: 이벤트와 이전 로그 확보 (가장 중요한 2개)

# 이벤트에서 kubelet이 관측한 이유를 읽는다
kubectl describe pod -n <NAMESPACE> <POD_NAME>

# 직전 실행(크래시 직전)의 로그를 본다
kubectl logs -n <NAMESPACE> <POD_NAME> -c <CONTAINER_NAME> --previous --tail=200
  • describeEvents:에는 Back-off restarting failed container, Unhealthy, OOMKilled 등 힌트가 직접 나옵니다.
  • --previous는 CrashLoop에서 거의 필수입니다. 현재 컨테이너는 이미 새로 떠서 로그가 비어 있을 수 있습니다.

1-3. 6분: 종료 코드로 원인 범주 확정

아래 표처럼 exit code는 진단을 매우 빠르게 합니다.

  • 1: 앱 내부 에러, 설정 누락, 외부 의존성 실패 등 “일반 실패”
  • 137: OOM kill 또는 SIGKILL
  • 139: 세그폴트
  • 143: SIGTERM(종종 graceful shutdown 실패나 probe/롤링 업데이트와 연관)
# 종료 사유/메시지까지 포함해 한 번 더 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\n"}{.lastState.terminated.reason}{"\n"}{.lastState.terminated.message}{"\n"}{.lastState.terminated.exitCode}{"\n---\n"}{end}'

2) 원인 1: 앱이 “즉시 종료”하는 경우 (exit code 1)

가장 흔한 케이스입니다. 컨테이너는 잘 떴지만, 프로세스가 스스로 종료합니다.

증상

  • lastState.terminated.exitCode1 또는 2
  • logs --previous에 스택트레이스, 설정 파일 없음, 환경 변수 누락, 마이그레이션 실패 등이 보임

10분 내 확인 포인트

  1. 엔트리포인트/커맨드가 기대대로 실행되는지
kubectl get deploy -n <NAMESPACE> <DEPLOY_NAME> -o yaml | sed -n '1,200p'
  • command: args:가 이미지의 기본 엔트리포인트를 덮어쓰는지 확인
  • 쉘 스크립트에서 set -e로 인해 사소한 실패에도 종료되는지 확인
  1. ConfigMap/Secret 키 누락, 마운트 경로 오류
kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Mounts:/,/Conditions:/p'
  1. 외부 의존성(DB, 메시지 브로커, OIDC 등) 연결 실패

즉시 조치

  • “죽지 않게” 만들기보다 왜 죽는지 로그를 남기게 하는 게 우선입니다.
  • 앱이 너무 빨리 죽어 디버깅이 어려우면, 임시로 커맨드를 바꿔 컨테이너를 붙잡아 둡니다.
# 임시 디버그: 엔트리포인트 대신 sleep로 컨테이너 유지(운영에서는 주의)
kubectl patch deploy -n <NAMESPACE> <DEPLOY_NAME> --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/command","value":["/bin/sh","-c"]},
  {"op":"replace","path":"/spec/template/spec/containers/0/args","value":["sleep 3600"]}
]'

그 다음 kubectl exec로 내부에서 실행 파일/환경을 직접 확인합니다.

3) 원인 2: OOMKilled (exit code 137)

CrashLoopBackOff의 두 번째로 흔한 원인입니다. 앱은 실행되지만 메모리를 과도하게 쓰다 커널에 의해 강제 종료됩니다.

증상

  • describeOOMKilled 또는 Reason: OOMKilled
  • 종료 코드 137
  • 재시작이 특정 트래픽/배치 시점에 맞물림

10분 내 확인 포인트

# 컨테이너별 requests/limits 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{.resources}{"\n---\n"}{end}'

# metrics-server가 있다면 현재 사용량 확인
kubectl top pod -n <NAMESPACE> <POD_NAME> --containers
  • limits.memory가 너무 낮거나, requests.memory가 없어 노드 압박 상황에서 먼저 죽는지 확인
  • JVM/Node/Python 등 런타임의 기본 메모리 전략이 limits를 고려하지 않아 터지는 경우가 많습니다.

즉시 조치

  • 단기: limits.memory 상향, 문제 배치 비활성화, 캐시 크기 축소
  • 중기: 힙/워커 수/동시성 튜닝, 메모리 누수 점검

4) 원인 3: Liveness/Readiness Probe 오탐으로 강제 재시작

앱은 살아있지만 livenessProbe가 실패하면 kubelet이 컨테이너를 죽이고 재시작합니다. 이 경우도 CrashLoopBackOff로 보입니다.

증상

  • describe 이벤트에 Unhealthy 와 함께 Liveness probe failed가 반복
  • 앱 로그에는 치명적 에러가 없을 수 있음

10분 내 확인 포인트

kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Liveness:/,/Environment:/p'
  • initialDelaySeconds가 너무 짧아 “부팅 중”에 죽는지
  • timeoutSeconds가 너무 짧아 일시적 지연에도 실패하는지
  • HTTP probe의 path/port가 실제 앱과 일치하는지

즉시 조치

  • 부팅이 느린 앱은 startupProbe를 도입해 초기 구간을 보호합니다.
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  • “헬스 체크는 빠르고 가볍게”가 원칙입니다. DB 쿼리 같은 무거운 체크는 readiness에만 두거나 별도 지표로 빼는 편이 안전합니다.

5) 원인 4: ConfigMap/Secret/볼륨 마운트 문제로 앱이 종료

볼륨 자체가 붙지 않으면 ContainerCreating에서 멈추는 경우가 많지만, 마운트는 됐는데 앱이 기대한 파일이 없거나 권한이 없어서 종료하면 CrashLoopBackOff가 됩니다.

증상

  • 로그에 permission denied, no such file or directory, failed to load config
  • runAsUser, fsGroup 미설정으로 권한 문제

10분 내 확인 포인트

# 볼륨/마운트/보안 컨텍스트 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o yaml | sed -n '1,220p'
  • securityContext.runAsNonRoot: true 인데 이미지가 root 전제면 실패 가능
  • readOnlyRootFilesystem: true 인데 앱이 /tmp나 작업 디렉터리에 쓰면 즉시 종료

즉시 조치

  • 쓰기 경로를 emptyDir로 분리하고 해당 경로만 writable로 제공
  • fsGroup 설정으로 볼륨 권한 정리

6) 원인 5: 노드 디스크/inode 고갈로 런타임이 비정상 종료

노드의 디스크가 가득 차거나 inode가 고갈되면, 이미지 레이어/로그/임시 파일 쓰기에서 실패하며 앱이 죽거나 런타임이 불안정해질 수 있습니다.

증상

  • 로그에 no space left on device
  • 특정 노드에 스케줄된 Pod만 반복 크래시

10분 내 확인 포인트

# Pod가 올라간 노드 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o wide

# 해당 노드 이벤트/상태 확인
kubectl describe node <NODE_NAME> | sed -n '/Conditions:/,/Addresses:/p'

노드 디스크/inode 진단은 리눅스 관점에서 접근해야 빨리 풀립니다.

즉시 조치

  • 단기: 문제 노드에서 Pod를 다른 노드로 옮기기(노드 cordon/drain)
  • 중기: 로그/이미지 GC, ephemeral storage requests/limits 설정, 노드 용량 증설

7) 원인 6: 외부 API 타임아웃이 부팅을 막아 CrashLoop로

앱이 시작 단계에서 외부 API 호출을 동기적으로 수행하고, 타임아웃 처리/재시도가 부적절하면 “부팅 실패”로 이어져 CrashLoop가 됩니다.

증상

  • 로그에 ReadTimeout, ConnectTimeout, DEADLINE_EXCEEDED
  • readiness가 뜨기 전에 프로세스가 종료

10분 내 확인 포인트

  • 앱이 “시작 시점”에 어떤 외부 의존성을 호출하는지 확인
  • 네트워크/DNS가 정상이어도, 재시도 정책이 공격적으로 짜여 있으면 기동 시간이 제한을 초과할 수 있습니다.

재시도 설계 자체가 원인인 경우 아래 글의 패턴이 유용합니다.

즉시 조치

  • 시작 단계의 외부 호출을 비동기화하거나, 실패해도 프로세스가 죽지 않게 degrade 모드 도입
  • startupProbe로 초기 구간 보호 + 재시도 상한/지터 적용

8) 원인 7: “정상 종료”인데도 재시작되는 경우 (프로세스 모델 문제)

컨테이너는 PID 1 프로세스가 살아있어야 합니다. 배치 작업처럼 실행 후 정상 종료(Exit 0)하는 이미지가 Deployment로 돌면, 쿠버네티스는 계속 새로 띄우며 결과적으로 CrashLoop처럼 보일 수 있습니다.

증상

  • 종료 코드가 0인데 재시작
  • 로그는 정상 완료 메시지

10분 내 확인 포인트

  • 워크로드 타입이 맞는지 확인: Job/CronJob로 돌려야 하는데 Deployment로 돌리고 있지 않은지
kubectl get deploy -n <NAMESPACE> <DEPLOY_NAME> -o jsonpath='{.spec.template.spec.restartPolicy}{"\n"}'
  • DeploymentrestartPolicy가 사실상 Always 전제입니다.

즉시 조치

  • 배치성이라면 Job으로 전환
  • 상주 프로세스가 필요하다면, 메인 프로세스가 종료되지 않도록 구조 변경

9) 마무리: 10분 루틴 체크리스트

아래 6개만 습관처럼 고정하면, CrashLoop의 대부분은 빠르게 결론이 납니다.

  1. kubectl get pod -o jsonpath로 컨테이너별 restartCountexitCode 확인
  2. kubectl describe pod에서 Events:를 끝까지 읽기
  3. kubectl logs --previous로 “죽기 직전 로그” 확보
  4. exitCode 137이면 메모리부터 본다(limits, top)
  5. Unhealthy면 probe 오탐을 의심하고 startupProbe 검토
  6. 특정 노드에만 재현되면 노드 디스크/inode/자원 압박을 의심

CrashLoopBackOff는 현상이 강렬해서 원인을 과대해석하기 쉬운데, 실제로는 종료 코드 + 이벤트 + 이전 로그 3종 세트로 대부분 정리됩니다. 이 루틴을 템플릿처럼 돌려서, “추측”이 아니라 “증거”로 복구 시간을 줄이는 쪽이 운영에서 가장 큰 효과를 냅니다.