Published on

Kubernetes CrashLoopBackOff 원인별 로그·해결 9가지

Authors

CrashLoopBackOff는 쿠버네티스 에러라기보다 증상입니다. 핵심은 컨테이너가 Running으로 유지되지 못하고 종료되며, kubelet이 재시작을 반복하다가 백오프(backoff)로 재시작 간격을 늘리는 상태라는 점입니다.

이 글은 다음 순서로 문제를 빠르게 좁힙니다.

  • 먼저 어떤 로그를 어디서 봐야 하는지
  • 그 다음 대표 원인 9가지를 “증상 -> 확인 로그 -> 해결”로 정리

또한 CrashLoopBackOff를 Image pull 문제와 혼동하는 경우가 많습니다. 이미지 다운로드 단계에서 막히는 케이스는 아래 글이 더 직접적입니다.

CrashLoopBackOff에서 가장 먼저 보는 로그 3종

1) 이벤트: kubectl describe pod

CrashLoopBackOff의 1차 단서는 대부분 이벤트에 있습니다.

kubectl -n <ns> describe pod <pod>

아래 항목을 집중해서 봅니다.

  • Last State: TerminatedReason, Exit Code
  • EventsBack-off restarting failed container, Killing, Unhealthy

2) 현재 로그: kubectl logs

컨테이너가 짧게라도 실행되었다면 애플리케이션 로그가 남습니다.

kubectl -n <ns> logs <pod> -c <container>

3) 직전 크래시 로그: kubectl logs --previous

재시작이 이미 한 번 이상 발생했다면, 직전 실행의 로그가 핵심입니다.

kubectl -n <ns> logs <pod> -c <container> --previous

추가로, 프로세스가 OOMKill로 죽는지 확인하려면 종료 사유를 봅니다.

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

원인 1) 엔트리포인트·커맨드 오류로 즉시 종료

전형적 증상

  • 로그가 거의 없거나 exec: "...": executable file not found in $PATH
  • Exit Code: 127 또는 Exit Code: 1

확인 포인트

  • spec.containers[].command, args
  • 이미지 내부에 해당 바이너리/스크립트가 존재하는지
kubectl -n <ns> get pod <pod> -o yaml | sed -n '1,160p'

해결

  • command/args를 이미지의 실제 엔트리포인트와 일치시키기
  • 스크립트 실행 시 chmod +x, shebang(#!/bin/sh) 확인
  • distroless 이미지 사용 시 셸이 없을 수 있으니 sh -c 가정 금지

예시: 잘못된 커맨드를 올바르게 수정

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

원인 2) 애플리케이션 설정/환경변수 누락으로 부팅 실패

전형적 증상

  • 로그에 Missing env var, KeyError, NullPointerException 같은 초기화 실패
  • ConfigMap/Secret 참조 오류

확인 로그

  • kubectl logs --previous에서 애플리케이션이 시작 직후 예외로 종료
  • describe pod에서 CreateContainerConfigError까지는 아니고, 컨테이너가 실행되었다가 종료

해결

  • 필수 환경변수에 기본값 제공 또는 required 검증을 더 친절하게
  • ConfigMap/Secret 키 존재 여부 확인
kubectl -n <ns> get cm <cm-name> -o yaml
kubectl -n <ns> get secret <secret-name> -o yaml

예시: Secret 키를 env로 주입

env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: database_url

원인 3) 포트 바인딩 실패 또는 권한 문제

전형적 증상

  • listen tcp :80: bind: permission denied
  • EACCES 또는 Permission denied

왜 발생하나

  • 리눅스에서 1024 미만 포트는 일반 사용자로 바인딩 불가
  • 파일/디렉터리 권한(로그 디렉터리, 소켓 파일 등)

해결

  • 컨테이너 포트를 8080 등으로 올리고 Service에서 80으로 매핑
  • securityContext를 정리하고, 필요한 디렉터리에 쓰기 권한 부여

예시: 비루트 실행 + 안전한 권한

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true

원인 4) liveness/readiness probe 오설정으로 kubelet이 계속 죽임

전형적 증상

  • 애플리케이션은 실제로 뜨지만, kubelet이 Unhealthy로 판단해 Killing 반복
  • describe pod 이벤트에 Liveness probe failed가 연속으로 찍힘

확인 로그

kubectl -n <ns> describe pod <pod>

Events 예시(인라인 코드로만 표기): Liveness probe failed: HTTP probe failed with statuscode: 500

해결

  • 초기 기동이 느리면 startupProbe를 추가하거나 initialDelaySeconds를 늘리기
  • readiness와 liveness의 목적을 분리
    • readiness: 트래픽 받을 준비
    • liveness: 프로세스가 비정상 상태인지

예시: startupProbe로 부팅 구간 보호

startupProbe:
  httpGet:
    path: /health
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5

원인 5) OOMKilled: 메모리 제한으로 강제 종료

전형적 증상

  • Last State: TerminatedReason: OOMKilled
  • 애플리케이션 로그가 중간에 끊기거나 GC 로그 후 종료

확인 포인트

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

해결

  • resources.limits.memory를 현실적으로 올리거나, requests와 함께 재조정
  • JVM이라면 -Xmx가 cgroup 메모리보다 크지 않게
  • 메모리 누수/캐시 폭증을 의심

Spring/Java에서 OOM 분석이 필요하면 힙 덤프 기반 접근이 가장 빠릅니다.

예시: 리소스 가드레일 설정

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

원인 6) Crash on start: 외부 의존성(DB, Kafka, Redis) 연결 실패

