홈/인프라/CrashLoopBackOff 해결: Pod 무한 재시작 7가지 원인 진단법
인프라CrashLoopBackOff쿠버네티스 트러블슈팅

CrashLoopBackOff 해결: Pod 무한 재시작 7가지 원인 진단법

Pod가 CrashLoopBackOff로 계속 재시작된다면 kubectl describe와 logs --previous로 1분 만에 원인을 좁히세요. OOMKilled(Exit 137), 시크릿 누락, liveness probe 오류 등 7가지 원인별 진단 명령어와 YAML 수정 예제를 정리했습니다.

CrashLoopBackOff 해결: Pod 무한 재시작 7가지 원인 진단법
목차
  1. 배포는 됐는데 Pod가 무한 재시작된다면
  2. 1분 진단 루틴: describe → logs --previous → Exit Code
  3. 원인별 진단·해결 7선
  4. Exit Code 해석 표
  5. 로그가 아예 안 남는 즉시 종료 디버깅
  6. 결론: 진단 체크리스트
  7. 참고: 공식 문서
  8. 자주 묻는 질문 (FAQ)

CrashLoopBackOff 완벽 해결: Pod 무한 재시작 7가지 원인 진단법

이 글이 맞는 상태: kubectl get pod의 STATUS가 CrashLoopBackOff이고, kubectl describe pod의 Events에 Back-off restarting failed container가 찍힙니다. 컨테이너는 떴다가 죽고 있으니, 종료 코드와 직전 로그로 원인을 고릅니다.

원인확인 명령해결
앱 예외·설정 오류 (Exit 1)kubectl logs <pod> --previous로그가 가리키는 코드·환경변수 수정
메모리 한도 초과 (OOMKilled, Exit 137)kubectl describe pod <pod>의 Last Statememory limit 상향 또는 사용량 축소
liveness probe 실패Events의 Liveness probe failedstartupProbe 추가, liveness 임계치 완화
의존 서비스 미준비 (connection refused)kubectl logs <pod> --previous앱 재시도 로직, 필요하면 initContainer
명령·경로 오류 (no such file or directory)kubectl describe pod <pod>의 Command·Mounts이미지 ENTRYPOINT 확인, 경로·subPath 수정

참조한 Secret·ConfigMap이 아예 없으면 STATUS는 CrashLoopBackOff가 아니라 CreateContainerConfigError입니다(원인 3 참고).

배포는 됐는데 Pod가 무한 재시작된다면

kubectl get pod를 쳤더니 STATUS 칸에 CrashLoopBackOff가 떠 있고, RESTARTS 카운트가 계속 올라가고 있다면 이 글이 정확히 필요한 순간입니다.

가장 먼저 알아둘 것은 **CrashLoopBackOff는 "상태"가 아니라 "증상"**이라는 점입니다. 컨테이너가 시작 → 크래시 → 쿠버네티스가 재시작 → 또 크래시를 반복하니까, 백오프(지연 재시도) 간격을 두며 다시 시작한다는 뜻일 뿐입니다. 진짜 원인은 컨테이너 안에서 죽은 이유에 있습니다.

이 시리즈의 1편에서 다룬 Pending은 스케줄링 단계의 문제(자원 부족·노드 셀렉터)였고, 2편의 ImagePullBackOff는 이미지를 아예 못 받아오는 문제였습니다. 반면 CrashLoopBackOff는 이미지도 받았고 컨테이너도 떴는데, 실행되자마자 죽는 단계입니다. 즉 한 단계 더 안쪽으로 들어온 셈이죠. 그래서 디버깅 포인트는 "스케줄러"가 아니라 "컨테이너 프로세스의 종료 코드와 로그"입니다.

1분 진단 루틴: describe → logs --previous → Exit Code

원인이 뭐든 시작은 항상 이 3단계입니다. 순서대로 치면 범위가 빠르게 좁혀집니다.

1단계: describe로 Events 확인

Bash
kubectl describe pod my-app-7d9f8-abcde

맨 아래 Events 섹션이 핵심입니다.

CODE
Events:
  Type     Reason     Age                From     Message
  ----     ------     ----               ----     -------
  Normal   Pulled     2m                 kubelet  Successfully pulled image
  Normal   Created    2m (x4 over 3m)    kubelet  Created container app
  Normal   Started    2m (x4 over 3m)    kubelet  Started container app
  Warning  BackOff    30s (x8 over 3m)   kubelet  Back-off restarting failed container

