Published on

K8s CrashLoopBackOff 원인별 진단·해결 체크리스트

Authors

서버리스/VM 장애와 달리 Kubernetes의 CrashLoopBackOff는 **"프로세스가 정상적으로 떠 있지 못한다"**는 신호를 가장 직접적으로 드러냅니다. 하지만 원인은 애플리케이션 버그부터 이미지/권한/리소스/프로브/스토리지까지 광범위합니다. 이 글은 현장에서 가장 많이 만나는 패턴을 원인별로 분류하고, kubectl 명령 중심으로 진단 → 증거 수집 → 해결 순서로 정리합니다.

참고로 “로그가 아예 안 나오는” 케이스는 접근법이 약간 다른데, 그 경우는 별도 글인 EKS Pod CrashLoopBackOff 로그 없을 때 7단계 진단도 함께 보면 좋습니다.

CrashLoopBackOff의 의미와 재시작 메커니즘

  • CrashLoopBackOff는 Kubernetes 이벤트/상태에서 보이는 백오프(Backoff) 재시작 상태입니다.
  • 컨테이너가 종료되면 kubelet이 재시작을 시도하고, 반복 실패 시 재시작 간격을 점점 늘립니다.
  • 핵심은 “왜 종료되는가”이며, 종료 원인은 크게 아래로 나뉩니다.
  1. 애플리케이션이 즉시 종료(예: 설정 누락으로 exit 1)
  2. OOMKilled 등 커널에 의해 강제 종료
  3. liveness/startup probe 실패로 kubelet이 kill
  4. 볼륨 마운트/권한/시크릿 등 런타임 의존성이 충족되지 않음
  5. 엔트리포인트/명령/아키텍처 불일치

0단계: “지금 무엇이 죽는지” 60초 안에 확인

아래 3가지는 거의 모든 케이스에서 첫 화면으로 봅니다.

1) Pod 상태와 재시작 횟수

kubectl get pod -n <ns> <pod> -o wide
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[*].restartCount}'
  • RESTARTS가 급격히 증가하면 CrashLoop 패턴입니다.
  • 노드 변경이 잦다면 스케줄링/노드 리소스 문제 가능성도 올라갑니다.

2) describe로 종료 이유/이벤트 확인

kubectl describe pod -n <ns> <pod>

여기서 특히 봐야 할 곳:

  • Containers:Last State: TerminatedReason, Exit Code, Finished
  • Events:Back-off restarting failed container, Killing container, Unhealthy

3) 이전(직전) 컨테이너 로그 확인

kubectl logs -n <ns> <pod> -c <container> --previous
  • CrashLoop일 때는 현재 컨테이너가 이미 죽어있어 로그가 비어 보일 수 있으므로 --previous가 중요합니다.

1) Exit Code로 원인 빠르게 분류하기

종료 코드/Reason은 원인 분류의 지름길입니다.

Exit Code 1/2/…: 앱이 스스로 종료

  • 설정 파일/환경변수 누락
  • 외부 의존성(DB, Redis, API) 연결 실패 시 즉시 종료
  • 마이그레이션 실패, 포트 바인딩 실패

진단 체크:

kubectl logs -n <ns> <pod> -c <container> --previous
kubectl exec -n <ns> -it <pod> -c <container> -- env | sort

해결 포인트:

  • ConfigMap/Secret 키 이름 오타, 마운트 경로 불일치
  • 앱이 의존성 실패 시 즉시 종료하지 말고 재시도/지수 백오프 적용
  • readiness로 트래픽 유입을 막고, liveness는 너무 공격적으로 두지 않기

Exit Code 137 / Reason=OOMKilled: 메모리 부족

  • Exit Code 137은 SIGKILL(대개 OOM) 가능성이 큽니다.
  • describe에서 Reason: OOMKilled면 거의 확정.

진단 체크:

kubectl describe pod -n <ns> <pod> | sed -n '/Last State:/,/Events:/p'
kubectl top pod -n <ns> <pod>

