Published on

K8s CrashLoopBackOff 원인 12가지 10분 진단

Authors

CrashLoopBackOff는 쿠버네티스가 컨테이너를 띄웠지만 프로세스가 비정상 종료되어 재시작을 반복하는 상태입니다. 중요한 점은 CrashLoopBackOff 자체가 “원인”이 아니라 “증상”이라는 것입니다. 따라서 kubectl describe와 로그, 종료 코드, 프로브 이벤트를 짧은 시간에 훑어 범위를 빠르게 좁히는 루틴이 핵심입니다.

이 글은 운영 중인 워크로드에서 10분 내로 원인을 진단하는 순서와, 실무에서 가장 흔한 원인 12가지를 체크리스트처럼 제공합니다.

10분 진단 루틴(가장 먼저 할 것)

1) 어떤 컨테이너가 죽는지부터 확인

Pod에 사이드카가 있으면 “어느 컨테이너가” CrashLoop인지부터 확정해야 합니다.

kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>

kubectl describe의 핵심 포인트:

  • State: WaitingReason: CrashLoopBackOff
  • 직전 상태 Last State: TerminatedExit Code, Reason, Started, Finished
  • Events 섹션의 경고(특히 Back-off restarting failed container, Unhealthy, Killing)

2) “이전 로그”를 본다

CrashLoop에서는 현재 컨테이너가 이미 재시작되어 초기 로그가 사라졌을 수 있습니다. 반드시 --previous를 먼저 시도합니다.

kubectl logs -n <ns> <pod> -c <container> --previous
kubectl logs -n <ns> <pod> -c <container> --tail=200

3) 종료 코드로 1차 분류

Exit Code만으로도 상당수는 갈라집니다.

  • 1: 앱 예외, 설정 누락, 커맨드 실패 등 일반 오류
  • 126: 실행 권한 문제
  • 127: 커맨드 없음(PATH 문제)
  • 137: OOMKilled 또는 강제 종료(SIGKILL)
  • 139: 세그폴트

4) 프로브가 죽이는지 확인

EventsUnhealthy가 있고 Liveness probe failed가 반복되면 “앱이 죽는 게 아니라 kubelet이 죽이는 상황”일 수 있습니다.

kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}{"\n"}'

5) 최근 변경사항(이미지/시크릿/ConfigMap) 확인

CrashLoop은 배포 직후 급증하는 경우가 많습니다.

kubectl rollout history deploy -n <ns> <deploy>
kubectl rollout status deploy -n <ns> <deploy>

원인 12가지와 해결 포인트

1) 애플리케이션이 즉시 종료됨(포그라운드 프로세스 없음)

컨테이너는 PID 1이 살아있어야 합니다. 예를 들어 서버를 백그라운드로 띄우고 스크립트가 끝나면 컨테이너가 종료됩니다.

증상:

  • 로그가 거의 없고 즉시 종료
  • Exit Code: 0인데도 재시작(워크로드가 restartPolicy: Always)

해결:

  • 메인 프로세스를 포그라운드로 실행
  • CMD 또는 ENTRYPOINT가 서버 프로세스를 직접 실행하도록 수정
# 나쁜 예: 백그라운드 실행
CMD ["/bin/sh", "-c", "my-server &"]

# 좋은 예: 포그라운드 실행
CMD ["my-server", "--port=8080"]

2) 잘못된 command/args로 엔트리포인트 깨짐

이미지의 기본 엔트리포인트를 오버라이드하면서 실행이 실패하는 케이스입니다.

증상:

  • Exit Code: 127 또는 exec: ...: not found

해결:

  • command/args를 제거하고 이미지 기본값으로 확인
  • 쉘을 쓸 때는 sh -c 형태로 정확히
containers:
  - name: app
    image: myorg/app:1.2.3
    command: ["/bin/sh", "-c"]
    args: ["node server.js"]

3) 이미지에 실행 파일이 없거나 PATH 문제

빌드 스테이지 분리 시 바이너리가 최종 이미지에 복사되지 않았거나, 경로가 다를 수 있습니다.

진단:

  • 로그에 not found
  • Exit Code: 127

해결:

  • 멀티스테이지 COPY --from 경로 확인
  • 실행 파일 위치를 절대 경로로 지정
COPY --from=builder /app/bin/server /usr/local/bin/server
ENTRYPOINT ["/usr/local/bin/server"]

4) 실행 권한/파일 권한 문제

특히 알파인 기반 이미지에서 스크립트 권한이 빠져 permission denied가 자주 납니다.

증상:

  • Exit Code: 126
  • 로그에 permission denied

해결:

RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

쿠버네티스 보안 컨텍스트로 인해 쓰기 권한이 막히는 경우도 있으니 runAsUser, readOnlyRootFilesystem도 함께 확인합니다.

5) ConfigMap/Secret 누락 또는 키 이름 오타

환경변수 주입이나 볼륨 마운트가 기대한 키를 못 찾으면 앱이 즉시 종료할 수 있습니다.

진단:

  • 앱 로그에 missing env 또는 설정 파싱 에러
  • kubectl describe에서 볼륨 마운트는 성공했는데 앱이 설정 파일을 못 찾음

해결:

kubectl get cm -n <ns>
kubectl get secret -n <ns>

kubectl get cm -n <ns> <cm> -o yaml
kubectl get secret -n <ns> <secret> -o yaml

키 이름이 틀린 경우가 많으므로, 매니페스트의 key:와 실제 데이터 키를 대조합니다.

6) 의존 서비스(DB/Redis/외부 API) 연결 실패로 즉시 종료

앱이 부팅 시 DB 마이그레이션을 강제하거나, 연결 실패 시 즉시 exit(1) 하는 경우 CrashLoop로 보입니다.