Back-off restarting failed container는 CrashLoopBackOff의 공통 신호입니다. 이게 보이면 "컨테이너가 시작은 했는데 죽었다"가 확정입니다. 이제 왜 죽었는지를 봐야 합니다.

2단계: logs --previous로 죽은 컨테이너 로그 확보

지금 떠 있는 컨테이너는 또 죽기 직전이라 로그가 비어 있을 수 있습니다. 방금 죽은(이전) 컨테이너의 로그를 봐야 합니다.

Bash
kubectl logs my-app-7d9f8-abcde --previous
# 컨테이너가 여러 개면
kubectl logs my-app-7d9f8-abcde -c app --previous

--previous(축약 -p)를 빼먹어서 "로그가 안 나온다"며 헤매는 경우가 정말 많습니다. 크래시 디버깅에서 가장 먼저 챙길 옵션입니다.

3단계: Exit Code 확인

describe 출력의 Last State 블록에서 종료 코드를 읽습니다.

CODE
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137

kubectl get pod의 RESTARTS 카운트도 같이 보세요. 빠르게 치솟으면 즉시 크래시, 한참 떠 있다가 죽으면 메모리 누수나 probe 문제일 가능성이 큽니다.

에러 메시지 → 원인 매핑 표

로그/Events 메시지유력 원인바로 갈 섹션
Back-off restarting failed container공통 신호 (컨테이너 크래시)전부
OOMKilled / Exit 137메모리 한도 초과원인 2
missing env / env XXX is undefined (앱 로그)환경변수·시크릿 키 누락원인 3
Error: secret "xxx" not found (STATUS CreateContainerConfigError)참조한 Secret 자체가 없음(CrashLoopBackOff 아님)원인 3
Liveness probe failed프로브 설정 오류원인 4
connection refused / dial tcp ...의존 서비스(DB) 미준비원인 5
no such file or directory마운트 경로/command 오류원인 6·7
Exit 1 + 스택트레이스앱 일반 예외원인 1

원인별 진단·해결 7선

원인 1. 애플리케이션 예외 / 엔트리포인트 오류 (Exit 1)

가장 흔한 케이스. 앱 코드가 시작하자마자 예외를 던지고 죽습니다.

Bash
kubectl logs <pod> --previous   # 스택트레이스 확인

로그에 NullPointerException, Cannot find module, panic: 같은 메시지가 그대로 찍힙니다. 이건 인프라 문제가 아니라 앱 버그이므로, 로그가 가리키는 코드/설정을 고치는 게 정답입니다. 인프라 담당자라면 로그를 캡처해 개발팀에 그대로 넘기면 됩니다.

원인 2. OOMKilled (Exit 137)

Bash
kubectl describe pod <pod> | grep -A2 "Last State"   # Reason: OOMKilled 확인
kubectl top pod <pod>                                 # 실사용 메모리 확인(metrics-server 필요)

137 = 128 + 9, 즉 SIGKILL입니다. 메모리 limit을 초과해 커널 OOM killer가 강제 종료한 것이고, describe에 Reason: OOMKilled, Exit Code: 137로 남습니다(리소스 관리 문서).

Before (한도가 너무 빡빡)

YAML
resources:
  limits:
    memory: "128Mi"
  requests:
    memory: "128Mi"

After (실사용 기반 상향)

YAML
resources:
  requests:
    memory: "256Mi"   # 평상시 사용량
  limits:
    memory: "512Mi"   # 피크 여유분

근본 해결책은 kubectl top pod(metrics-server 필요)로 실제 사용량을 측정한 뒤 limit을 맞추는 것입니다. OOM이 잦은 워크로드라면 VPA(Vertical Pod Autoscaler)로 requests/limits를 자동 튜닝하는 것도 최근 많이 쓰는 방법입니다.

원인 3. 환경변수 · 시크릿 누락

Bash
kubectl logs <pod> --previous           # "secret not found" / "env XXX is undefined"
kubectl get secret db-credentials        # 시크릿 존재 여부
kubectl describe secret db-credentials  # 키 이름·크기만 확인(값은 출력되지 않음)

먼저 구분할 점이 있습니다. 참조한 Secret이 아예 없으면 컨테이너가 시작되지 않으므로 STATUS는 CrashLoopBackOff가 아니라 CreateContainerConfigError이고, Events에 Error: secret "db-credential" not found가 찍힙니다. kubelet은 Secret이 생길 때까지 주기적으로 다시 시도합니다(Secret 문서). CrashLoopBackOff가 되는 건 Secret은 있지만 키 이름이 달라 envFrom으로 기대한 환경변수가 비어 있고, 앱이 기동 중 설정 검증에 실패해 죽는 경우입니다. -o yaml은 Secret 값(base64)까지 터미널에 출력하므로 키 확인에는 describe가 안전합니다.

