배포 직후 Pod가 Running으로 넘어가지 못하고 ImagePullBackOff에 멈춰 있는 상황은 쿠버네티스 운영에서 가장 자주 만나는 장애 중 하나입니다. 컨테이너가 시작조차 못 한 상태라 애플리케이션 로그도 없고, 원인은 이미지 이름 오타부터 레지스트리 인증 만료, 네트워크 차단까지 제각각입니다.
결론부터 말하면 ImagePullBackOff는 kubectl describe pod의 Events에 찍힌 에러 메시지 한 줄로 원인 계열을 먼저 분류해야 합니다. not found·manifest unknown은 이미지 이름·태그 문제, unauthorized·pull access denied는 인증 문제, i/o timeout·x509는 네트워크·인증서 문제입니다. 이 글에서는 메시지별 진단 분기와 해결 방법을 실무 순서대로 정리합니다.
Pod가 뜬 다음 반복 재시작되는 문제라면 Kubernetes CrashLoopBackOff 해결 가이드를 참고하세요. 두 상태는 원인 계열이 완전히 다릅니다.
핵심 요약
ErrImagePull은 “이미지 받기 실패”,ImagePullBackOff는 “실패가 반복되어 재시도 간격을 늘리며 대기 중”이라는 뜻입니다. 같은 문제의 두 단계입니다.- 첫 번째 명령은 언제나
kubectl describe pod입니다. Events의 Failed 메시지 원문이 원인을 알려줍니다. - 프라이빗 레지스트리는
docker-registry타입 Secret을 만들고imagePullSecrets로 연결해야 하며, Secret은 네임스페이스 단위로 존재해야 합니다. - Docker Hub 익명 pull은 rate limit(429 Too Many Requests)에 걸릴 수 있습니다. 노드가 많을수록 잘 터집니다.
- ECR·GCR 같은 클라우드 레지스트리는 토큰 만료(ECR 기준 12시간)가 단골 원인입니다.
- Apple Silicon에서 빌드한 arm64 이미지를 amd64 노드에서 당기면
no matching manifest로 실패합니다. 멀티 아키텍처 빌드로 해결합니다.
1. ErrImagePull과 ImagePullBackOff의 관계
kubelet은 컨테이너 이미지를 받지 못하면 일단 ErrImagePull을 기록하고 재시도합니다. 실패가 반복되면 재시도 간격을 5초 → 10초 → 20초처럼 지수적으로 늘리는데(최대 5분), 이 대기 상태가 ImagePullBackOff입니다.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-7d4b9c6f5d-x2k8p 0/1 ImagePullBackOff 0 3m
즉 BackOff 자체는 병명이 아니라 증상입니다. 실제 병명은 Events에 있습니다.
2. 첫 번째 명령 — describe로 에러 메시지 원문 읽기
$ kubectl describe pod api-7d4b9c6f5d-x2k8p | tail -n 15
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Failed 2m kubelet Failed to pull image "myregistry.io/team/api:v1.2.3":
rpc error: ... pull access denied ... unauthorized
Warning Failed 2m kubelet Error: ErrImagePull
Normal BackOff 1m kubelet Back-off pulling image "myregistry.io/team/api:v1.2.3"
메시지 원문에 따라 아래처럼 분기합니다. 이 표가 이 글의 지도입니다.
| Events 메시지 키워드 | 원인 계열 | 해당 섹션 |
|---|---|---|
not found, manifest unknown | 이미지 이름·태그 오류 | 3 |
unauthorized, pull access denied, authentication required | 레지스트리 인증 실패 | 4 |
toomanyrequests, 429 | Docker Hub rate limit | 5 |
i/o timeout, no such host, connection refused | 네트워크·DNS·프록시 | 6 |
x509: certificate signed by unknown authority | 사설 인증서 미신뢰 | 6 |
no matching manifest for linux/amd64 | CPU 아키텍처 불일치 | 7 |
no space left on device, DiskPressure | 노드 디스크 부족 | 8 |
3. 원인 1 — 이미지 이름·태그 오류
가장 흔하고 가장 허무한 원인입니다. CI가 만든 태그와 매니페스트에 적힌 태그가 다른 경우, 레지스트리 주소나 리포지토리 경로에 오타가 있는 경우입니다.
- 매니페스트의 이미지 문자열을 그대로 복사해 레지스트리에서 존재 여부를 확인합니다. 태그 목록은 레지스트리 웹 UI나 CLI(
aws ecr list-images,gcloud artifacts docker tags list등)로 확인합니다. - CI 파이프라인이 이미지 push에 실패했는데 배포만 진행된 경우도 있습니다. 빌드 로그에서 push 성공 여부를 확인하세요.
- 기본 레지스트리 착각도 잦습니다.
api:v1처럼 호스트 없이 쓰면 Docker Hub(docker.io/library/api:v1)로 해석됩니다. 사내 레지스트리 이미지는 반드시 전체 경로로 적어야 합니다.
4. 원인 2 — 프라이빗 레지스트리 인증 실패
pull access denied·unauthorized가 보이면 인증 문제입니다. 쿠버네티스는 노드의 도커 로그인 상태와 무관하게, Pod에 연결된 imagePullSecrets로 인증합니다.
Secret 생성과 연결
# 1) docker-registry 타입 Secret 생성 (네임스페이스 주의)
kubectl create secret docker-registry regcred \
--docker-server=myregistry.io \
--docker-username=USER \
--docker-password=PASSWORD_OR_TOKEN \
-n my-namespace
# 2) Pod 스펙에 연결
spec:
imagePullSecrets:
- name: regcred
containers:
- name: api
image: myregistry.io/team/api:v1.2.3
자주 놓치는 포인트는 다음과 같습니다.
- Secret은 네임스페이스 리소스입니다.
default에 만든 Secret은 다른 네임스페이스 Pod에서 쓸 수 없습니다. - 매번 Pod마다 붙이기 번거롭다면 네임스페이스의 ServiceAccount에 imagePullSecrets를 등록해 두는 방법이 있습니다. 해당 SA를 쓰는 모든 Pod에 자동 적용됩니다.
- ECR은 토큰이 12시간 만에 만료됩니다. 수동으로 만든 Secret은 반나절 뒤에 다시 실패합니다. EKS라면 노드 IAM Role에
AmazonEC2ContainerRegistryReadOnly를 부여해 Secret 없이 당기는 것이 정석이고, EKS 밖이라면 토큰을 주기적으로 갱신하는 CronJob이나 자격 증명 헬퍼를 사용합니다. - 비밀번호에 특수문자가 있으면 셸 이스케이프 문제로 잘못 저장되는 경우가 있습니다.
kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d로 실제 저장된 값을 검증하세요.
5. 원인 3 — Docker Hub rate limit
Docker Hub는 익명 pull 횟수를 IP 기준으로 제한합니다. NAT 뒤의 노드 여러 대가 같은 공인 IP로 pull하면 한도가 빠르게 소진되어 toomanyrequests: You have reached your pull rate limit 메시지와 함께 실패합니다.
- 단기 대응: Docker Hub 계정으로 인증하는 imagePullSecrets를 연결해 한도를 올립니다.
- 근본 대응: 사내 미러/프록시 캐시(Harbor proxy cache, registry mirror 등)를 두거나, 자주 쓰는 베이스 이미지를 사내 레지스트리로 복사해 참조를 바꿉니다.
- 재현 확인: 같은 이미지가 어떤 노드에서는 받아지고 어떤 노드에서는 실패한다면 rate limit 또는 노드별 네트워크 문제를 의심합니다.
6. 원인 4 — 네트워크·DNS·사설 인증서
i/o timeout·no such host는 노드에서 레지스트리까지의 경로 문제입니다. 이때는 쿠버네티스가 아니라 노드 입장에서 확인해야 합니다.
# 문제 노드에 접속해 직접 pull 테스트 (containerd 기준)
crictl pull myregistry.io/team/api:v1.2.3
# DNS 확인
nslookup myregistry.io
- 노드에서 직접 pull이 되는데 Pod 스케줄 시에만 실패한다면 인증·설정 문제, 노드에서도 안 되면 네트워크 문제로 좁혀집니다.
- 사내망은 프록시·방화벽에서 레지스트리 도메인과 스토리지 백엔드 도메인(예: 클라우드 레지스트리의 blob 저장소)까지 열려 있는지 확인해야 합니다. 매니페스트 조회는 되는데 레이어 다운로드에서 타임아웃 나는 패턴이 전형적입니다.
x509: certificate signed by unknown authority는 사설 CA 인증서를 노드의 컨테이너 런타임이 신뢰하지 않는 경우입니다. containerd라면/etc/containerd/certs.d/아래 레지스트리별 디렉터리에 CA를 배포하고 런타임을 재시작합니다. 노드가 많다면 DaemonSet이나 노드 프로비저닝 단계에서 자동화하는 것이 좋습니다.
7. 원인 5 — CPU 아키텍처 불일치
Apple Silicon 맥에서 docker build로 만든 이미지는 기본 arm64입니다. amd64 노드에서 당기면 no matching manifest for linux/amd64로 실패합니다. 반대로 Graviton·ARM 노드풀에 amd64 전용 이미지를 배포할 때도 같은 문제가 생깁니다.
# 멀티 아키텍처 빌드·푸시
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry.io/team/api:v1.2.3 --push .
노드풀에 아키텍처가 섞여 있다면 nodeSelector(kubernetes.io/arch)로 배포 대상을 제한하는 것도 방법입니다.
8. 원인 6 — 노드 디스크 부족
이미지를 받을 공간이 없으면 pull이 실패하고, DiskPressure 상태에서는 kubelet이 이미지 GC를 반복하며 방금 받은 이미지를 지우는 악순환이 생길 수 있습니다.
kubectl describe node에서DiskPressure컨디션을 확인합니다.- 노드에서
crictl rmi --prune또는docker system prune으로 미사용 이미지를 정리합니다. - 거대한 이미지(수 GB)가 원인이라면 멀티 스테이지 빌드로 이미지 크기를 줄이는 것이 근본 대책입니다.
9. imagePullPolicy와 latest 태그 함정
| 설정 | 동작 | 기본값이 되는 경우 |
|---|---|---|
Always | 매번 레지스트리 확인 | 태그가 latest이거나 태그 생략 시 |
IfNotPresent | 노드에 있으면 재사용 | 그 외 태그 |
Never | pull 안 함(노드에 있어야 함) | - |
latest태그는Always로 동작하므로 레지스트리 장애·rate limit에 노출되는 빈도가 높고, 무엇보다 “어떤 버전이 떠 있는지”를 알 수 없게 만듭니다. 배포에는 불변 태그(버전·커밋 해시)를 사용하세요.- 같은 태그를 덮어써서 push하는 습관은
IfNotPresent노드에서 예전 이미지가 계속 뜨는 “유령 배포”의 원인이 됩니다.
10. 흔한 실수
실수 1. Pod를 지우고 다시 만들기만 반복한다
BackOff는 재시도 대기일 뿐이므로 Pod 재생성으로는 아무것도 해결되지 않습니다. Events 메시지를 먼저 읽으세요.
실수 2. 노드에서 docker login이 되어 있으니 될 거라 생각한다
kubelet은 노드의 로그인 세션을 쓰지 않습니다. imagePullSecrets 또는 노드 IAM 권한으로 인증해야 합니다.
실수 3. Secret을 다른 네임스페이스에 만들어 둔다
describe에서 인증 오류가 계속된다면 kubectl get secret -n 해당네임스페이스로 Secret 존재부터 확인하세요.
실수 4. 처음 한 번 성공한 뒤의 실패를 다른 문제로 착각한다
“어제까지 됐는데 오늘 안 된다”면 토큰 만료(ECR 12시간), rate limit, 레지스트리 장애처럼 시간에 따라 변하는 원인부터 의심하는 것이 빠릅니다.
실전 체크리스트
kubectl describe pod의 Events에서 실패 메시지 원문을 확인했다.- 이미지 전체 경로·태그가 레지스트리에 실제로 존재하는지 확인했다.
- 프라이빗 레지스트리라면 같은 네임스페이스에 Secret이 있고 Pod(또는 SA)에 연결했다.
- 클라우드 레지스트리라면 토큰 만료·노드 IAM 권한을 확인했다.
- 문제 노드에서
crictl pull로 직접 받아지는지 테스트했다. - 이미지 아키텍처(amd64/arm64)와 노드 아키텍처가 일치하는지 확인했다.
- 배포 태그를 latest가 아닌 불변 태그로 바꿨다.
마치며
ImagePullBackOff는 원인이 다양해 보이지만, Events 메시지로 분류하면 이름·인증·네트워크·아키텍처·디스크의 다섯 계열로 수렴합니다. 진단 순서를 몸에 익혀 두면 대부분 몇 분 안에 원인을 좁힐 수 있습니다.
재발 방지까지가 트러블슈팅입니다. 불변 태그 사용, CI에서 push 성공 검증, 레지스트리 미러 구성, 토큰 자동 갱신처럼 같은 장애가 반복되지 않게 만드는 장치를 함께 남겨 두세요.
FAQ
Q1. ImagePullBackOff는 얼마나 기다리면 자동 복구되나요?
원인이 일시적(레지스트리 순단, rate limit 창 초기화)이라면 kubelet이 최대 5분 간격으로 계속 재시도하므로 자동 복구됩니다. 설정·인증 문제라면 영원히 복구되지 않으므로 기다리는 것은 의미가 없습니다.
Q2. 수정 후 즉시 다시 시도하게 하려면 어떻게 하나요?
Deployment라면 kubectl rollout restart deployment/이름으로 새 Pod를 만들면 됩니다. BackOff 대기를 기다릴 필요가 없습니다.
Q3. ErrImagePull과 InvalidImageName은 뭐가 다른가요?
InvalidImageName은 이미지 문자열 형식 자체가 잘못된 경우(대문자, 잘못된 문자 등)로, 레지스트리에 요청도 못 해 본 상태입니다. 매니페스트의 이미지 문자열을 수정해야 합니다.
Q4. 프라이빗 이미지인데 어떤 노드에서는 되고 어떤 노드에서는 실패합니다.
이미 이미지를 캐시한 노드는 IfNotPresent 정책으로 pull 없이 뜨기 때문입니다. 인증 설정이 안 된 새 노드만 실패하는 전형적인 패턴이므로, Secret·IAM 설정을 클러스터 전체 기준으로 점검하세요.
참고 공식 문서
함께 읽으면 좋은 글
'트러블슈팅 & 장애 대응' 카테고리의 다른 글
| Kubernetes Pod Pending 해결 가이드 — 리소스·Taint·Affinity·PVC 원인별 점검 (0) | 2026.08.10 |
|---|---|
| Kubernetes CrashLoopBackOff 해결 가이드 — 로그·이벤트·Probe·OOM 점검 (0) | 2026.07.26 |
| Kafka Consumer Lag 원인과 해결 방법: 운영 장애 기준으로 정리 (0) | 2026.07.15 |