해결 포인트:

  • resources.limits.memory 상향 또는 메모리 사용량 감소
  • JVM/Node/Python 등 런타임 힙 설정을 limit에 맞추기
  • 캐시/버퍼가 큰 라이브러리(예: 대용량 모델 로딩, 이미지 처리)라면 초기 로딩 메모리도 고려

예시(YAML):

resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "512Mi"

Exit Code 139: Segmentation fault

  • 네이티브 라이브러리, glibc/musl 호환, 잘못된 바이너리, 메모리 접근 버그

진단 체크:

kubectl logs -n <ns> <pod> -c <container> --previous
kubectl describe pod -n <ns> <pod> | grep -E 'Exit Code|Reason|Message' -n

해결 포인트:

  • 이미지 베이스(alpine vs debian)와 네이티브 의존성 호환성 확인
  • CPU 아키텍처(amd64/arm64) 불일치 여부 확인

2) 프로브(liveness/readiness/startup) 설정이 과격한 경우

CrashLoopBackOff의 “범인”이 앱이 아니라 프로브인 경우가 생각보다 많습니다.

전형적 증상

  • 앱은 느리게 뜨는데 liveness가 먼저 실패 → kubelet이 kill → 무한 반복
  • 로그에는 큰 에러가 없고, EventsUnhealthy / Killing container가 반복

진단 체크:

kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.containers[0].livenessProbe}'

해결 전략(권장 패턴):

  • startupProbe로 “부팅 시간”을 별도로 보호
  • readiness는 트래픽 차단용, liveness는 “정말로 죽었을 때만”

예시:

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  failureThreshold: 3

팁:

  • 부팅이 느린 앱(마이그레이션, 모델 로딩, 캐시 워밍업)은 startupProbe 없이 liveness만 두면 거의 사고가 납니다.

3) 이미지/엔트리포인트/명령 문제

전형적 증상

  • exec format error (아키텍처 불일치)
  • no such file or directory (ENTRYPOINT 경로/권한/라인엔딩)
  • permission denied (실행 비트, runAsUser)

진단 체크:

kubectl logs -n <ns> <pod> -c <container> --previous
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.containers[0].image}'

해결 포인트:

  • 멀티 아키텍처 이미지라면 linux/amd64, linux/arm64 매니페스트 확인
  • 셸 스크립트가 CRLF로 들어가면 /bin/sh^M: bad interpreter 형태로 터질 수 있음
  • distroless 이미지에서 /bin/sh가 없는데 command: ["sh", "-c", ...]를 쓰는 실수

예시(명령 오버라이드):

containers:
  - name: app
    image: myrepo/app:1.2.3
    command: ["/app/server"]
    args: ["--port=8080"]

4) ConfigMap/Secret/환경변수/볼륨 마운트 문제

앱이 “필수 설정이 없으면 즉시 종료”하는 경우 CrashLoop이 됩니다.

전형적 증상

  • 로그에 missing env / cannot read config / file not found
  • describeMountVolume.SetUp failed 이벤트

진단 체크:

kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'
kubectl get cm -n <ns>
kubectl get secret -n <ns>

해결 포인트:

  • 키 이름/경로 오타, subPath 사용 시 파일 교체 이슈
  • Secret이 다른 네임스페이스에 있는지(기본적으로 불가)
  • CSI 드라이버/외부 시크릿 연동(예: External Secrets) 동기화 지연

예시(Secret env 주입):

env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: my-db
        key: password

5) 권한/보안 컨텍스트(runAsUser, fsGroup)로 인한 즉시 종료

컨테이너는 떠도, 앱이 파일/포트에 접근 못해 죽는 케이스입니다.

전형적 증상

  • EACCES: permission denied, Operation not permitted
  • 1024 미만 포트 바인딩 실패(비루트)

진단 체크:

kubectl logs -n <ns> <pod> -c <container> --previous
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.securityContext}'
kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.containers[0].securityContext}'

해결 포인트:

  • 쓰기 디렉터리에는 emptyDir를 마운트하고 권한 맞추기
  • PV 사용 시 fsGroup 설정으로 그룹 권한 부여

예시:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  fsGroup: 10001