Before (Secret 이름 오타 → CreateContainerConfigError)

YAML
envFrom:
  - secretRef:
      name: db-credential   # 's' 누락

After

YAML
envFrom:
  - secretRef:
      name: db-credentials

근본 해결책: 시크릿/ConfigMap을 Pod보다 먼저 배포하고, 이름과 키를 kubectl get secret으로 대조하세요. 같은 네임스페이스에 있는지도 꼭 확인합니다.

원인 4. Liveness Probe 오류

컨테이너 자체는 멀쩡한데, 앱이 다 뜨기도 전에 probe가 실패해서 kubelet이 죽이는 경우입니다.

CODE
Warning  Unhealthy  kubelet  Liveness probe failed: HTTP probe failed with statuscode: 500
Normal   Killing    kubelet  Container failed liveness probe, will be restarted

Before (기동 시간을 안 줌)

YAML
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 1
  failureThreshold: 1

After (기동 여유 + 임계치 완화)

YAML
startupProbe:          # 기동 전용 프로브(1.20 GA): 통과할 때까지 liveness 검사 보류
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 5
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  failureThreshold: 3
  periodSeconds: 10

근본 해결책: 기동이 느린 앱은 startupProbe로 분리하고, liveness는 "진짜로 죽었을 때만" 발동하도록 보수적으로 설정합니다. startupProbe는 1.20에서 GA가 됐고(기능 게이트 목록), 성공할 때까지 liveness·readiness 검사를 미룹니다(프로브 설정 문서). 위 예시는 최대 30×5초=150초까지 기동을 기다립니다.

프로브 필드 기본값

필드기본값조정할 때 볼 것
initialDelaySeconds0startupProbe를 쓰면 liveness 쪽은 0으로 둬도 됩니다
periodSeconds10 (최소 1)너무 짧으면 일시적인 지연에도 실패가 빨리 누적됩니다
timeoutSeconds1 (최소 1)헬스 엔드포인트가 DB·외부 API를 조회하면 1초를 넘기기 쉽습니다
failureThreshold3 (최소 1)startupProbe는 failureThreshold × periodSeconds가 최악의 기동 시간보다 길어야 합니다
successThreshold1liveness·startup 프로브는 1이어야 합니다

기본값은 Probe API 레퍼런스 기준입니다. 표의 숫자를 정답으로 쓰기보다, 실제 기동 시간과 헬스 엔드포인트 응답 시간을 재서 맞추세요.

원인 5. 의존 서비스(DB) 연결 실패

Bash
kubectl logs <pod> --previous   # "connection refused" / "dial tcp 10.x:5432"

앱이 DB·Redis가 준비되기 전에 떠서 연결 실패로 죽는 케이스입니다.

해결: initContainer로 의존성 대기

YAML
initContainers:
  - name: wait-for-db
    image: busybox:1.36
    command: ['sh', '-c',
      'until nc -z postgres 5432; do echo waiting; sleep 2; done']

근본적으로는 앱에 재시도(backoff retry) 로직을 넣는 것이 가장 견고합니다. initContainer는 기동 순서를 보장하지만, 운영 중 DB가 잠깐 끊겼을 때의 복원력은 앱 레벨 재시도가 책임집니다.

원인 6. ConfigMap 마운트 경로 오류

Bash
kubectl logs <pod> --previous   # "no such file or directory: /config/app.yaml"
kubectl describe pod <pod>      # Volumes / Mounts 확인

Before (subPath 없이 디렉터리째 덮어 기존 파일이 사라짐)

YAML
volumeMounts:
  - name: config
    mountPath: /app/config/app.yaml   # 파일을 디렉터리로 마운트

After (subPath로 단일 파일만 주입)

YAML
volumeMounts:
  - name: config
    mountPath: /app/config/app.yaml
    subPath: app.yaml
volumes:
  - name: config
    configMap:
      name: app-config

근본 해결책: 단일 파일을 주입할 땐 subPath를 쓰고, mountPath가 앱이 실제로 읽는 경로와 일치하는지 확인합니다. 단, subPath로 마운트한 파일은 ConfigMap을 수정해도 갱신되지 않으므로 변경 후 Pod를 재시작해야 합니다(ConfigMap 문서).

원인 7. 잘못된 command / args

이미지 엔트리포인트를 잘못 덮어쓰면 exec: "xxx": executable file not found가 뜹니다.

Before

