Published on

Kubernetes CrashLoopBackOff 12가지 원인·해결

Authors

CrashLoopBackOff는 쿠버네티스가 컨테이너를 실행했지만 프로세스가 종료되어 재시작을 반복할 때 표시되는 상태입니다. 중요한 점은 CrashLoopBackOff 자체가 “원인”이 아니라, 반복 크래시의 결과라는 것입니다. 따라서 진단은 왜 프로세스가 죽는가왜 재시작 정책에 의해 계속 살아나려 하는가를 분리해서 접근해야 합니다.

아래는 운영에서 가장 흔한 12가지 원인과, 각각을 빠르게 확인·해결하는 방법입니다.

먼저 3분 진단 루틴 (로그·이벤트·종료코드)

CrashLoopBackOff를 보면 즉시 아래 3가지를 확보합니다.

  1. 파드 이벤트: 이미지 풀, 프로브 실패, OOMKilled, 마운트 오류 등 힌트가 가장 먼저 나옵니다.
kubectl -n <namespace> describe pod <pod-name>
  1. 이전 컨테이너 로그: 재시작 루프에서는 --previous가 핵심입니다.
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
  1. 종료코드와 종료 사유: Last StateExit Code를 봅니다.
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason} {.status.containerStatuses[0].lastState.terminated.exitCode}'
  • OOMKilled 137이면 메모리 문제일 확률이 높습니다.
  • Error 1이면 애플리케이션 설정/의존성/권한 이슈가 많습니다.
  • Completed 0인데도 재시작이면 Job 성격을 Deployment로 돌리는 등 워크로드 타입이 안 맞을 수 있습니다.

1) 애플리케이션 설정 오류 (ENV, 플래그, 설정 파일 누락)

가장 흔한 케이스입니다. 예를 들어 필수 환경변수 미설정, 잘못된 플래그, 설정 파일 경로 오타 등으로 프로세스가 즉시 종료합니다.

확인 포인트

  • --previous 로그에 missing env, config not found, invalid flag 같은 메시지
  • kubectl describe에 특별한 인프라 이벤트는 없고, 컨테이너만 빠르게 종료

해결

  • 필수 ENV를 명시적으로 검증하고, 누락 시 친절한 로그와 함께 종료 코드 1을 반환하도록 개선
  • ConfigMap/Secret 키 이름과 마운트 경로 재점검
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: app-secret
        key: databaseUrl

2) 이미지/엔트리포인트 문제 (CMD, ENTRYPOINT, 쉘 스크립트)

컨테이너가 뜨자마자 exec format error, no such file or directory로 죽는 경우가 많습니다. 특히 멀티아키텍처(arm64, amd64) 혼용, ENTRYPOINT 스크립트의 CRLF, 실행 권한 누락이 자주 발생합니다.

확인 포인트

  • 로그에 exec /app/start.sh: no such file or directory
  • 이벤트에 Back-off restarting failed container

해결

  • 스크립트는 LF로 저장, 실행 권한 부여
  • 이미지 아키텍처 확인
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh
ENTRYPOINT ["/app/start.sh"]

3) 프로브(liveness/readiness/startup) 오설정

프로브는 “헬스체크 실패 시 컨테이너를 죽일 수 있는 장치”입니다. liveness가 너무 공격적이면 실제로는 정상 부팅 중인데도 계속 죽여 CrashLoopBackOff를 유발합니다.

확인 포인트

  • kubectl describe pod 이벤트에 Liveness probe failed 또는 Startup probe failed
  • 애플리케이션 로그는 정상 부팅 중인데 일정 시간 내 응답이 없어서 kill

해결

  • 초기 부팅이 긴 앱은 startupProbe를 사용
  • initialDelaySeconds, timeoutSeconds, failureThreshold 조정
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

4) OOMKilled (메모리 부족) 또는 메모리 제한 과소

노드 메모리가 충분해 보여도, 컨테이너 resources.limits.memory가 낮으면 커널 OOM에 의해 종료됩니다. 종료코드 137이 대표적입니다.

확인 포인트

  • kubectl describe podOOMKilled
  • 메모리 사용량이 피크에서 튐

해결

  • requests/limits 재산정
  • 메모리 누수 의심 시 heap dump, GC 로그, 프로파일링
resources:
  requests:
    memory: "256Mi"
    cpu: "200m"
  limits:
    memory: "512Mi"
    cpu: "500m"

