Published on

K8s Pod CrashLoopBackOff 원인 7가지와 해결

Authors

서버가 죽었다가 다시 뜨는 현상은 익숙하지만, Kubernetes의 CrashLoopBackOff는 훨씬 더 빠르고(초 단위), 원인도 훨씬 다양합니다. 특히 애플리케이션 자체 오류뿐 아니라 프로브(liveness/readiness), 리소스 제한, 볼륨/권한, 네트워크/외부 의존성 등 “플랫폼 레이어” 이슈가 함께 얽혀서 진단이 어려워집니다.

이 글에서는 CrashLoopBackOff를 7가지 대표 원인으로 분류하고, 각 원인별로 어떤 증거를 먼저 봐야 하는지, 어떻게 고치는지, 재발 방지 체크리스트까지 정리합니다.

CrashLoopBackOff 빠른 이해: 무엇이 ‘BackOff’인가

  • 컨테이너 프로세스가 종료되면 kubelet이 재시작합니다.
  • 일정 시간 내 반복 종료가 발생하면 kubelet은 재시작 간격을 점점 늘립니다(BackOff).
  • 즉, CrashLoopBackOff는 “원인이 해결되지 않은 채 재시작만 반복”되는 상태입니다.

아래 커맨드로 기본 증거를 확보합니다.

# 상태/재시작 횟수 확인
kubectl get pod -n <ns> <pod> -o wide

# 이벤트(BackOff, OOMKilled, probe fail 등) 확인
kubectl describe pod -n <ns> <pod>

# 현재 컨테이너 로그
kubectl logs -n <ns> <pod> -c <container>

# 직전(이전) 크래시 로그: CrashLoopBackOff에서 특히 중요
kubectl logs -n <ns> <pod> -c <container> --previous

> 팁: describeEvents 섹션과 --previous 로그만으로도 절반은 결론이 납니다.

원인 1) 애플리케이션 프로세스 즉시 종료(Exit Code 1/2/…)

대표 증상

  • kubectl logs --previous에 스택트레이스/설정 파싱 오류/포트 바인딩 실패 등이 보임
  • describeLast State: Terminated + Exit Code: 1

진단 포인트

  • 애플리케이션이 “정상적으로 오래 떠 있는 프로세스”인지 확인
    • 예: 웹 서버는 foreground로 실행되어야 함
    • 잘못된 엔트리포인트로 스크립트가 실행 후 종료되는 경우 많음
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'

해결 방법

  • command/args 또는 Dockerfile ENTRYPOINT/CMD 수정
  • 설정 파일/환경변수 누락 수정
  • 포트 충돌/권한 문제 해결(예: 1024 미만 포트 바인딩)

예: 잘못된 command로 프로세스가 즉시 끝나는 케이스 수정

containers:
  - name: app
    image: myapp:1.0
    # 잘못된 예: init 스크립트만 실행하고 종료
    # command: ["/bin/sh", "-c", "./init.sh"]

    # 올바른 예: 서버 프로세스를 foreground로 실행
    command: ["/bin/sh", "-c", "./init.sh && exec ./server --port=8080"]

원인 2) livenessProbe 실패로 강제 재시작(‘정상인데 죽는’ 케이스)

CrashLoopBackOff의 고전적인 함정은 앱은 살아있지만 livenessProbe가 실패해서 kubelet이 죽이는 상황입니다.

대표 증상

  • describe 이벤트에 Liveness probe failed가 반복
  • 재시작 주기가 일정(예: 30초~1분)하게 반복
kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'

흔한 원인

  • 앱이 기동에 오래 걸리는데 초기부터 liveness가 때림
  • /healthz가 외부 의존성(DB 등)까지 포함해 “가끔” 실패
  • timeoutSeconds가 너무 짧음

해결 방법

  • 기동 시간이 길면 startupProbe를 도입
  • liveness는 “프로세스 생존”에 집중, readiness는 “트래픽 수신 가능”에 집중
