- Published on
Kubernetes CrashLoopBackOff 원인 8가지 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff는 에러 메시지처럼 보이지만, 실제로는 컨테이너가 반복적으로 종료되어 kubelet이 백오프(재시작 지연)를 걸고 있는 상태를 의미합니다. 즉, “왜 죽었는지”는 CrashLoopBackOff 자체가 말해주지 않습니다. 핵심은 마지막 종료 원인(Exit Code, OOMKilled, Signal) 과 이전 로그(previous logs), 그리고 이벤트(Event) 를 묶어서 보는 것입니다.
운영에서 시간을 아끼는 방법은 두 가지입니다.
- 먼저
kubectl로 증상을 정확히 분류하고 - 자주 나오는 원인 8가지를 체크리스트처럼 빠르게 배제하는 것
아래는 그 루틴을 그대로 정리한 글입니다.
0) 2분 안에 상태를 고정: 필수 커맨드 세트
CrashLoopBackOff를 보면 제일 먼저 아래 4개를 실행합니다. 이 4개만으로도 원인 후보가 절반 이상 줄어듭니다.
# 1) 현재 상태와 재시작 횟수
kubectl get pod -n <NAMESPACE> <POD_NAME> -o wide
# 2) 이벤트: 언제/왜 재시작이 일어났는지(스케줄링, 프로브, OOM 등)
kubectl describe pod -n <NAMESPACE> <POD_NAME>
# 3) 현재 로그
kubectl logs -n <NAMESPACE> <POD_NAME> -c <CONTAINER_NAME>
# 4) 직전(이전) 컨테이너 로그: CrashLoop에서 가장 중요
kubectl logs -n <NAMESPACE> <POD_NAME> -c <CONTAINER_NAME> --previous
추가로 “종료 코드”를 바로 확인하면 방향이 빨라집니다.
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
echo
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
echo
exitCode가137이면 OOMKill(또는 SIGKILL) 가능성이 높습니다.reason이OOMKilled로 찍히면 거의 확정입니다.
1) 원인 1: 애플리케이션이 즉시 종료(엔트리포인트/커맨드 문제)
가장 흔합니다. 컨테이너는 “프로세스가 살아있어야” 유지됩니다. CMD 또는 ENTRYPOINT가 잘못되었거나, 실행 파일 경로/권한이 틀리거나, 실행 직후 설정 오류로 프로세스가 종료되면 CrashLoop로 이어집니다.
확인 포인트
kubectl logs --previous에 애플리케이션의 즉시 종료 로그가 남음describe의Last State: Terminated에Exit Code: 1같은 일반 오류 코드- 이벤트에
Back-off restarting failed container반복
자주 나오는 케이스
- 이미지에 바이너리가 없는데 실행하려 함
command/args를 오버라이드하면서 기본 엔트리포인트가 깨짐- 쉘 스크립트가
set -e로 인해 중간 실패 시 즉시 종료
해결 예시
# deployment 일부 예시
containers:
- name: app
image: myrepo/myapp:1.2.3
command: ["/app/myapp"]
args: ["--config", "/etc/myapp/config.yaml"]
또는 실행 권한 문제라면 Dockerfile에서 권한을 보장합니다.
COPY myapp /app/myapp
RUN chmod +x /app/myapp
2) 원인 2: 설정/시크릿/환경변수 누락으로 부팅 실패
Kubernetes에서 “설정 누락”은 애플리케이션이 부팅 단계에서 죽는 대표 원인입니다. 특히 DB URL, API 키, JWT 시크릿, 필수 플래그 등이 비어 있으면 프로세스가 즉시 종료합니다.
확인 포인트
- 로그에
missing env,invalid config,cannot parse같은 메시지 kubectl describe에서Environment:섹션 확인configMapKeyRef/secretKeyRef가 잘못된 키를 참조
점검 커맨드
kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Environment:/,/Mounts:/p'
kubectl get configmap -n <NAMESPACE>
kubectl get secret -n <NAMESPACE>
해결 예시
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database_url
시크릿 키 이름이 틀리면 Pod는 뜨지만 앱이 죽을 수도 있고, 경우에 따라 컨테이너 시작 자체가 실패할 수도 있습니다. 로그와 이벤트를 같이 보세요.
3) 원인 3: OOMKilled (메모리 부족) 또는 메모리 제한 과도
CrashLoopBackOff의 “정답”으로 가장 많이 나오는 케이스입니다. 애플리케이션이 메모리를 쓰다가 limits.memory를 넘으면 커널 OOMKiller에 의해 죽고, kubelet이 재시작합니다.
확인 포인트
describe에Reason: OOMKilled- 종료 코드가
137 - 재시작이 트래픽 증가/배치 작업 타이밍과 맞물림
점검 커맨드
kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Last State:/,/Events:/p'
# metrics-server가 있다면
kubectl top pod -n <NAMESPACE> <POD_NAME>
해결 전략
- 단기:
resources.limits.memory상향, 또는 요청량(requests) 조정 - 중기: 메모리 릭/캐시 설정/배치 처리량 조절
- 장기: HPA/VPA, 워크로드 분리
예시:
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
메모리 상향만으로 해결되면 좋지만, 반복 OOM은 결국 비용 폭탄이 됩니다. 애플리케이션 레벨에서 원인을 찾아야 합니다.
4) 원인 4: liveness/readiness/startup 프로브 오설정
프로브는 “헬스체크”가 아니라 프로세스 생존을 결정하는 정책입니다. 특히 liveness가 너무 공격적이면, 앱이 정상인데도 kubelet이 계속 죽입니다.
- readiness 실패: 트래픽에서 제외(재시작은 안 할 수도 있음)
- liveness 실패: 컨테이너 재시작 유발
- startup은 초기 구동이 느린 앱에 유용
확인 포인트
describe이벤트에Liveness probe failed또는Readiness probe failed- 로그는 크게 문제 없어 보이는데 재시작이 반복
해결 예시
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
애플리케이션이 초기 마이그레이션/캐시 워밍업 때문에 느리다면, liveness 대신 startupProbe로 “초기 구간”을 보호하는 게 안정적입니다.
5) 원인 5: 포트/바인딩 문제 (127.0.0.1만 리슨, 잘못된 PORT)
컨테이너 내부에서 앱이 127.0.0.1에만 바인딩하면, 프로브나 사이드카가 접근하지 못해 실패할 수 있습니다. 또는 PORT 환경변수 불일치로 서버가 다른 포트에 떠서 프로브가 실패합니다.
확인 포인트
- 이벤트는 프로브 실패인데, 앱 로그에는 “서버 시작”이 찍힘
- 앱이
Listening on 127.0.0.1:8080처럼 출력
해결 힌트
- 바인딩 주소를
0.0.0.0로 - 프로브 포트와 실제 리슨 포트 일치
예시(Node.js):
app.listen(process.env.PORT || 8080, '0.0.0.0')
6) 원인 6: 의존 서비스 연결 실패 (DB, Kafka, Redis)로 즉시 종료
애플리케이션이 “부팅 시점에 외부 의존성이 반드시 필요”하도록 작성되어 있으면, 네트워크 일시 장애나 DNS 지연에도 바로 죽고 재시작을 반복합니다.
이 경우 CrashLoopBackOff는 증상이고, 실제 원인은 다음 중 하나입니다.
- DNS 간헐 실패
- 네트워크 MTU/PMTUD 문제
- 보안그룹/네트워크폴리시 차단
- 서비스 메시 mTLS 핸드셰이크 실패
문맥상 함께 보면 좋은 글:
확인 포인트
- 로그에
connection refused,i/o timeout,no such host,TLS handshake error - 특정 노드에 스케줄링될 때만 실패(노드/ENI/MTU 차이 가능)
빠른 네트워크 확인(임시 디버그 파드)
kubectl run -n <NAMESPACE> net-debug --rm -it --image=curlimages/curl -- sh
# DNS
nslookup mydb.<NAMESPACE>.svc.cluster.local
# HTTP 확인
curl -sv http://myservice:8080/healthz
애플리케이션이 의존 서비스 연결 실패 시 즉시 종료하도록 되어 있다면, 재시작만 반복하며 장애를 증폭시킵니다. 연결 재시도(지수 백오프), graceful degradation, startupProbe로 완충을 고려하세요.
7) 원인 7: 이미지/아키텍처/런타임 문제 (exec format error 등)
이미지가 잘못 빌드되어 노드 아키텍처와 맞지 않으면 컨테이너가 시작하자마자 죽습니다. 예를 들어 ARM 노드에 AMD64 이미지가 배포되거나(또는 반대), glibc/musl 호환성 문제로 실행이 실패할 수 있습니다.
확인 포인트
- 로그 또는 이벤트에
exec format error - 노드 그룹이 혼합 아키텍처
해결
- 멀티 아키텍처 이미지로 빌드
- 노드 셀렉터/테인트로 워크로드 고정
예시:
nodeSelector:
kubernetes.io/arch: amd64
8) 원인 8: 볼륨 마운트/권한/읽기전용 파일시스템 문제
애플리케이션이 파일을 쓰려는데, 볼륨이 없거나 권한이 없어 실패하면 부팅 단계에서 종료할 수 있습니다. 보안 강화를 위해 readOnlyRootFilesystem: true를 켜둔 환경에서 특히 자주 터집니다.
확인 포인트
- 로그에
permission denied,read-only file system,no such file or directory describe의Mounts:및Volumes:섹션- SecurityContext 설정 확인
해결 예시
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
애플리케이션이 /tmp나 로그 디렉터리에 쓰는 구조라면, 쓰기 가능한 볼륨을 명시적으로 제공해야 합니다.
재발 방지: CrashLoopBackOff를 “장애”로 키우지 않는 운영 팁
1) 로그는 반드시 표준 출력으로
컨테이너 환경에서 파일 로그만 남기면, 크래시 직전 단서가 사라지기 쉽습니다. stdout/stderr로 남기고 수집 파이프라인(예: Fluent Bit)을 붙이세요.
2) 의존 서비스 실패 시 즉시 종료하지 말 것
부팅 시점에 DB가 잠깐 느려도 전체 배포가 CrashLoop로 무너집니다. 연결 재시도 정책과 startupProbe를 조합하면 “죽지 않고 기다리는 앱”이 됩니다.
3) 프로브는 보수적으로 시작하고 점진적으로 조이기
초기에는 initialDelaySeconds와 failureThreshold를 넉넉히 잡고, 실제 지표를 보고 줄이는 편이 안전합니다.
결론: 원인은 8가지로 수렴하고, 답은 항상 이벤트와 이전 로그에 있다
CrashLoopBackOff를 보면 당황하기 쉽지만, 실제로는 다음 순서로 거의 정리됩니다.
describe이벤트로 프로브/OOM/스케줄링 문제를 먼저 분기logs --previous로 “죽기 직전”을 확인- 종료 코드와
reason으로 OOM/시그널/일반 오류를 분류 - 설정, 네트워크, 권한, 아키텍처를 체크리스트로 제거
이 루틴을 팀의 런북으로 고정해두면, CrashLoopBackOff는 “긴급 장애”가 아니라 “빠르게 분류 가능한 운영 이벤트”가 됩니다.