Published on

K8s CrashLoopBackOff 원인별 빠른 진단·복구

Authors

서버가 아니라 Pod가 계속 재시작될 때, Kubernetes는 친절하게 CrashLoopBackOff라는 상태를 보여줍니다. 하지만 이 상태 자체가 원인은 아닙니다. 컨테이너가 비정상 종료(Exit Code != 0) 하거나, 프로브 실패로 강제 종료되거나, 시작조차 못 한 채 실패하는 등 다양한 실패가 누적되어 “재시도(back-off)”에 들어간 결과입니다.

이 글은 “왜 죽는지”를 빠르게 분류하고, 가장 짧은 경로로 복구하는 방법을 원인별로 정리합니다. 운영에서 중요한 건 정답 풀이가 아니라 시간을 줄이는 진단 루틴입니다.

CrashLoopBackOff 한 줄 정의와 관찰 포인트

  • CrashLoopBackOff: 컨테이너가 반복적으로 종료되어 kubelet이 재시작 간격을 점점 늘리는 상태
  • 핵심 지표
    • Exit Code (예: 1, 137, 139)
    • Reason (Error, OOMKilled, Completed 등)
    • Events (ImagePullBackOff, Back-off restarting failed container, Unhealthy 등)
    • Restart count (재현성/빈도)

아래 3가지만 먼저 확보하면 대부분 5분 내로 방향이 잡힙니다.

# 1) 현재 상태/리스타트 확인
kubectl get pod -n <ns> <pod> -o wide

# 2) 이벤트/종료 사유 확인 (가장 중요)
kubectl describe pod -n <ns> <pod>

# 3) 직전 크래시 로그 확인 (CrashLoopBackOff에서는 --previous가 핵심)
kubectl logs -n <ns> <pod> -c <container> --previous

> 팁: kubectl logs가 비어있다면 “애플리케이션이 로그를 남기기 전에 죽었거나”, “엔트리포인트가 실행되지 못했거나”, “stdout/stderr로 로그를 안 내보내는 구조”일 수 있습니다. 이때는 describeLast State / Terminated / Exit Code가 더 중요합니다.

0분 컷: 원인 분류를 위한 결정 트리

아래 질문 순서대로 보면, 원인의 70~80%는 빠르게 분류됩니다.

  1. describeState: Waiting 인가?
    • ImagePullBackOff, ErrImagePull → 이미지/레지스트리
    • CreateContainerConfigError → env/secret/configmap/volume
    • CreateContainerError → 런타임/권한/마운트
  2. State: Terminated 인가?
    • Exit Code 137 / Reason OOMKilled → 메모리
    • Exit Code 139 → segfault(네이티브/런타임)
    • Exit Code 0인데 재시작? → 프로브/사이드카/정책
  3. 이벤트에 Unhealthy(Readiness/Liveness) 가 있는가?
    • 프로브 실패로 kill/restart
  4. 노드 이슈/스케줄링 이슈인가?
    • kubectl get eventsFailedMount, FailedScheduling, NodeNotReady

원인 1) 애플리케이션 즉시 종료(Exit Code 1 등)

가장 흔한 케이스입니다. 설정 누락, 잘못된 인자, DB 연결 실패 시 즉시 exit(1)로 떨어지며 CrashLoopBackOff로 이어집니다.

빠른 진단

kubectl logs -n <ns> <pod> -c <container> --previous --tail=200
kubectl describe pod -n <ns> <pod> | sed -n '/Last State:/,/Events:/p'
  • 로그에 missing env, cannot connect, permission denied, config parse error 같은 문구가 보이면 거의 확정
  • Last State: TerminatedFinished 시간이 촘촘하면 “시작 직후 죽음”

즉시 복구

  • 환경변수/시크릿/컨피그맵 누락이면 배포 스펙부터 수정
  • 임시 우회: 앱이 “필수 의존성”이 준비될 때까지 기다리게 만들기 (initContainer, startupProbe)