containers:
  - name: app
    image: myapp:1.0
    ports:
      - containerPort: 8080

    startupProbe:
      httpGet:
        path: /healthz
        port: 8080
      failureThreshold: 30
      periodSeconds: 2

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

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

이 주제는 원인/패턴이 많아 별도 글로 정리해두었습니다: EKS Pod 1분마다 재시작? livenessProbe 실패 해결

원인 3) OOMKilled: 메모리 제한 초과로 커널이 종료

대표 증상

  • describeReason: OOMKilled
  • Exit Code: 137(SIGKILL)로 끝나는 경우가 많음
kubectl describe pod -n <ns> <pod> | grep -E 'OOMKilled|Exit Code|Reason' -n

진단 포인트

  • requests/limits가 실제 사용량 대비 너무 낮은지
  • JVM/Node/Python 등 런타임의 기본 메모리 설정이 컨테이너 제한과 불일치하는지

메트릭 서버가 있다면:

kubectl top pod -n <ns> <pod>

해결 방법

  • resources.limits.memory 상향 또는 메모리 누수 수정
  • JVM이라면 -XX:MaxRAMPercentage 등 컨테이너 인지 설정
  • Node.js라면 --max-old-space-size 조정
resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

> 팁: OOM은 “앱 버그”일 수도 있지만, 운영에서 더 흔한 건 limit를 너무 타이트하게 잡은 설정 문제입니다.

원인 4) Image/권한/파일시스템 문제: 읽기 전용, 실행 권한, 루트 권한 필요

CrashLoopBackOff는 이미지 자체는 잘 받아왔는데 컨테이너 시작 직후 권한 문제로 종료되는 형태로도 자주 나타납니다.

대표 증상

  • 로그에 permission denied, exec format error, no such file or directory
  • readOnlyRootFilesystem: true인데 앱이 /tmp/var에 쓰기 시도

진단 포인트

  • 보안 컨텍스트(runAsNonRoot, fsGroup, readOnlyRootFilesystem) 확인
  • 실행 파일 권한/라인엔딩/아키텍처(arm64 vs amd64) 확인

해결 방법

  • 쓰기 필요한 경로를 emptyDir로 마운트
  • securityContext에서 필요한 권한만 최소로 부여
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true

volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}

원인 5) ConfigMap/Secret/환경변수 누락 또는 잘못된 값

구성 오류는 “배포 직후 즉시 크래시” 패턴을 만듭니다.

대표 증상

  • 로그에 missing env, failed to load config, invalid yaml/json, connection string parse error
  • describeCreateContainerConfigError가 섞여 보이기도 함(완전 크래시는 아니지만 자주 동반)

진단 포인트

  • envFrom, valueFrom.secretKeyRef 키 이름 오타
  • Secret이 base64 인코딩은 맞지만 내용이 잘못됨(개행 포함 등)
kubectl get secret -n <ns> <secret> -o yaml
kubectl get configmap -n <ns> <cm> -o yaml

해결 방법

  • 필수 환경변수는 앱 시작 시점에 명확한 에러 메시지로 검증
  • Helm/Kustomize 템플릿에서 키 이름을 상수화/검증

예: 필수 키 누락을 빠르게 드러내는 패턴(애플리케이션 레벨)

# entrypoint.sh
set -euo pipefail
: "${DATABASE_URL:?DATABASE_URL is required}"
exec ./server

원인 6) 볼륨 마운트/스토리지 문제로 앱이 시작 실패(PVC/권한/경로)

엄밀히는 PVC가 Pending이면 Pod가 Running까지 못 가지만, 마운트는 되었는데 앱이 원하는 경로/권한/파일이 없어 즉시 종료하면 CrashLoopBackOff로 보입니다.

대표 증상

  • 로그에 No such file or directory(데이터 디렉터리), permission denied(볼륨 소유권)
  • initContainer 없이 앱이 “필수 디렉터리 생성”을 못 하고 종료