6) 리소스/노드 이슈: OOM 말고도 CPU 스로틀링, 디스크 압박

OOMKilled가 아니어도 CPU 스로틀링으로 타임아웃이 나고, 그 결과 프로브 실패 → 재시작 루프로 이어질 수 있습니다. 또한 노드 디스크 압박(DiskPressure)은 이미지 풀/로그 쓰기 실패를 유발합니다.

진단 체크:

kubectl top pod -n <ns>
kubectl describe node <node>

해결 포인트:

  • requests를 너무 낮게 잡으면 CPU 부족으로 부팅이 길어져 startupProbe가 필요
  • 노드 디스크/이미지 GC 정책 점검

7) 의존성(DB/외부 API) 장애로 ‘즉시 종료’하는 설계

K8s에서 가장 흔한 안티패턴 중 하나가 “DB 연결 실패하면 프로세스 종료”입니다. 일시 장애에도 CrashLoop이 되어 오히려 복구를 더 늦춥니다.

권장 설계

  • 프로세스는 살아있되 readiness를 false로 만들어 트래픽만 차단
  • 재시도/서킷브레이커/타임아웃을 명시적으로 둠

예시(간단한 Node.js 의존성 재시도 의사코드):

import pRetry from 'p-retry';

async function connectDB() {
  return pRetry(async () => {
    // 실제 DB 연결
  }, { retries: 10, minTimeout: 500, maxTimeout: 5000 });
}

(async () => {
  await connectDB();
  // 서버 시작
})();

DB 타임아웃/커넥션 풀 고갈로 장애가 확대되는 패턴은 애플리케이션 레벨에서도 자주 보이므로, 원인 분석 시에는 앱 내부 리소스(풀, 타임아웃)도 함께 확인하는 게 좋습니다. (관련 주제: Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단)


8) 실전 “원인별 체크리스트” 요약

아래는 현장에서 빠르게 체크하는 순서입니다.

A. 상태/이벤트

  • kubectl describe pod에서 Last State/Exit Code/Reason
  • EventsUnhealthy, Killing, Back-off 패턴 확인

B. 로그

  • kubectl logs --previous 필수
  • 로그가 없다면 엔트리포인트/권한/프로브/즉시 크래시를 의심

C. 대표 원인 매핑

  • OOMKilled / 137 → 메모리 limit, 런타임 힙, 초기 로딩
  • Unhealthy 이벤트 반복 → startupProbe/liveness 튜닝
  • MountVolume 실패 → Secret/ConfigMap/PV/CSI
  • permission denied → securityContext, fsGroup, 쓰기 경로
  • exec format error → 아키텍처/이미지 빌드

9) 트러블슈팅을 “재현 가능”하게 만드는 팁

CrashLoopBackOff는 재시작이 너무 빨라서 관찰이 어렵습니다. 아래 방법으로 관찰 시간을 확보합니다.

1) 임시로 command를 sleep으로 바꿔 내부 확인

kubectl -n <ns> patch deploy <deploy> --type='json' \
  -p='[{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["sh","-c","sleep 3600"]}]'
  • distroless면 sh가 없으니 이미지에 맞게 조정해야 합니다.

2) Ephemeral container로 디버깅(가능한 클러스터에서)

kubectl debug -n <ns> -it <pod> --image=busybox --target=<container>
  • 네트워크/DNS/파일 마운트 상태를 빠르게 확인할 수 있습니다.

마무리: “죽는 이유”를 Exit Code와 Events로 고정하라

CrashLoopBackOff를 빠르게 끝내는 핵심은 추측이 아니라 **증거(Exit Code/Reason/Events/previous logs)**로 원인을 고정하는 것입니다. 특히 프로브 설정과 리소스 제한은 “정상 앱도 죽일 수 있는” 인프라 레벨 요인이므로, 앱 로그만 보다가 시간을 낭비하지 않도록 describe의 이벤트와 종료 사유를 항상 먼저 확인하세요.

추가로, 로그가 아예 남지 않는 까다로운 케이스는 EKS Pod CrashLoopBackOff 로그 없을 때 7단계 진단에서 더 깊게 다룹니다.