예: DB가 준비되기 전 앱이 죽어버리는 경우, initContainer로 대기

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      initContainers:
        - name: wait-db
          image: busybox:1.36
          command: ['sh', '-c', 'until nc -z db 5432; do echo waiting; sleep 2; done']
      containers:
        - name: api
          image: myrepo/api:1.0.0

원인 2) OOMKilled (Exit Code 137)

메모리 부족으로 커널이 프로세스를 강제 종료하면 Kubernetes는 OOMKilled로 표시하고 재시작합니다.

빠른 진단

kubectl describe pod -n <ns> <pod> | grep -E 'OOMKilled|Exit Code|Reason' -n

# metrics-server가 있다면
kubectl top pod -n <ns> <pod>
  • Reason: OOMKilled, Exit Code: 137이면 거의 확정
  • JVM/Node/Python도 메모리 스파이크가 가능

즉시 복구

  1. requests/limits 조정 (limit이 너무 낮으면 즉시 OOM)
resources:
  requests:
    cpu: "200m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"
  1. 애플리케이션 레벨
  • JVM: -Xms/-Xmx를 limit보다 여유 있게
  • Node: --max-old-space-size
  • 대용량 캐시/버퍼 사용 여부 확인
  1. 재발 방지
  • HPA/메모리 기반 스케일링
  • 메모리 프로파일링/누수 점검

원인 3) Liveness/Readiness/Startup Probe 실패

앱이 살아있는데도 프로브가 실패하면 kubelet이 컨테이너를 죽이고 재시작합니다. 특히 livenessProbe가 너무 공격적이면 정상 부팅 중인 앱도 계속 죽습니다.

빠른 진단

describe의 Events에 이런 패턴이 보입니다.

  • Unhealthy + Liveness probe failed
  • Back-off restarting failed container
kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'

즉시 복구

  • 부팅 시간이 긴 앱은 startupProbe를 도입하고, 그 전까지 liveness를 유예
  • timeoutSeconds, failureThreshold, periodSeconds를 현실적으로 조정
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 60
  periodSeconds: 2

livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5

> 운영 팁: “일단 살려야” 하면 livenessProbe를 잠시 제거하고 원인 분석 후 복구하는 것도 방법입니다(단, 장애 격리/자동복구는 약해짐).

원인 4) ImagePullBackOff / ErrImagePull (CrashLoopBackOff처럼 보이는 착시)

엄밀히는 CrashLoopBackOff가 아니라 이미지 풀 단계에서 막히는 경우가 많습니다. 하지만 현장에서는 “Pod가 안 뜬다”로 묶여 들어옵니다.

빠른 진단

kubectl describe pod -n <ns> <pod> | grep -E 'ErrImagePull|ImagePullBackOff|Failed to pull image' -n

즉시 복구

  • 이미지 태그 오타/미푸시
  • 레지스트리 인증(ImagePullSecret)
  • 프라이빗 네트워크에서 레지스트리 접근 불가
spec:
  imagePullSecrets:
    - name: regcred

원인 5) CreateContainerConfigError (Secret/ConfigMap/Env/Volume)

컨테이너가 시작되기 전, kubelet이 설정을 구성하다 실패하는 케이스입니다.

빠른 진단

kubectl describe pod -n <ns> <pod>
# Events에
# - configmap "xxx" not found
# - secret "yyy" not found
# - couldn't find key ...

즉시 복구

  • 참조하는 Secret/ConfigMap 이름/키 확인
  • 배포 순서(먼저 secret 생성 후 deploy)
kubectl get secret -n <ns>
kubectl get configmap -n <ns>

원인 6) 권한/보안 컨텍스트 문제(파일/포트 바인딩/PSA)

  • 1024 미만 포트 바인딩을 non-root로 시도
  • 볼륨 마운트 경로에 쓰기 권한 없음
  • Pod Security Admission(구 PSP) 정책에 의해 제한

빠른 진단

kubectl logs -n <ns> <pod> -c <container> --previous | tail -n 200
# permission denied, operation not permitted 등이 단서