YAML
command: ["python3"]
args: ["app.py"]   # 작업 디렉터리에 app.py가 없음 → 즉시 종료

After

YAML
command: ["python3"]
args: ["/app/main.py"]

근본 해결책: 이미지의 기본 ENTRYPOINT/CMD를 확인하고, 꼭 필요할 때만 override 하세요.

Exit Code 해석 표

Exit Code시그널의미먼저 볼 곳
0-정상 종료인데 재시작restartPolicy·엔트리포인트가 데몬으로 안 떠 있음
1-앱 일반 예외logs --previous 스택트레이스
2-셸 내장 명령 오용·인자 오류(셸 관례)엔트리포인트 스크립트 문법, 옵션 오타
126-명령은 찾았지만 실행할 수 없음실행 권한(chmod +x), 파일 형식
127-명령을 찾을 수 없음command/args 경로, PATH, 이미지에 바이너리가 있는지
137SIGKILLOOM 또는 강제 종료memory limit, kubectl top
139SIGSEGV세그멘테이션 폴트네이티브 라이브러리/아키텍처(arm vs amd)
143SIGTERM정상 종료 신호graceful shutdown 처리, 외부 종료 요인

Exit 0인데 재시작한다면 대개 "한 번 실행하고 끝나는 스크립트"를 Deployment로 띄운 경우입니다. 일회성 작업이면 Job으로 바꾸세요.

126·127은 Bash 종료 상태 관례(명령을 찾지 못하면 127, 찾았지만 실행할 수 없으면 126)를 따르는 셸이 돌려주는 값입니다. 그래서 엔트리포인트가 셸 스크립트이고 그 안의 명령이 실패할 때 주로 보입니다. 엔트리포인트 바이너리 자체가 이미지에 없으면 셸을 거치지 않고 컨테이너 런타임이 프로세스 시작에 실패하므로, Reason이 StartError로 표시되고 메시지에 executable file not found in $PATH가 남습니다(이때 종료 코드는 런타임에 따라 다릅니다).

Exit 0(Completed)인데 계속 재시작될 때

restartPolicy: Always인 Deployment에서는 정상 종료(0)도 재시작 대상입니다. 일회성 작업이 아닌데 0으로 끝난다면, 메인 프로세스(PID 1)가 포그라운드에 머물지 않는 경우가 대부분입니다.

  • 데몬이 백그라운드로 빠지는 설정: nginx라면 nginx -g 'daemon off;'처럼 포그라운드로 실행합니다. 공식 nginx 이미지의 기본 CMD가 이 형태입니다.
  • 엔트리포인트 스크립트가 서버를 &로 띄우고 끝나는 경우: 마지막 줄을 exec <서버 명령>으로 바꿔 서버가 PID 1이 되게 합니다. exec를 쓰면 SIGTERM도 서버 프로세스로 바로 전달됩니다.
Bash
#!/bin/sh
# entrypoint.sh — 준비 작업 후 서버를 PID 1로 실행
set -e
/app/migrate.sh
exec /app/server --port=8080

exec format error: 이미지와 노드의 CPU 아키텍처 불일치

logs --previous에 exec format error 한 줄만 남고 즉시 죽는다면, 이미지가 노드와 다른 아키텍처로 빌드됐을 가능성이 큽니다. arm64 Mac에서 빌드한 이미지를 amd64 노드에서 실행할 때가 대표적입니다.

Bash
# 노드 아키텍처
kubectl get nodes -L kubernetes.io/arch
# 이미지에 들어 있는 플랫폼 목록
docker buildx imagetools inspect <registry>/<image>:<tag>

노드 아키텍처가 이미지 목록에 없으면 그 플랫폼으로 다시 빌드합니다. 아키텍처가 다른 노드가 섞여 있다면 멀티 플랫폼 이미지로 빌드해 푸시합니다(Docker 멀티 플랫폼 빌드 문서).

Bash
docker buildx build --platform linux/amd64,linux/arm64 -t <registry>/<image>:<tag> --push .

로그가 아예 안 남는 즉시 종료 디버깅

가장 짜증나는 케이스. --previous를 쳐도 로그가 비어 있고, 컨테이너가 너무 빨리 죽어 exec도 못 들어가는 상황입니다. 이럴 땐 컨테이너를 일부러 살려두고 내부를 직접 봅니다.

YAML
# 임시로 엔트리포인트를 sleep으로 덮어 컨테이너를 살려둠
command: ["sleep", "3600"]

이 상태로 배포한 뒤 내부에 들어가 손으로 실행해 봅니다.