진단:

  • 로그에 connection refused, timeout, no route to host

해결:

  • 부팅 시 의존성 체크를 리트라이로 변경
  • readiness와 liveness를 분리
  • 네트워크 정책, 서비스 DNS, 엔드포인트 확인
kubectl get svc -n <ns>
kubectl get endpoints -n <ns> <svc>

7) OOMKilled(메모리 한도 초과)로 재시작

가장 흔한 원인 중 하나입니다.

진단:

  • kubectl describe podLast StateReason: OOMKilled
  • Exit Code: 137

해결:

  • resources.limits.memory 상향 또는 누수/캐시 정책 점검
  • JVM/Node/Python 등 런타임 힙 상한을 limit에 맞게 설정
resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "512Mi"

8) Liveness probe가 너무 공격적이라 kubelet이 계속 죽임

앱은 살아있지만, liveness가 초기화 시간을 못 기다리고 실패하여 반복 재시작하는 케이스입니다.

진단:

  • EventsLiveness probe failed 반복
  • 앱 로그는 정상 부팅 중인데 갑자기 종료

해결:

  • startupProbe를 추가해 초기 구간 보호
  • initialDelaySeconds, timeoutSeconds, failureThreshold 조정
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2

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

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

9) Readiness 실패를 Liveness로 착각(프로브 설계 오류)

readiness는 “트래픽을 받을 준비”이고 liveness는 “프로세스가 살아있는지”입니다. DB 연결 불가를 liveness에 넣으면 DB 장애가 곧 재시작 폭풍으로 이어집니다.

해결:

  • DB 의존성은 readiness로 이동
  • liveness는 프로세스/이벤트 루프/스레드 데드락 같은 치명 상태만 감지

10) 이미지 아키텍처 불일치(Exec format error)

ARM 노드에 AMD64 이미지를 올리거나 그 반대인 경우 즉시 크래시합니다.

진단:

  • 로그에 exec format error

해결:

  • 멀티 아키텍처 이미지 빌드(linux/amd64, linux/arm64)
  • 노드 셀렉터/taint로 아키텍처 고정
docker buildx build --platform linux/amd64,linux/arm64 -t myorg/app:1.2.3 --push .

11) 볼륨 마운트/파일 시스템 이슈(읽기 전용 루트 등)

로그/캐시/소켓 파일을 루트에 쓰려다 실패하면 앱이 종료할 수 있습니다.

진단:

  • 로그에 Read-only file system, permission denied

해결:

  • 쓰기 경로를 emptyDir로 제공
  • 앱의 temp 경로를 /tmp 또는 마운트 지점으로 변경
volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}

12) 노드/런타임 레벨 이슈(드물지만 치명적)

컨테이너 런타임 문제, CNI 불안정, 노드 디스크 부족 등으로 프로세스가 비정상 종료하거나 kubelet이 컨테이너를 반복 정리하는 경우가 있습니다.

진단:

  • 특정 노드에서만 반복 발생
  • kubectl get pod -o wide로 노드 편중 확인

해결:

kubectl describe node <node>
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50

필요하면 해당 노드에서만 워크로드를 배제(cordon, drain)하고 원인(CRI 로그, 디스크/메모리 압박)을 확인합니다.

현장용 “10분 체크리스트” 요약

  • kubectl describe pod에서 Exit CodeEvents를 먼저 본다
  • kubectl logs --previous로 직전 크래시 로그를 확보한다
  • Exit Code 137이면 OOMKilled부터 의심한다
  • Unhealthy 이벤트가 있으면 프로브(특히 liveness) 설정을 의심한다
  • 배포 직후면 command/args, ConfigMap/Secret, 이미지 태그/아키텍처를 우선 점검한다

재발 방지 팁(운영 관점)

  • startupProbe로 “초기 부팅” 구간을 보호하고, readiness와 liveness의 역할을 분리합니다.
  • 앱이 의존 서비스 장애에서 즉시 종료하지 않도록 지수 백오프 재시도와 서킷 브레이커를 고려합니다.
  • 배포 파이프라인에서 이미지 아키텍처와 엔트리포인트 검증을 자동화하면 CrashLoop의 상당수를 사전에 제거할 수 있습니다.

GitOps로 운영 중이라면 배포 상태/헬스체크 흐름 자체를 점검하는 것도 도움이 됩니다. 드리프트나 헬스체크 실패가 겹치면 원인 파악이 더 어려워지므로, 아래 글의 단계별 점검법을 함께 참고하세요.

부록: 자주 쓰는 커맨드 모음

# 1) 문제 Pod 빠르게 찾기
kubectl get pod -A | grep -E "CrashLoopBackOff|Error"

# 2) 특정 Pod 상태/이벤트 집중 확인
kubectl describe pod -n <ns> <pod>

# 3) 직전 크래시 로그
kubectl logs -n <ns> <pod> -c <container> --previous --tail=300

# 4) 현재 로그
kubectl logs -n <ns> <pod> -c <container> --tail=300

# 5) 리소스 사용(메트릭 서버 필요)
kubectl top pod -n <ns>

# 6) 배포 롤아웃 확인
kubectl rollout status deploy -n <ns> <deploy>

CrashLoopBackOff는 원인이 다양하지만, 실제로는 Exit Code, Events, --previous 로그 3가지만 제대로 보면 10분 안에 절반 이상은 결론이 납니다. 위 12가지 중 어디에 해당하는지 먼저 분류하고, 그 다음에야 애플리케이션 코드/런타임/클러스터 레벨로 깊게 들어가는 것이 가장 빠른 해결 경로입니다.