Published on

Kubernetes CrashLoopBackOff 10가지 원인과 15분 진단

Authors

서버가 죽었다가 살아나는 것처럼 보이는데, 사실은 컨테이너 프로세스가 반복적으로 종료(exit) 되고 kubelet이 재시작을 걸면서 CrashLoopBackOff로 보이는 경우가 대부분입니다. 중요한 포인트는 “Kubernetes가 죽인 것인지(OOMKill, Probe 실패), 애플리케이션이 스스로 죽은 것인지(exit code), 또는 시작 자체가 안 되는지(ImagePull/권한/마운트)”를 이벤트와 로그로 빠르게 분리하는 것입니다.

이 글은

  • kubectl describe 이벤트에서 무엇을 먼저 봐야 하는지
  • kubectl logs --previous로 직전 크래시 원인을 어떻게 고정하는지
  • 프로브/리소스/볼륨/권한/엔트리포인트 등 CrashLoopBackOff 대표 원인 10가지를 어떻게 확인하는지

15분 진단 루틴으로 정리합니다.

CrashLoopBackOff의 의미(오해 2가지)

오해 1: “Pod가 죽는다”

대부분은 Pod가 아니라 컨테이너 프로세스가 종료됩니다. Pod는 살아있고 컨테이너만 재시작됩니다.

오해 2: “로그가 없어서 원인 파악이 불가능하다”

크래시 직전 로그는 보통 남습니다. 핵심은 현재 로그가 아니라 이전 컨테이너 로그를 보는 것입니다.

  • 현재 컨테이너: 재시작 이후의 로그(아무것도 없을 수 있음)
  • 이전 컨테이너: 크래시를 일으킨 그 프로세스의 로그

15분 진단 루틴(체크리스트)

아래 순서대로 하면 대부분 15분 내에 범주가 좁혀집니다.

1) 대상 Pod/컨테이너 재시작 상태 확인(1분)

kubectl get pod -n <ns> <pod> -o wide
kubectl get pod -n <ns> <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.state.waiting.reason}{"\n"}{end}'
  • restartCount가 빠르게 증가하면 프로브 실패/즉시 종료/OOM 가능성이 큽니다.
  • state.waiting.reasonCrashLoopBackOff라면, 바로 다음 단계로 갑니다.

2) 이벤트에서 “누가 왜 죽였는지” 먼저 본다(3분)

kubectl describe pod -n <ns> <pod>
# 이벤트만 빠르게
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -n 30

describe 하단 Events에서 우선순위:

  1. OOMKilled, Back-off restarting failed container
  2. Readiness probe failed, Liveness probe failed
  3. FailedMount, MountVolume, permission denied
  4. Error: crash 류(컨테이너 내부 종료)

여기서 이미 절반은 끝납니다. 예를 들어 FailedMount면 앱 로그를 보기 전에 PV/Secret/ConfigMap부터 봐야 합니다. PV 마운트 계열 이슈는 환경에 따라 자주 나오는데, EKS에서 파일시스템 타입/마운트 문제라면 EKS PV MountFailed - wrong fs type 해결법도 함께 참고하면 좋습니다.

3) “이전 컨테이너” 로그를 확보한다(3분)

# 단일 컨테이너
kubectl logs -n <ns> <pod> --previous

# 멀티 컨테이너
kubectl logs -n <ns> <pod> -c <container> --previous

# 마지막 200줄만
kubectl logs -n <ns> <pod> -c <container> --previous --tail=200
  • --previous가 핵심입니다. CrashLoopBackOff는 이미 재시작이 걸려서 현재 로그가 비어있을 수 있습니다.
  • 로그가 아예 없다면, 프로세스가 stdout/stderr로 아무것도 못 남기고 즉시 종료했거나, entrypoint 단계에서 죽었을 가능성이 큽니다.

4) 종료 코드/종료 이유를 컨테이너 상태에서 본다(3분)

kubectl get pod -n <ns> <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\n"}{"  lastState: "}{.lastState.terminated.reason}{" "}{.lastState.terminated.exitCode}{"\n"}{"  message: "}{.lastState.terminated.message}{"\n"}{end}'

