- Published on
EKS CrashLoopBackOff 원인 10분만에 찾기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 매니지드 쿠버네티스(EKS)에서 장애 대응이 어려운 순간은 대개 비슷합니다. 배포 직후 트래픽이 안 붙고, kubectl get pods 를 보니 CrashLoopBackOff 가 떠 있습니다. 더 답답한 건 로그가 비어 있거나 컨테이너가 너무 빨리 죽어서 kubectl logs 가 따라잡지 못하는 경우죠.
이 글은 10분 안에 원인을 좁히는 것을 목표로 합니다. “원인 후보를 넓게 나열”하기보다, 가장 정보 밀도가 높은 신호부터 확인해서 빠르게 수렴하는 루틴으로 구성했습니다.
관련해서 로그가 아예 안 보이는 케이스는 별도 글에서 더 깊게 다뤘습니다. 필요하면 함께 보세요.
- EKS Pod CrashLoopBackOff 로그 없을 때 7단계 진단
- EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검
- EKS에서 NLB 타겟 Unhealthy - 헬스체크·Pod·SG
0분: CrashLoopBackOff의 의미부터 정리
CrashLoopBackOff 는 “쿠버네티스가 컨테이너를 재시작했는데, 계속 죽어서 재시작 간격을 점점 늘리는(backoff) 상태”입니다.
즉, 핵심은 두 가지입니다.
- 왜 프로세스가 종료되는가 (exit code, signal, OOMKilled 등)
- 왜 쿠버네티스가 재시작을 계속 시도하는가 (Deployment/ReplicaSet의 desired state)
원인은 애플리케이션 버그부터 설정/권한/리소스/프로브까지 다양하지만, 진단은 항상 이전 종료 원인을 먼저 잡으면 빨라집니다.
1분: 현재 상태를 한 줄로 요약하기
먼저 “어떤 Pod가 어떤 이유로 죽는지”를 한 번에 보겠습니다.
# 네임스페이스 포함해서 확인
kubectl get pods -n myns -o wide
# 특정 Pod의 컨테이너 상태(ExitCode, Reason, LastState 등)
kubectl describe pod -n myns mypod-xxxxx
kubectl describe pod 에서 특히 아래를 집중해서 봅니다.
State/Last StateReason(예:Error,OOMKilled,ContainerCannotRun,CrashLoopBackOff)Exit Code(예:1,137)- 하단
Events섹션 (이미 답이 적혀있는 경우가 많습니다)
Exit Code 빠른 해석
Exit Code 137인 경우가 많다면: 대개 OOMKilled 또는 SIGKILL 계열(메모리 압박) 의심Exit Code 1: 앱이 스스로 종료(환경변수 누락, 설정 파일 없음, 마이그레이션 실패 등) 가능성 큼Reason: ContainerCannotRun/RunContainerError: 엔트리포인트, 권한, 이미지, 마운트 문제 가능성
2분: “이전 로그”를 먼저 본다
CrashLoop에서는 현재 컨테이너가 이미 죽어버려 로그가 끊기는 경우가 많습니다. 이럴 때는 --previous 가 가장 빠른 답입니다.
# 이전에 죽은 컨테이너 로그
kubectl logs -n myns mypod-xxxxx --previous
# 멀티 컨테이너 Pod라면 컨테이너 지정
kubectl logs -n myns mypod-xxxxx -c app --previous
여기서 바로 아래 같은 메시지가 나오면 거의 끝입니다.
- 설정 파일 경로 오류, 환경변수 누락
- DB/Redis 연결 실패
- 마이그레이션 실패
- TLS 인증서/키 파일 없음
- 권한 부족(예: AWS API
AccessDenied)
로그가 짧거나 아예 없다면 “프로세스가 뜨자마자 죽는” 케이스일 수 있으니 다음 단계로 넘어갑니다.
3분: 이벤트에서 “쿠버네티스가 본 실패”를 확인
애플리케이션 로그가 없을 때는 이벤트가 더 정직합니다.
# Pod 이벤트만 빠르게
kubectl get events -n myns --sort-by=.metadata.creationTimestamp | tail -n 50
# 특정 Pod에 붙은 이벤트는 describe 하단에서도 확인 가능
kubectl describe pod -n myns mypod-xxxxx
자주 나오는 이벤트 패턴은 다음과 같습니다.
Back-off restarting failed container: 반복 크래시FailedMount: 볼륨/시크릿/컨피그맵 마운트 실패Readiness probe failed/Liveness probe failed: 프로브 설정 문제 또는 앱 기동 지연Failed to pull image/ImagePullBackOff: 이미지/레지스트리 인증 문제(이건 CrashLoop 이전에 걸리는 경우가 많음)
4분: 프로브(liveness/readiness/startup)가 앱을 죽이고 있지 않은가
CrashLoopBackOff의 흔한 함정은 “앱은 사실 정상인데, 프로브가 너무 공격적이라서 kubelet이 계속 죽이는” 상황입니다.
다음 조합이면 특히 위험합니다.
- 기동이 느린데
livenessProbe가 빠르게 시작됨 initialDelaySeconds가 너무 짧음timeoutSeconds가 너무 짧음failureThreshold가 너무 작음
확인은 Deployment 스펙을 보면 됩니다.
kubectl get deploy -n myns myapp -o yaml | sed -n '1,200p'
빠른 해결 방향
- 기동이 느리면
startupProbe를 추가하고,startupProbe가 성공하기 전까지livenessProbe를 유예 - 최소한 기동 시간만큼
initialDelaySeconds를 늘리기
예시(HTTP 앱 기준):
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
프로브 실패가 NLB/Ingress 헬스체크와 엮여 “타겟이 Unhealthy”로 보이는 경우도 많습니다. 이 연결고리는 아래 글이 도움이 됩니다.
5분: OOMKilled(메모리) 여부를 즉시 판별
describe 에 OOMKilled 가 보이거나 Exit Code가 137 이면, 우선 “메모리가 부족했다”를 사실로 두고 다음을 확인합니다.
# 리소스 요청/제한 확인
kubectl get pod -n myns mypod-xxxxx -o jsonpath='{.spec.containers[*].resources}'
# metrics-server가 있다면 현재 사용량
kubectl top pod -n myns mypod-xxxxx
kubectl top node
자주 발생하는 원인
limits.memory가 너무 낮음- JVM/Node/Python 등 런타임 메모리 튜닝이 limit을 초과
- 캐시/버퍼가 기동 시 한꺼번에 잡힘
- 큰 파일 다운로드/압축해제 등으로 순간 피크 발생
빠른 조치
- 우선
limits.memory를 올려서 “원인이 메모리인지”를 확정 - JVM이면
-Xmx를limits.memory보다 여유 있게 작게 설정 - Node.js면
--max-old-space-size조정
6분: ConfigMap/Secret/볼륨 마운트 실패 확인
앱 로그가 전혀 없이 바로 죽는 케이스 중 상당수가 “필수 파일이 없다”입니다. 특히 Secret 키 이름 오타, 경로 오타, subPath 실수는 빈번합니다.
# 마운트/환경변수로 주입된 시크릿/컨피그맵 확인
kubectl describe pod -n myns mypod-xxxxx
# 실제 리소스 존재 여부
kubectl get secret -n myns
kubectl get configmap -n myns
이벤트에 FailedMount 가 찍히면 거의 확정입니다. 예를 들어 아래처럼 나옵니다.
MountVolume.SetUp failed for volume ... secret ... not foundconfigmap ... not found
7분: IRSA/IAM 권한 문제(AccessDenied) 빠르게 잡기
EKS에서 CrashLoop의 “체감상” 가장 흔한 원인 중 하나가 AWS API 호출 권한 문제입니다. 앱이 기동 중 S3/SSM/Secrets Manager/KMS 등을 읽다가 AccessDenied 를 만나고 즉시 종료하는 패턴이 많습니다.
확인 순서는 다음이 빠릅니다.
- 이전 로그에서
AccessDenied문자열 확인 - ServiceAccount에 IRSA annotation이 붙어 있는지 확인
- Pod가 실제로 그 ServiceAccount를 쓰는지 확인
- Trust policy와 OIDC issuer가 맞는지 확인
명령어:
# Pod가 사용하는 ServiceAccount
kubectl get pod -n myns mypod-xxxxx -o jsonpath='{.spec.serviceAccountName}'
# ServiceAccount annotation (role-arn)
kubectl get sa -n myns myserviceaccount -o yaml
IRSA 점검은 경우의 수가 많아서 체크리스트 형태로 보는 게 빠릅니다.
8분: 컨테이너 커맨드/엔트리포인트/권한 문제 확인
이미지 자체는 정상인데, 실행 커맨드가 잘못되면 즉시 종료합니다.
command/args오타- 쉘 스크립트에 실행 권한 없음
#!/bin/sh경로 불일치(알파인/디비안 차이)- 워킹 디렉터리/파일 경로가 이미지와 불일치
확인은 Pod spec과 이미지 메타를 함께 봅니다.
kubectl get pod -n myns mypod-xxxxx -o yaml | sed -n '1,220p'
그리고 로컬에서 동일 이미지로 커맨드를 재현해보면 바로 드러납니다.
# 로컬 재현(가능한 경우)
docker run --rm -it myrepo/myimage:tag sh -lc 'ls -al && ./start.sh'
EKS 노드가 containerd를 쓰더라도 “문제 재현” 관점에선 Docker가 가장 빠른 도구인 경우가 많습니다.
9분: 네트워크/의존성 장애를 “기동 단계”에서 분리하기
앱이 기동하자마자 외부 의존성(DB, Redis, Kafka, 외부 API)에 붙다가 실패하고 종료하는 설계라면, 작은 네트워크 이슈도 CrashLoop로 증폭됩니다.
빠르게 분리 진단하는 방법은 다음입니다.
- 앱을 “의존성 없이 기동” 가능하도록 플래그 제공(가능하면)
- 같은 네임스페이스에 임시 디버그 Pod를 띄워 DNS/연결만 확인
# 임시 디버그 Pod (curl/dig 등)
kubectl run -n myns net-debug --rm -it --image=curlimages/curl -- sh
# Pod 내부에서
# DNS 확인
nslookup mydb.myns.svc.cluster.local
# HTTP 연결 확인
curl -sv http://myservice.myns.svc.cluster.local:8080/healthz
여기서 DNS가 깨졌다면 CoreDNS, 네트워크 폴리시, VPC CNI, 보안그룹/라우팅 등으로 범위가 좁혀집니다.
10분: “원인 확정”을 위한 최소 재현과 롤백 전략
10분 내 목표는 “완벽한 근본 원인 분석”이 아니라 원인 후보를 하나로 수렴하고, 서비스 복구를 위한 선택지를 확보하는 것입니다.
최소 재현 체크
--previous로그에 같은 에러가 반복되는가Events의 실패 원인이 일관적인가- 프로브/리소스/권한 중 하나를 바꿨을 때 증상이 즉시 변하는가
가장 빠른 복구 옵션
- 직전 정상 버전으로 롤백
kubectl rollout history deploy -n myns myapp
kubectl rollout undo deploy -n myns myapp
kubectl rollout status deploy -n myns myapp
- 프로브 완화(특히 기동이 느린 서비스)
- 메모리 limit 상향으로 OOM 여부 확정
- IRSA/Secret 누락은 즉시 수정 후 재배포
자주 만나는 원인 10가지 요약(체크리스트)
아래 10개는 현장에서 실제로 CrashLoopBackOff로 가장 자주 이어지는 항목들입니다.
- 앱 설정/환경변수 누락으로 기동 실패
- DB/Redis 등 의존성 연결 실패를 “즉시 종료”로 처리
livenessProbe가 너무 빨라서 kubelet이 앱을 죽임startupProbe부재로 기동 지연을 장애로 오판OOMKilled(limit 부족, 런타임 메모리 튜닝 미스)- Secret/ConfigMap 키 오타 또는 리소스 미존재
- 볼륨 마운트 경로/권한 문제(subPath 포함)
- IRSA 설정 불일치로 AWS API
AccessDenied - 엔트리포인트/커맨드/실행 권한 문제
- 이미지 태그/아키텍처 불일치로 실행 불가(특히 ARM/AMD 혼재)
결론: 가장 빠른 루트는 describe 와 --previous
CrashLoopBackOff 대응에서 시간을 아끼는 핵심은 “추측”을 줄이고, 쿠버네티스가 이미 수집한 신호를 먼저 소비하는 것입니다.
kubectl describe pod로Last State와Events확인kubectl logs --previous로 직전 크래시 로그 확인- 그 다음에 프로브, OOM, 마운트, IRSA 순으로 범위를 좁히기
이 루틴만 습관화해도, 대부분의 CrashLoopBackOff는 10분 안에 원인 후보가 1개로 수렴합니다.