Bash
kubectl exec -it <pod> -- sh
# 안에서 직접 실행해 진짜 에러 메시지 확인
/app/entrypoint.sh

원본 이미지를 건드리기 싫다면 ephemeral container를 붙이는 kubectl debug도 좋습니다.

Bash
kubectl debug -it <pod> --image=busybox:1.36 --target=app -- sh

컨테이너가 너무 빨리 죽어 --target으로 붙을 수 없다면, 명령을 셸로 바꾼 Pod 사본을 만듭니다. 원본 Pod는 그대로 두고 사본 안에서 엔트리포인트를 손으로 실행해 볼 수 있습니다(공식 디버깅 문서).

Bash
kubectl debug <pod> -it --copy-to=<pod>-debug --container=app -- sh
# 확인이 끝나면 사본 삭제
kubectl delete pod <pod>-debug

실무 경험 한마디: 새벽 장애 대응에서 제가 가장 자주 만난 CrashLoopBackOff 원인은 "환경변수·시크릿 키 설정 오류"와 "OOMKilled"였습니다. 그래서 호출 받으면 무조건 describe로 Exit Code부터 봅니다. 137이면 메모리, 설정 관련 메시지면 Secret·ConfigMap, 그 외면 logs --previous로 직행합니다.

결론: 진단 체크리스트

  1. kubectl get pod → RESTARTS 카운트와 STATUS 확인
  2. kubectl describe pod → Events의 Back-off restarting...와 Exit Code 확인
  3. kubectl logs <pod> --previous → 죽은 컨테이너 로그 확보
  4. Exit Code로 분기: 137→메모리, 1→앱 예외, secret/env→설정, probe→프로브
  5. 로그가 없으면 command: sleep override 또는 kubectl debug로 내부 진입

이미지를 못 받아오는 ImagePullBackOff는 2편, 스케줄링 단계에서 막히는 Pending은 1편을 참고하세요. 컨테이너는 떴는데 외부에서 접속이 안 된다면, 다음 4편 Service/Endpoint 연결 실패 트러블슈팅에서 이어집니다.

참고: 공식 문서

이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.

자주 묻는 질문 (FAQ)

Q. kubectl logs --previous를 쳤는데 "previous terminated container not found"가 나옵니다. A. 아직 컨테이너가 재시작되지 않았거나(첫 크래시 직후), 노드가 이전 컨테이너를 정리한 경우입니다. 잠깐 기다렸다가 RESTARTS가 올라간 뒤 다시 치거나, kubectl describe의 Last State 블록에서 종료 이유를 확인하세요.

Q. Exit Code 137인데 kubectl top pod로 보면 메모리가 limit보다 낮습니다. 왜 OOM이 날까요? A. 순간 피크에서 limit을 넘겼다가 죽은 뒤 측정된 값일 수 있습니다. top은 현재값만 보여주므로, Prometheus 등으로 container_memory_working_set_bytes의 피크를 확인하고 limit을 그 위로 올리세요. 137이 SIGKILL이라는 점에서 외부 강제 종료(예: 노드 자원 압박)도 의심해볼 수 있습니다.

Q. CrashLoopBackOff 상태에서 재시작 간격이 점점 길어지는데 정상인가요? A. 정상입니다. kubelet은 재시작 지연을 10초, 20초, 40초…로 두 배씩 늘리고 300초(5분)에서 멈춥니다. 컨테이너가 10분 동안 문제없이 실행되면 백오프 타이머가 리셋되고, RESTARTS 숫자 자체는 그대로 남습니다(Pod 라이프사이클 문서). 1.35부터 베타(기본 활성)인 KubeletCrashLoopBackOffMax를 쓰면 노드별로 최대 지연을 조정할 수 있습니다(기능 게이트 목록). 간격이 길다고 장애가 심한 건 아니니, 간격보다 종료 코드와 로그에 집중하세요.

확인 정보
✦ ✦ ✦
출처·검증 안내

글마다 출처와 확인 범위가 다릅니다. 아래에 확인일·확인 범위·출처가 기록된 글만 그 범위에서 확인된 것입니다. 기록이 없는 글은 적용 전 사용 중인 버전의 공식 문서로 직접 확인하고, 오류는 이메일로 알려주세요. 편집 정책 · 이메일로 제보

이 글에는 사람이 확인한 날짜·범위 기록이 아직 없습니다.

발행 · ·업데이트 ·
주제 관련 공식 문서(자동 연결 · 문장별 근거 아님)Kubernetes 공식 문서 ↗

댓글

첫 번째 댓글을 남겨보세요.