진단 포인트

  • 마운트 경로가 앱 설정과 일치하는지
  • EBS/EFS 등 CSI 드라이버 환경에서 소유권/퍼미션 이슈가 있는지

해결 방법

  • initContainer로 디렉터리/권한 준비
  • fsGroup 설정으로 볼륨 권한 정리
securityContext:
  fsGroup: 2000

initContainers:
  - name: init-perms
    image: busybox:1.36
    command: ["sh", "-c", "mkdir -p /data && chown -R 2000:2000 /data"]
    volumeMounts:
      - name: data
        mountPath: /data

containers:
  - name: app
    image: myapp:1.0
    volumeMounts:
      - name: data
        mountPath: /data

volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-pvc

PVC 자체가 Pending/프로비저닝 실패라면 아래 글에서 체크리스트를 참고하세요: Kubernetes PVC Pending - EBS CSI 동적 프로비저닝 실패 해결

원인 7) 노드 리소스/디스크 압박 및 Eviction 여파(간접적 CrashLoop)

노드가 DiskPressure/MemoryPressure 상태면 이미지 레이어/로그/emptyDir 등이 꼬이면서 앱이 비정상 종료하거나 Evicted→재스케줄→재시작 루프로 보일 수 있습니다.

대표 증상

  • 이벤트에 Evicted, node had condition: DiskPressure
  • 이미지 풀/컨테이너 생성이 불안정하게 실패
kubectl describe node <node> | grep -E 'DiskPressure|MemoryPressure|PIDPressure|Evict' -n
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -n 30

해결 방법

  • 노드 디스크 확장, 로그/이미지 정리, ephemeral storage requests/limits 설정
  • DaemonSet 로그 폭주/캐시 폭주 원인 제거

이 케이스는 운영에서 빈도가 높아 별도 가이드가 있습니다: EKS 노드 디스크 부족 Evicted 폭주 해결 가이드

실전 트러블슈팅 플로우(10분 컷)

아래 순서대로 보면 대부분의 CrashLoopBackOff는 빠르게 좁혀집니다.

  1. kubectl describe podEvents에서 probe failed, OOMKilled, Evicted 키워드 확인
  2. kubectl logs --previous로 “직전 죽기 전 로그” 확보
  3. 종료 코드 확인(1: 앱 에러, 137: OOM 가능성, 143: SIGTERM 등)
  4. 프로브가 있다면 일시적으로 완화/비활성화 후 정상 기동 여부 확인
  5. 리소스 제한/노드 압박 확인(kubectl top, node conditions)
  6. ConfigMap/Secret 키/값 검증
  7. 파일시스템/볼륨/권한(특히 readOnlyRootFilesystem, fsGroup) 확인

재발 방지 체크리스트

  • startupProbe로 “기동 구간”과 “운영 구간”을 분리
  • liveness는 최소 조건(프로세스 생존), readiness는 트래픽 수신 조건
  • --previous 로그가 남도록 로깅/종료 메시지 정리(구성 검증 에러를 친절히)
  • 메모리 limit는 런타임 특성(JVM/Node 등) 반영해 여유 있게
  • readOnlyRootFilesystem 사용 시 /tmp 등 쓰기 경로를 emptyDir로 제공
  • 스토리지는 initContainer로 권한/디렉터리 준비
  • 노드 디스크/로그/이미지 관리(특히 DaemonSet 로그 폭주 주의)

CrashLoopBackOff는 “원인”이 아니라 “현상”입니다. 중요한 건 이벤트(Events) + 이전 로그(--previous) + 종료 코드 3종 세트를 먼저 확보하고, 그 다음에 프로브/리소스/권한/스토리지/노드 상태로 분류해 들어가는 것입니다. 이 루틴만 팀 내 표준으로 만들어도, 장애 대응 시간이 눈에 띄게 줄어듭니다.