5) Crash on startup: 의존 서비스 미기동 (DB, Redis, 외부 API)

애플리케이션이 시작 시점에 DB 연결 실패를 치명 오류로 처리하면, DB 장애나 네트워크 정책 변화로 즉시 종료하며 루프에 빠집니다.

확인 포인트

  • 로그에 connection refused, timeout, host not found
  • 재시작 간격이 점점 늘어나는 백오프 패턴

해결

  • 시작 시 의존성 실패를 “재시도”로 처리하고, 준비 완료 전까지 readiness를 false로 유지
  • initContainer로 마이그레이션/체크를 분리
initContainers:
  - name: wait-db
    image: busybox
    command: ["sh", "-c", "until nc -z db 5432; do sleep 2; done"]

DNS가 간헐적으로 흔들리는 환경이라면 NodeLocal DNSCache도 고려할 수 있습니다. 관련해서는 EKS NodeLocal DNSCache로 DNS 간헐 실패 잡기도 함께 참고하면 진단 속도가 빨라집니다.

6) Secret/ConfigMap 마운트 및 키 불일치

Secret은 존재하지만 키가 다르거나, 볼륨 마운트 경로가 앱이 기대하는 위치와 다르면 시작 즉시 크래시합니다. 특히 subPath 사용 시 파일 갱신 이슈도 발생할 수 있습니다.

확인 포인트

  • 로그에 file not found, permission denied
  • kubectl describe podMountVolume.SetUp failed가 보이면 CrashLoop 이전에 Pending/ContainerCreating일 가능성도 있음

해결

  • Secret 키 이름, 템플릿, 마운트 경로를 일치
  • 외부 시크릿 동기화(ExternalSecret) 환경이면 IRSA/KMS/IAM 권한을 함께 점검

외부 시크릿이 아예 내려오지 않아 앱이 크래시하는 경우는 EKS ExternalSecret 미동작 - IRSA·KMS·권한 10분 진단 체크리스트가 바로 도움이 됩니다.

7) 볼륨 마운트 실패 후 앱이 즉시 종료 (PV, CSI, 권한)

스토리지가 붙지 않으면 원래는 컨테이너가 뜨지 못하는 경우가 많지만, 일부 구성에서는 빈 디렉터리로 시작하거나 앱이 특정 경로를 쓰다가 실패하며 크래시할 수도 있습니다.

확인 포인트

  • 이벤트에 CSI 관련 오류, 권한 오류, AZ 불일치
  • 앱 로그에 read-only file system, permission denied

해결

  • PV/PVC 상태, CSI 드라이버, 노드 IAM 권한, AZ 스케줄링 확인

스토리지 쪽은 원인 범위가 넓어서 별도 체크리스트가 유용합니다: K8s PV Pending·Mount 실패 - CSI·IAM·AZ 점검법

8) 파일 디스크립터 고갈 (EMFILE) 또는 커널 리소스 제한

트래픽이 조금만 올라가도 프로세스가 Too many open files로 죽으면 CrashLoopBackOff로 이어집니다. 노드 레벨 ulimit 또는 컨테이너 런타임/이미지 기본값이 낮을 수 있습니다.

확인 포인트

  • 로그에 EMFILE 또는 Too many open files
  • 소켓/파일 핸들 누수

해결

  • 애플리케이션에서 커넥션/파일 핸들 누수 제거
  • 워커 수, 커넥션 풀 조정
  • 필요 시 노드 및 런타임 설정 점검

리눅스 관점에서의 원인과 처방은 Linux EMFILE(Too many open files) 원인과 해결에 자세히 정리되어 있습니다.

9) 권한/보안 컨텍스트 문제 (runAsNonRoot, fsGroup, readonlyRootFilesystem)

보안 강화를 위해 runAsNonRoot를 켠 뒤, 앱이 루트 권한을 가정하고 / 하위에 쓰기를 시도하면 즉시 죽을 수 있습니다.

확인 포인트

  • 로그에 EACCES, permission denied
  • 특정 경로에 PID 파일, 소켓 파일 생성 실패

해결

  • 쓰기 경로를 emptyDir로 분리
  • securityContext와 파일 소유권 정리
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}

10) CPU 제한으로 인한 스타트업 타임아웃 (프로브 연쇄 실패)