즉시 복구

  • securityContext로 runAsUser/fsGroup 설정
  • 필요한 경우만 capability 추가(최소 권한)
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000

containers:
- name: web
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]

원인 7) 노드/네트워크/iptables 계열(특히 CNI/kube-proxy)

애플리케이션이 아니라 클러스터 네트워킹이 깨지면, 의존성 호출 실패로 앱이 죽거나(예: DB/DNS 접근 실패), 시스템 파드가 직접 CrashLoopBackOff에 빠질 수 있습니다.

  • DNS 실패: Temporary failure in name resolution
  • 서비스 라우팅 문제: iptables/conntrack

EKS에서 kube-proxy가 iptables 오류로 CrashLoopBackOff 나는 케이스는 별도 패턴과 처방이 있습니다. 아래 글을 함께 보면 원인 좁히는 시간이 크게 줄어듭니다.

또한 “외부 API 호출이 502/게이트웨이로 터지며 앱이 종료” 같은 패턴이라면, 네트워크/프록시/엔드포인트 계층을 분리해 확인해야 합니다.

원인 8) 디스크/파일시스템 이슈(로그 폭증, ephemeral storage)

노드 디스크가 가득 차거나, 컨테이너가 파일을 쓰지 못하면 앱이 비정상 종료할 수 있습니다.

빠른 진단

  • 이벤트에 evicted, DiskPressure, Failed to write
  • 노드에서 삭제된 파일이 열려 공간이 회수되지 않는 경우도 흔합니다(특히 로그).

이런 “디스크 100%인데 큰 파일이 안 보이는” 상황은 lsof로 추적하는 게 정석입니다.

즉시 복구

  • 로그 로테이션/사이즈 제한
  • ephemeral-storage requests/limits 설정
  • 노드 정리(불필요한 이미지/컨테이너)

실전: 10분 내 복구를 위한 커맨드 세트

1) 한 번에 정보 모으기

NS=<ns>
POD=<pod>

kubectl get pod -n $NS $POD -o wide
kubectl describe pod -n $NS $POD
kubectl logs -n $NS $POD --all-containers --previous --tail=200

2) 특정 컨테이너만 계속 죽는지 확인

kubectl get pod -n <ns> <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

3) 재현을 위해 임시로 컨테이너 진입(디버그)

CrashLoop이면 컨테이너가 바로 죽어 exec가 안 될 수 있습니다. 이때는 ephemeral debug container 또는 동일 이미지로 별도 Pod를 띄워 확인합니다.

# ephemeral debug container (클러스터/버전에 따라 지원)
kubectl debug -n <ns> -it <pod> --image=busybox:1.36 --target=<container>

# 또는 동일 이미지로 임시 실행
kubectl run -n <ns> tmp-shell --rm -it --image=myrepo/api:1.0.0 --command -- sh

재발 방지 체크리스트(운영 관점)

  • 프로브 설계: liveness는 “진짜로 죽었을 때만” 실패하도록, startupProbe로 부팅 보호
  • 리소스 계약: requests/limits를 근거 기반으로 설정하고 OOM 이벤트를 알림화
  • 의존성 준비: initContainer/재시도 로직/서킷브레이커로 일시 장애에 죽지 않게
  • 배포 안전장치: maxUnavailable, maxSurge, PDB로 롤아웃 중 전체 다운 방지
  • 관측성: 종료 코드/이벤트를 로그로 남기고, 재시작 횟수 급증을 경보로

마무리: CrashLoopBackOff는 “증상”, 답은 describe에 있다

CrashLoopBackOff 대응에서 가장 흔한 실수는 로그만 보다가 시간을 쓰는 것입니다. 실제로는 kubectl describe podEvents + Last State(Exit Code/Reason) 만으로도 원인의 절반 이상이 갈립니다.

  1. describe로 분류 → 2) --previous 로그로 확정 → 3) 프로브/리소스/설정/권한/네트워크 중 해당 축을 즉시 수정

이 루틴을 팀 런북으로 고정해두면, “원인 찾기”가 아니라 “복구 시간”이 줄어듭니다.