전형적 증상

  • 로그에 Connection refused, timeout, UnknownHostException
  • 컨테이너가 재시작되며 백오프

확인 포인트

  • DNS 해석 실패인지, 네트워크 정책 차단인지, 인증 실패인지 분리
  • 동일 네임스페이스에서 디버그 파드로 확인
kubectl -n <ns> run net-debug --image=busybox:1.36 --restart=Never -it -- sh
# 내부에서
nslookup <service-name>
wget -qO- http://<service-name>:<port>/health

해결

  • 부팅 시 외부 의존성에 강하게 결합되어 있으면 재시작 루프가 심해집니다.
    • 재시도(backoff) 로직을 애플리케이션에 넣고, 일정 시간은 살아있게 유지
    • readiness로 트래픽만 차단하고 프로세스는 유지
  • Service/Endpoint 존재 여부 확인
kubectl -n <ns> get svc,endpoints

원인 7) 볼륨 마운트/파일 경로 문제로 초기화 실패

전형적 증상

  • No such file or directory, Read-only file system
  • 인증서/설정 파일을 특정 경로에서 읽는데 마운트가 안 됨

확인 로그

  • 애플리케이션 로그에서 파일 접근 예외
  • kubectl describe pod에서 볼륨 관련 이벤트도 함께 확인

해결

  • volumeMounts.mountPath와 애플리케이션이 참조하는 경로를 일치
  • subPath 사용 시 파일/디렉터리 타입 불일치 주의
  • readOnlyRootFilesystem: true 사용 시 쓰기 디렉터리는 emptyDir로 분리

예시: 쓰기 경로를 emptyDir로 제공

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

원인 8) SIGTERM 처리 미흡으로 롤링 업데이트 중 반복 크래시

전형적 증상

  • 배포/스케일링 시점에만 CrashLoopBackOff가 튀거나, 종료 직후 재기동이 꼬임
  • 종료 처리 중 예외 발생, 워커가 강제 종료되며 상태가 망가짐

확인 포인트

  • terminationGracePeriodSeconds가 너무 짧은지
  • 애플리케이션이 SIGTERM을 받아 정상 종료하는지

해결

  • 프레임워크별 graceful shutdown 활성화
  • preStop 훅으로 드레이닝 시간을 확보

예시: preStop으로 종료 전 대기

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 30

/bin/sh가 없는 이미지라면 위 커맨드는 실패합니다. 이 경우 앱 내 graceful shutdown에 집중하거나, 셸이 포함된 베이스 이미지로 바꾸는 편이 낫습니다.

원인 9) 노드 시간 불일치/인증서·서명 검증 실패로 즉시 종료

전형적 증상

  • TLS 오류: x509: certificate has expired or is not yet valid
  • AWS 서명 오류: RequestTimeTooSkewed
  • JWT 검증 실패: clock skew 관련 메시지

이 케이스는 애플리케이션이 “보안상 즉시 종료”하도록 구현되어 있으면 CrashLoopBackOff로 이어집니다.

확인 포인트

  • 로그에서 시간/서명/인증서 관련 키워드
  • 노드의 NTP 동기화 상태, 컨테이너의 시간대 설정

해결

  • 노드 시간 동기화(chrony, systemd-timesyncd) 점검

  • 클러스터에서 특정 노드로만 스케줄될 때 발생하면 해당 노드의 시간 드리프트 의심

  • AWS API 호출에서 RequestTimeTooSkewed가 난다면 아래 케이스를 함께 점검

  • EKS Pod→S3 업로드 403 RequestTimeTooSkewed 해결

재현과 분리: 디버깅을 빠르게 만드는 운영 팁

종료 코드로 1차 분류하기

  • Exit Code: 137은 OOMKill 가능성이 큼
  • Exit Code: 126은 실행 권한/실행 불가
  • Exit Code: 127은 커맨드/바이너리 없음
kubectl -n <ns> get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'

재시작 루프를 잠시 멈추고 내부를 보고 싶을 때

컨테이너가 너무 빨리 죽어 exec가 안 되면, 동일 이미지로 sleep 파드를 띄워 환경을 확인합니다.

kubectl -n <ns> run debug-sleep \
  --image=<image> \
  --restart=Never \
  --command -- sh -c "sleep 3600"

sh가 없는 이미지라면 sleep 바이너리가 있는지부터 확인이 필요합니다. distroless라면 별도 디버그 이미지(busybox 등)로 네트워크/볼륨만 점검하고, 앱 내부는 로그로 접근하는 전략이 안전합니다.

이벤트를 시간순으로 보기

kubectl -n <ns> get events --sort-by=.metadata.creationTimestamp

체크리스트: CrashLoopBackOff를 10분 안에 좁히는 순서

  1. describe pod에서 Reason, Exit Code, Events 확인
  2. logs --previous로 직전 크래시 로그 확보
  3. probe 실패 여부 확인(특히 Unhealthy, Killing)
  4. OOMKilled 여부 확인
  5. env/config/secret 누락 확인
  6. 외부 의존성 연결 실패인지 DNS부터 확인
  7. 볼륨 마운트 경로/권한 확인
  8. 엔트리포인트/커맨드와 이미지 타입(distroless 여부) 확인
  9. 시간 드리프트/TLS/JWT 서명 검증 오류 확인

CrashLoopBackOff는 대부분 “컨테이너가 왜 종료되었는가”로 환원됩니다. 종료 코드와 직전 로그(--previous)만 제대로 잡아도, 원인의 80%는 위 9가지 중 하나로 빠르게 분류됩니다.