자주 보는 패턴:

  • exitCode: 137 → OOMKill 또는 SIGKILL(메모리/노드 압박)
  • exitCode: 1 → 앱 내부 오류(설정/의존성/권한)
  • reason: Error + message에 exec format error, permission denied 등 → 이미지/엔트리포인트

5) 프로브/리소스/볼륨/권한을 빠르게 훑는다(5분)

kubectl get deploy -n <ns> <deploy> -o yaml | sed -n '1,200p'
# 또는 pod spec만
kubectl get pod -n <ns> <pod> -o yaml | sed -n '1,220p'

여기서 확인할 것:

  • livenessProbe, readinessProbe, startupProbe
  • resources.requests/limits (특히 memory)
  • volumeMounts/volumes (ConfigMap/Secret/PVC)
  • securityContext (runAsUser, fsGroup, readOnlyRootFilesystem)
  • command/args (entrypoint override)

CrashLoopBackOff 원인 10가지(증상·확인법·해결 힌트)

아래 10가지는 현장에서 가장 많이 만나는 케이스 위주입니다.

1) 애플리케이션이 즉시 종료(exit 1): 환경변수/설정 누락

증상

  • 이벤트는 단순 Back-off restarting failed container
  • --previous 로그에 Missing ENV, config not found, failed to connect 같은 메시지

확인

  • kubectl logs --previous에서 “필수 설정 누락” 문구
  • Deployment/Pod YAML의 env, envFrom, configMapKeyRef, secretKeyRef

해결 힌트

  • 필수 ENV를 명시하거나 기본값 처리
  • ConfigMap/Secret 키 이름 오타 방지
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: app-secret
        key: database_url

2) OOMKilled(메모리 부족)로 exit 137

증상

  • describe podOOMKilled
  • lastState.terminated.exitCode: 137

확인

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

해결 힌트

  • resources.limits.memory 상향 또는 메모리 누수/캐시 제한
  • JVM/Node/Python 런타임 힙 제한 확인
resources:
  requests:
    memory: "256Mi"
    cpu: "200m"
  limits:
    memory: "512Mi"
    cpu: "500m"

3) Liveness Probe 실패로 kubelet이 강제 재시작

증상

  • 이벤트에 Liveness probe failed 반복
  • 앱은 “살아있지만 느림/초기화 중”인데 계속 죽음

확인

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

해결 힌트

  • 초기 구동이 긴 앱은 startupProbe를 추가
  • timeoutSeconds, failureThreshold 완화
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2

4) Readiness Probe 실패를 Liveness와 혼동(트래픽/상태 꼬임)

증상

  • readiness는 실패하지만 liveness는 통과해야 정상인데, 둘을 같은 엔드포인트로 두고 “준비 안 됨”을 “죽음”으로 처리

확인

  • readiness/liveness가 동일 path이고, readiness가 DB 연결 등을 강하게 요구

해결 힌트

  • readiness는 “트래픽 받을 준비”, liveness는 “프로세스 생존”으로 분리

5) 엔트리포인트/커맨드 오버라이드 실수

증상

  • exec: "...": executable file not found in $PATH
  • permission denied

확인

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

해결 힌트

  • 이미지의 기본 ENTRYPOINT를 불필요하게 덮어쓰지 않기
  • 스크립트에 실행 권한(+ LF 라인엔딩) 확인
command: ["/bin/sh", "-c"]
args: ["./start.sh"]

6) ConfigMap/Secret/PVC 마운트 실패(FailedMount)

증상

  • 이벤트에 FailedMount, Unable to attach or mount volumes
  • 컨테이너가 시작도 못 하고 재시도

확인

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

해결 힌트

  • PVC bound 여부, StorageClass, 접근 모드 확인
  • Secret/ConfigMap 이름/키 존재 여부 확인

7) 파일/디렉터리 권한 문제(runAsUser, readOnlyRootFilesystem)

증상

  • 로그에 EACCES, permission denied, Read-only file system
  • 특정 디렉터리에 쓰기 시도하다 즉시 종료

