- Published on
Kubernetes CrashLoopBackOff, 로그 없이 진단하는 법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff는 말 그대로 크래시와 재시작이 반복되는 상태입니다. 문제는 많은 경우 애플리케이션 로그가 남기기도 전에 프로세스가 종료되어 kubectl logs가 비어 보이거나, 재시작이 너무 빨라 로그를 놓치거나, 아예 컨테이너가 실행 단계에 진입하지 못해 로그가 생성되지 않는다는 점입니다.
이 글은 로그가 없다는 전제에서, Kubernetes가 남기는 다른 단서(이벤트, 종료 코드, 프로브 실패, 이미지 풀, 권한, 리소스, 런타임 오류)를 이용해 원인을 체계적으로 좁히는 방법을 다룹니다.
관련해서 OOMKilled나 프로브 문제를 빠르게 분류하는 글도 함께 보면 좋습니다: K8s CrashLoopBackOff - OOMKilled·Probe 5분 진단
1) 가장 먼저 할 일: "로그" 대신 "상태"를 본다
로그가 없을 때는 Pod의 상태가 곧 로그입니다. 핵심은 아래 3가지를 한 번에 확인하는 것입니다.
Last State의Reason과Exit Code- 이벤트(Event)에서 반복되는 실패 메시지
- 컨테이너가 실제로 실행까지 갔는지(
Waiting인지Terminated인지)
필수 커맨드 3종 세트
# 1) 현재 상태 요약
kubectl -n <namespace> get pod <pod-name> -o wide
# 2) 상태/이벤트/종료코드까지 한 번에
kubectl -n <namespace> describe pod <pod-name>
# 3) "이전 컨테이너" 로그(재시작 직전)
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous --tail=200
주의: 본문에서 <namespace> 같은 표기는 MDX에서 JSX로 오인될 수 있으므로, 위처럼 반드시 인라인 코드 또는 코드 블록 안에서만 사용해야 합니다.
describe에서 봐야 하는 포인트
kubectl describe pod 출력에서 다음 위치를 집중적으로 봅니다.
Containers:섹션State: Waiting이면Reason을 확인합니다. 예:ImagePullBackOff,CreateContainerConfigError,CrashLoopBackOffLast State: Terminated이면Reason과Exit Code를 확인합니다. 예:Error,OOMKilled,Completed
Events:섹션Back-off restarting failed containerLiveness probe failedError: failed to start containerd taskMountVolume.SetUp failed
여기서 이미 절반은 결론이 납니다. 로그가 없어도 Exit Code와 이벤트 메시지로 범주를 나눌 수 있기 때문입니다.
2) 로그가 "없는" 대표 원인 6가지와 분기 방법
2-1) 컨테이너가 실행 자체를 못 함: 이미지 풀/시크릿/권한
컨테이너가 시작도 못 하면 당연히 앱 로그가 없습니다. 이 경우 State: Waiting이고 Reason이 아래 중 하나로 나옵니다.
ImagePullBackOff/ErrImagePullCreateContainerConfigErrorCreateContainerError
예시 확인:
kubectl -n <namespace> describe pod <pod-name> | sed -n '1,200p'
Events에 Failed to pull image가 보이면 레지스트리 권한 문제일 가능성이 큽니다. GKE라면 Artifact Registry 권한이 흔한 원인입니다: GKE PullBackOff - Artifact Registry 403 해결
추가로 CreateContainerConfigError는 다음이 자주 원인입니다.
- 참조한
Secret또는ConfigMap이 없음 envFrom또는volumeMount대상이 없음- 잘못된
imagePullSecrets
빠른 점검:
kubectl -n <namespace> get secret
kubectl -n <namespace> get configmap
kubectl -n <namespace> get sa <serviceaccount>
2-2) 프로세스가 너무 빨리 죽음: 엔트리포인트/커맨드/환경변수
앱이 뜨자마자 종료되면 로그가 남을 틈이 없습니다(특히 stdout flush 전에 죽는 경우). 이때 Last State: Terminated, Reason: Error, Exit Code: 1 같은 형태가 흔합니다.
가장 흔한 실수는 command와 args 오버라이드입니다.
- 이미지의 기본
ENTRYPOINT를 의도치 않게 덮어씀 args만 바꾸려다command까지 지정해서 실행 파일이 바뀜
점검 방법:
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.containers[0].command} {.spec.containers[0].args}'
또 하나는 필수 환경변수 누락입니다. ConfigMap 키 오타, Secret key 오타가 있으면 앱이 즉시 종료할 수 있습니다. 이 경우에도 앱 로그가 없을 수 있으니, describe의 Environment: 섹션을 확인합니다.
2-3) OOMKilled인데 앱 로그가 없게 보이는 케이스
메모리 부족으로 커널이 프로세스를 강제 종료하면, 애플리케이션은 정상 종료 로직을 실행하지 못해 로그가 비어 보일 수 있습니다. describe에서 Last State: Terminated의 Reason: OOMKilled가 핵심 단서입니다.
kubectl -n <namespace> describe pod <pod-name> | grep -n "OOMKilled" -n
OOMKilled가 의심되면 다음을 바로 확인합니다.
- 컨테이너
resources.limits.memory - 실제 사용량(메트릭 서버 또는 Prometheus)
- 노드 메모리 압박 이벤트
cgroup v2 환경에서 메모리 진단은 추가 관찰 포인트가 있습니다: K8s OOMKilled 반복? cgroup v2 메모리 진단
2-4) Probe가 죽인다: Liveness/Startup/Readiness 오해
로그가 없는데 CrashLoopBackOff라면, 앱이 살아있는데도 livenessProbe가 지속 실패해서 kubelet이 죽이는 상황을 의심해야 합니다.
describe의 이벤트에 이런 문장이 반복됩니다.
Liveness probe failed: ...Killing container ... failed liveness probe
이때는 앱 자체 버그가 아니라 프로브 설정 문제일 수 있습니다.
대표 패턴:
- 앱 부팅이 느린데
startupProbe없이livenessProbe만 있음 - 초기화 동안
httpGet이503을 반환하는데 이를 실패로 간주 timeoutSeconds가 너무 짧아 간헐적 타임아웃
프로브 디버깅은 다음 순서가 안전합니다.
startupProbe를 도입하거나 부팅 구간을 충분히 커버livenessProbe는 "진짜 죽었을 때만" 실패하도록 조건을 단순화readinessProbe로 트래픽 차단을 담당
예시(YAML):
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 60
periodSeconds: 2
livenessProbe:
httpGet:
path: /live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
3) "이전 로그"도 없을 때: 컨테이너 종료 코드를 직접 해석
kubectl logs --previous조차 비어 있다면, 종료가 너무 빠르거나 stdout이 아예 생성되지 않은 상황입니다. 이때는 종료 코드가 가장 강력한 단서입니다.
종료 코드 확인
kubectl -n <namespace> get pod <pod-name> \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
kubectl -n <namespace> get pod <pod-name> \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
자주 보는 종료 코드 의미
137: 대개 OOMKilled 또는 강제 종료(SIGKILL)143:SIGTERM으로 종료(롤링 업데이트나 preStop, 혹은 kubelet 종료)1또는2: 앱/스크립트 에러(설정 누락, 인자 오류 등)
종료 코드가 137인데 Reason이 OOMKilled로 안 찍히는 경우도 있어, 이벤트와 노드 상태까지 같이 보는 게 안전합니다.
4) 노드/런타임 레벨 이슈: Pod는 말이 없고 노드가 말한다
앱 로그가 없고 Pod 이벤트도 애매하면, 노드에서 문제가 발생했을 가능성이 있습니다.
- 컨테이너 런타임(containerd) 오류
- CNI 네트워크 설정 문제
- 디스크 압박(
DiskPressure)으로 이미지 풀/쓰기 실패 - 노드 OOM, kubelet 재시작
노드 이벤트 확인
kubectl get node
kubectl describe node <node-name>
Conditions에서 MemoryPressure, DiskPressure, PIDPressure가 True인지 봅니다.
가능하다면 노드에서 시스템 로그도 확인합니다(관리형 환경이면 접근 방식이 다를 수 있음).
# 노드에서
sudo journalctl -u kubelet --since "1 hour ago"
sudo journalctl -u containerd --since "1 hour ago"
5) 로그 없이도 "컨테이너 안"에 들어가 진단하는 3가지 패턴
CrashLoopBackOff는 컨테이너가 계속 재시작되니 exec 타이밍 잡기가 어렵습니다. 이럴 때는 아래 패턴이 효과적입니다.
5-1) kubectl debug로 ephemeral container 붙이기
애플리케이션 컨테이너가 바로 죽더라도, 같은 Pod 네임스페이스에 디버그 컨테이너를 붙여 네트워크/볼륨/ DNS를 확인할 수 있습니다.
kubectl -n <namespace> debug -it pod/<pod-name> --image=busybox:1.36 --target=<container-name>
디버그 컨테이너에서 체크:
# DNS
nslookup kubernetes.default.svc.cluster.local
# 서비스 연결
wget -S -O- http://<service-name>:<port>/healthz
# 볼륨 마운트 확인
ls -al /path
5-2) 커맨드를 "sleep"로 바꿔서 일단 살려두기(임시)
원인 파악을 위해 컨테이너가 너무 빨리 죽는 것을 막아야 할 때가 있습니다. command를 임시로 sleep로 바꿔 컨테이너를 살려두고 파일/환경을 확인합니다.
containers:
- name: app
image: your-image:tag
command: ["sh", "-c", "sleep 3600"]
그 다음:
kubectl -n <namespace> exec -it <pod-name> -c app -- sh
env | sort
ls -al
원인 해결 후 반드시 원래 엔트리포인트로 되돌리세요.
5-3) terminationMessage로 "죽기 직전" 메시지 남기기
애플리케이션이 stderr로 에러를 찍더라도 로그 수집이 되기 전에 죽는 경우가 있습니다. Kubernetes는 컨테이너 종료 메시지를 별도로 보관할 수 있습니다.
containers:
- name: app
image: your-image:tag
terminationMessagePolicy: FallbackToLogsOnError
종료 메시지 확인:
kubectl -n <namespace> describe pod <pod-name> | sed -n '/Termination Message:/,/Conditions:/p'
6) 실전 체크리스트: 10분 안에 원인 범주를 확정하는 순서
아래 순서대로 보면 "로그가 없어도" 대부분 범주 확정이 가능합니다.
kubectl describe pod에서State와Last State확인Exit Code와Reason기록(특히OOMKilled,Error,Completed)Events에서 반복되는 메시지 확인(프로브 실패, 이미지 풀, 마운트 실패)kubectl logs --previous시도(없어도 다음 단계로)- 프로브가 원인인지 확인(
Liveness probe failed여부) - 리소스 제한 확인(
limits.memory,limits.cpu) 및 노드Pressure확인 ConfigMap/Secret존재 여부와 키 오타 확인command/args오버라이드 여부 확인- 필요 시
kubectl debug로 네트워크/DNS/볼륨 확인 - 재현되면
terminationMessagePolicy로 종료 메시지 확보
7) 자주 나오는 케이스별 "처방" 예시
케이스 A: Exit Code 137 또는 Reason OOMKilled
- 메모리 limit 상향 또는 앱 메모리 사용량 최적화
- JVM/Node/Python이라면 런타임 힙 상한을 limit에 맞춤
- HPA보다 먼저 VPA 또는 적절한 요청량(
requests) 설정 검토
메모리 누수/커널 OOM 로그 추적이 필요하면 리눅스 관점에서 함께 추적하는 것도 좋습니다: Linux OOM Killer 로그 추적과 메모리 누수 진단
케이스 B: Liveness probe failed 반복
startupProbe추가timeoutSeconds와failureThreshold완화livenessProbe엔드포인트를 의존성(DB 등)과 분리
케이스 C: CreateContainerConfigError
Secret/ConfigMap이름과 키 재확인volumeMount경로 충돌 여부 확인serviceAccount및imagePullSecrets연결 확인
마무리
CrashLoopBackOff를 "로그로만" 잡으려 하면, 로그가 없는 순간부터 손이 묶입니다. 하지만 Kubernetes는 이미 충분한 단서를 남깁니다. describe의 상태/이벤트/종료 코드를 중심으로 범주를 빠르게 확정하고, 필요하면 kubectl debug나 sleep 패턴으로 관찰 가능 상태를 만든 뒤 원인을 제거하는 흐름이 가장 재현성 높습니다.
다음 번에 CrashLoopBackOff를 만나면 kubectl logs가 비어 있어도 당황하지 말고, 먼저 describe와 Exit Code부터 꺼내 보세요.