CPU limits가 너무 낮으면 앱이 느리게 부팅하고, 그 결과 프로브가 실패해 컨테이너가 kill됩니다. 겉으로는 프로브 문제처럼 보이지만, 실제 원인은 리소스 제한일 때가 많습니다.

확인 포인트

  • 부팅 로그가 매우 느리게 진행
  • 프로브 실패 이벤트가 반복

해결

  • 최소 CPU 요청량을 올려 스케줄링 및 초기 부팅 안정화
  • startupProbe로 부팅 구간을 보호
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "1000m"

11) 노드 디스크 압박, 이미지/로그로 인한 Eviction 연쇄

엄밀히는 CrashLoopBackOff와 별개로 Evicted가 보이기도 하지만, 디스크 압박이 심하면 컨테이너가 비정상적으로 종료되거나 재시작이 잦아져 CrashLoop로 관측되기도 합니다. 특히 로그가 컨테이너 파일시스템에 쌓이거나, 이미지가 과도하게 커서 노드가 압박을 받는 경우가 흔합니다.

확인 포인트

  • 노드 상태에 DiskPressure
  • 파드 이벤트에 Eviction 관련 메시지

해결

  • 로그를 stdout으로 내보내고 수집기로 넘기기
  • 이미지 슬림화, 이미지 GC 정책 확인
  • 노드 디스크 증설 또는 노드풀 분리

EKS에서 실제로 자주 터지는 케이스라면 EKS 노드 디스크 부족 Evicted 폭주 해결 가이드를 같이 보면 재발 방지까지 설계하기 좋습니다.

12) “정상 종료”인데 재시작되는 워크로드 타입/정책 문제

컨테이너가 작업을 수행하고 exit 0으로 정상 종료하는데, Deployment로 띄워서 계속 재시작되는 경우가 있습니다. 이때 상태는 CrashLoopBackOff로 보일 수 있습니다.

확인 포인트

  • 종료코드가 0 또는 Completed
  • 로그가 “작업 완료”로 끝남

해결

  • 배치성 작업은 Job 또는 CronJob으로 전환
  • restartPolicy를 목적에 맞게 설정
apiVersion: batch/v1
kind: Job
metadata:
  name: one-shot-task
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: task
          image: myrepo/task:1.0.0
          command: ["sh", "-c", "python run.py"]

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

1) 종료코드와 원인 분류를 표준화

  • 137이면 메모리, 126/127이면 실행/명령 문제 가능성이 큼
  • 앱에서 치명 오류 시 로그에 “원인 키워드”를 남기고 종료하도록 규약화

2) 프로브는 “죽이는 장치”라는 전제로 설계

  • 부팅이 긴 앱은 startupProbe를 우선 도입
  • readiness는 트래픽 보호, liveness는 “정말 회복 불가능할 때만”

3) 의존성 실패는 크래시가 아니라 “대기”로

  • DB 장애가 앱 전체의 재시작 루프를 만들지 않게, 재시도 및 서킷브레이커 적용
  • 초기화 작업은 initContainer로 분리

4) 리소스는 관측 기반으로 조정

  • requests는 안정적 스케줄링, limits는 폭주 방지
  • 메모리 상한이 낮을수록 OOMKilled는 빨리 옵니다

현장에서 바로 쓰는 명령 모음

# 1) 파드 이벤트/상태 요약
kubectl -n <namespace> get pod <pod-name> -o wide
kubectl -n <namespace> describe pod <pod-name>

# 2) 직전 크래시 로그
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous

# 3) 종료 사유/코드
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .status.containerStatuses[*]}{.name} {.lastState.terminated.reason} {.lastState.terminated.exitCode}{"\n"}{end}'

# 4) 프로브 실패만 빠르게 필터
kubectl -n <namespace> describe pod <pod-name> | grep -n "probe" -n

마무리

CrashLoopBackOff는 “쿠버네티스가 문제를 숨기는 상태”가 아니라, 문제를 매우 빠르게 드러내는 신호입니다. 핵심은 이벤트로 인프라 단서를 먼저 확인하고, --previous 로그와 종료코드로 애플리케이션 원인을 좁힌 뒤, 프로브/리소스/의존성 처리 방식을 재설계해 재발을 막는 것입니다.

원인이 스토리지(PV/CSI), 외부 시크릿(IRSA/KMS), DNS 같은 플랫폼 계층으로 확장되는 순간부터는 체크리스트 기반으로 영역을 분리해 추적하는 것이 시간 절약에 가장 효과적입니다.