확인

  • securityContextvolumeMounts 확인

해결 힌트

  • 쓰기가 필요한 경로를 emptyDir로 마운트
  • fsGroup 설정으로 볼륨 권한 부여
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}

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

증상

  • 로그에 connection refused, timeout, NXDOMAIN
  • 재시작 루프가 빠르게 발생

확인

  • 이전 로그에서 DNS/네트워크 오류
  • 같은 네임스페이스에서 임시 디버그 Pod로 네트워크 확인
kubectl run -n <ns> netdebug --rm -it --image=curlimages/curl -- sh
# inside
nslookup <service>
curl -v http://<service>:<port>/health

해결 힌트

  • “연결 실패 = 프로세스 종료” 대신 재시도(backoff) 로직 추가
  • Service/Endpoint 존재 확인

9) 노드 리소스 압박/축출(Evicted)과 혼동

증상

  • CrashLoopBackOff처럼 보이지만 사실 노드가 불안정하거나 압박 상태
  • 이벤트에 Evicted, The node was low on resource

확인

kubectl describe node <node> | sed -n '/Conditions:/,/Allocated resources:/p'
kubectl get pod -A --field-selector spec.nodeName=<node>

해결 힌트

  • 노드 증설/오토스케일, requests/limits 재조정
  • 로그/메트릭 기반으로 리소스 산정

10) 애드미션/웹훅/정책으로 인해 설정이 변형되거나 지연되어 부팅 실패

증상

  • 배포는 되는데 특정 필드가 주입되거나, 사이드카/정책 때문에 스타트업 타임이 늘어나 프로브에 걸림
  • 이벤트에 웹훅 타임아웃/거부 흔적

확인

  • kubectl get events에서 admission webhook 관련 메시지
  • 실제 생성된 Pod YAML과 원본 매니페스트 diff

해결 힌트

실전: 5개 커맨드로 원인 범주를 고정하는 템플릿

아래는 사고 대응 중 그대로 복붙해서 쓰는 “최소 커맨드 세트”입니다.

NS=<ns>
POD=<pod>

kubectl get pod -n $NS $POD -o wide
kubectl describe pod -n $NS $POD
kubectl logs -n $NS $POD --previous --tail=200
kubectl get pod -n $NS $POD -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
kubectl get pod -n $NS $POD -o yaml | sed -n '1,220p'
  • describe에서 이벤트로 “누가 죽였는지”를 먼저 확정
  • --previous로 “왜 죽었는지” 로그를 확보
  • exit code로 OOM/신호/앱 오류를 빠르게 분기

재발 방지 체크(배포 전 5분)

  • startupProbe 도입: 초기화가 긴 앱은 liveness에 맞아 죽기 쉽습니다.
  • 리소스 산정: 최소한 memory limit은 “피크 + 여유”로 잡고, OOM 발생 시 힙/캐시 상한을 같이 조정합니다.
  • 설정 검증: 필수 ENV/Secret 누락 시 즉시 종료하지 말고, 어떤 키가 없는지 명확히 로깅합니다.
  • 종료 시그널 처리: SIGTERM을 받아 graceful shutdown을 하도록 구현(롤링 업데이트 안정화).

마무리

CrashLoopBackOff는 원인 자체가 아니라 증상 라벨입니다. 진짜 원인은 대부분 이벤트(Probe/OOM/Mount)와 --previous 로그에 드러납니다. 위 15분 루틴대로 “이벤트 → 이전 로그 → exit code → 스펙(프로브/리소스/볼륨/권한)” 순서로 보면, 감으로 찍는 디버깅을 줄이고 재현 가능한 진단이 됩니다.

추가로, 클러스터/인그레스/네트워크 계층에서 장애가 연쇄로 보일 때는 원인이 CrashLoop이 아니라 상위 계층 타임아웃일 수도 있습니다. EKS에서 L7 타임아웃/502가 함께 보인다면 EKS ALB Ingress 502 target timeout 원인·해결처럼 인그레스 레벨도 같이 점검하세요.