홈/인프라/ImagePullBackOff·ErrImagePull 해결: Events로 원인 진단하고 복붙으로 끝내기
인프라KubernetesImagePullBackOff

ImagePullBackOff·ErrImagePull 해결: Events로 원인 진단하고 복붙으로 끝내기

kubectl ImagePullBackOff와 ErrImagePull을 Events 메시지 진단표로 5분 만에 특정합니다. imagePullSecrets 네임스페이스, private registry 인증, Docker Hub rate limit, ECR 토큰 만료까지 복붙 명령어로 해결하는 실전 가이드.

ImagePullBackOff·ErrImagePull 해결: Events로 원인 진단하고 복붙으로 끝내기
목차
  1. ImagePullBackOff vs ErrImagePull, 둘은 같은 문제다
  2. 1단계 진단 — Events 메시지 읽는 법
  3. 2단계 해결 — 원인별 복붙 명령어
  4. 3단계 흔한 함정과 클라우드 레지스트리
  5. 결론: 재발 방지 체크리스트
  6. 참고: 공식 문서
  7. 자주 묻는 질문 (FAQ)
  8. 출처 · 확인일 2026-10-04

ImagePullBackOff·ErrImagePull 해결: Events 메시지로 원인 진단하고 복붙으로 끝내기

K8s_Troubleshooting_Guide 17편

배포 파이프라인은 초록불인데 Pod가 또 ImagePullBackOff로 멈췄다면, PVC도 네트워크 정책도 통과한 다음 단계, 바로 kubelet이 레지스트리에서 이미지를 끌어오는 단계에서 막힌 겁니다. 이 글은 그 한 단계에만 집중합니다. kubectl describe pod의 Events 한 줄만 보고 7가지 원인 중 정답을 특정하고, 원인별 복붙 명령어로 5분 안에 복구하는 게 목표예요.

ImagePullBackOff vs ErrImagePull, 둘은 같은 문제다

먼저 헷갈리는 두 상태부터 정리하죠. 둘은 별개 문제가 아니라 같은 실패의 진행 단계입니다.

  • ErrImagePull: kubelet이 이미지 풀을 처음 시도했다가 실패한 직후 상태
  • ImagePullBackOff: 실패가 반복되어 kubelet이 재시도 간격을 지수적으로 늘리며(back-off) 대기 중인 상태

즉 ErrImagePull이 잠깐 보이다가 ImagePullBackOff로 굳습니다. 둘 다 원인은 동일하고 진단 방법도 같습니다. 상태 이름에 집착하지 말고 곧장 Events를 봅시다.

1단계 진단 — Events 메시지 읽는 법

원인의 90%는 Events 마지막 줄에 적혀 있습니다. 다음 두 명령이면 충분해요.

Bash
# 최신순으로 이벤트 모아보기
kubectl get events --sort-by=.lastTimestamp -n <namespace>

# 특정 Pod의 Events만 빠르게 추출
kubectl describe pod <pod> -n <namespace> | grep -A10 Events

Warning Failed 줄의 메시지 문구를 아래 진단표에 그대로 대조하세요.

원인 진단표

Events 메시지 (핵심 문구)원인해결 섹션
Failed to pull image ... not found / manifest unknown태그 오타·존재하지 않는 태그→ 태그 문제
unauthorized: authentication required / pull access denied인증 누락(private registry)→ 인증 누락
toomanyrequests: You have reached your pull rate limitDocker Hub rate limit→ rate limit
dial tcp: lookup ... no such host / i/o timeout레지스트리 호스트 해석·도달 실패→ 호스트 해석
failed to ... no space left on device노드 디스크 부족→ 디스크
x509: certificate signed by unknown authority노드가 사설 레지스트리 CA를 신뢰하지 않음→ x509
no match for platform in manifest태그에 노드 아키텍처용 이미지가 없음→ 플랫폼

이 표 한 장이면 "메시지 보이면 바로 여기로" 가 됩니다.

2단계 해결 — 원인별 복붙 명령어

원인 1: 태그 오타·없는 태그

not found면 가장 흔하고 가장 허무한 원인입니다. 레지스트리에 실제로 그 태그가 있는지 먼저 확인하세요.

Bash
# 매니페스트에 박힌 이미지 문자열 확인
kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[*].image}'

# 로컬에서 동일 태그 풀 가능 여부 테스트
docker pull <registry>/<image>:<tag>

오타였다면 kubectl set image deployment/<deploy> <container>=<image>:<correct-tag> 로 교체하면 끝입니다.

원인 2: 인증 누락 — imagePullSecrets

unauthorized면 자격증명이 없는 겁니다. 시크릿을 만들고 Pod에 연결하세요.

Bash
# 1) 레지스트리 로그인이 되는지 먼저 확인
docker login <registry>

# 2) docker-registry 타입 시크릿 생성 (반드시 앱과 같은 네임스페이스에!)
kubectl create secret docker-registry regcred \
  --docker-server=<registry> \
  --docker-username=<user> \
  --docker-password=<password> \
  --docker-email=<email> \
  -n <namespace>

Pod(또는 Deployment의 podTemplate)에 시크릿을 붙입니다.

YAML
spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: app
      image: <registry>/<image>:<tag>

매 워크로드마다 적기 귀찮다면 ServiceAccount에 한 번 붙여 네임스페이스 전체에 적용하세요.

Bash
kubectl patch serviceaccount default -n <namespace> \
  -p '{"imagePullSecrets":[{"name":"regcred"}]}'

원인 3: Docker Hub rate limit

Docker Hub는 이미지 pull 횟수를 6시간 단위로 제한합니다. 공식 문서(확인일 2026-10-04) 기준으로 비인증 사용자는 IPv4 주소 또는 IPv6 /64 서브넷당 6시간에 100회, 인증한 Personal 계정은 200회이고, Pro·Team·Business 요금제는 무제한입니다. 한도를 넘으면 toomanyrequests 오류가 납니다. 비인증 한도는 IP 단위라서 같은 NAT IP를 쓰는 클러스터와 CI 러너가 한도를 나눠 쓰게 되어 특히 잘 걸립니다. 대응은 두 갈래예요.

(A) 인증 계정으로 풀하기 — 인증하면 한도가 IP가 아니라 계정 기준으로 적용됩니다(Personal 200회/6시간, Pro·Team·Business 요금제는 무제한). 위의 regcred를 Docker Hub 계정으로 만들어 ServiceAccount에 붙이면 즉시 완화됩니다.

(B) 미러/풀스루 캐시로 전환 — 근본 해결책입니다. Harbor proxy cache나 ECR pull-through cache를 두고 이미지 경로만 바꾸세요.

YAML
# 기존: nginx:1.27  (Docker Hub 직접)
# 변경: <account>.dkr.ecr.<region>.amazonaws.com/dockerhub/library/nginx:1.27
image: harbor.mycorp.com/dockerhub-proxy/library/nginx:1.27

이미지 경로를 바꾸기 어렵다면 노드의 containerd에 Docker Hub 미러를 지정할 수 있습니다. config_path가 설정된 상태에서(아래 원인 6 참고) docker.io 디렉터리에 hosts.toml을 둡니다.

TOML
# /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://registry-1.docker.io"

[host."https://mirror.example.com"]
  capabilities = ["pull", "resolve"]

host에 적은 미러를 먼저 시도하고, 모두 실패하면 server로 넘어갑니다(containerd 레지스트리 설정 문서). 노드마다 설정해야 하므로 노드 이미지나 부트스트랩 스크립트에 넣어 두세요.

실무 경험담: 한 클러스터에서 야간 배치가 동시에 수십 개 Pod를 띄우자 전부 toomanyrequests로 깨진 적이 있습니다. 인증 계정만 붙여도 급한 불은 꺼졌지만, 결국 Harbor 프록시 캐시를 깔고 나서야 재발이 멈췄어요. CI와 운영이 같은 출구 IP를 쓴다면 캐시는 선택이 아니라 필수입니다.

원인 4: 레지스트리 호스트 해석 실패

lookup ... no such host는 kubelet이 레지스트리 도메인을 DNS로 풀지 못한 경우입니다. (DNS 일반 트러블슈팅은 별도 편에서 다뤘으니 여기선 레지스트리 호스트 관점만.) 노드에서 직접 확인하세요.

Bash
# 노드에 들어가 레지스트리 도메인 해석 테스트
nslookup <registry-host>
curl -v https://<registry-host>/v2/

사내 레지스트리라면 노드 /etc/hosts나 사설 DNS 등록 여부, 프라이빗 엔드포인트 라우팅을 점검합니다.

원인 5: 노드 디스크 부족

no space left on device는 이미지 레이어를 풀 디스크 공간이 없다는 뜻입니다.

Bash
# 어느 노드에 떴는지 확인
kubectl get pod <pod> -n <ns> -o wide

# 노드 디스크 압박 상태 확인
kubectl describe node <node> | grep -A5 Conditions   # DiskPressure 확인
# 노드 접속 후
df -h /var/lib/containerd   # 또는 /var/lib/docker
crictl rmi --prune          # 미사용 이미지 정리

kubelet은 이미지 파일시스템 사용률이 imageGCHighThresholdPercent를 넘으면 사용하지 않는 이미지를 지워 imageGCLowThresholdPercent까지 낮추려 합니다(가비지 컬렉션 문서). 이 정리가 실패하면 노드 이벤트에 ImageGCFailed가 남고, 여유 공간이 축출 기준 아래로 떨어지면 노드에 DiskPressure가 켜져 새 Pod 배치가 막힙니다(노드 압박 축출 문서). crictl rmi --prune은 급한 불을 끄는 용도이고, 반복된다면 노드 디스크 크기나 이미지 크기를 손봐야 합니다.

Bash
kubectl get events -A --field-selector reason=ImageGCFailed

원인 6: x509 — 사설 CA 미신뢰

CODE
failed to pull and unpack image ... x509: certificate signed by unknown authority

인증 정보 문제가 아니라 노드의 containerd가 레지스트리 인증서를 발급한 CA를 신뢰하지 않는 문제입니다. 사내 PKI로 인증서를 발급한 Harbor·Nexus 같은 사설 레지스트리에서 주로 납니다. containerd를 쓰는 노드라면 레지스트리별 설정은 certs.d 아래 hosts.toml에 둡니다.

먼저 /etc/containerd/config.toml에 config_path가 있는지 확인합니다. 섹션 이름이 containerd 버전마다 다릅니다.

TOML
# containerd 2.x (config version 3)
[plugins."io.containerd.cri.v1.images".registry]
  config_path = "/etc/containerd/certs.d"

# containerd 1.x (config version 2)
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

레지스트리 호스트 이름으로 디렉터리를 만들고 CA 인증서와 hosts.toml을 둡니다.

TOML
# /etc/containerd/certs.d/harbor.example.com/hosts.toml
server = "https://harbor.example.com"

[host."https://harbor.example.com"]
  capabilities = ["pull", "resolve"]
  ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"
  # skip_verify = true  ← 검증을 끄는 옵션. 재현 확인용으로만 쓰고 운영에 남기지 마세요
Bash
sudo cp internal-root-ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt
sudo systemctl restart containerd   # config.toml을 바꿨다면 재시작이 필요합니다

형식은 containerd hosts.md를 따릅니다. 예전 registry.mirrors·registry.configs 방식은 deprecated이므로 섞어 쓰지 말고 config_path 방식으로 정리하세요. Docker 엔진을 쓰는 노드라면 /etc/docker/certs.d/<레지스트리 호스트>/ca.crt에 CA를 두고, 포트를 쓰는 레지스트리면 디렉터리 이름에 포트까지 넣습니다(예: registry.example.com:5000, Docker 문서).

한 노드에서만 x509가 난다면: kubectl get pod -o wide로 실패한 Pod가 뜬 노드를 확인하세요. 새로 추가한 노드에 CA·hosts.toml 배포가 빠진 경우가 흔합니다. 노드 이미지, 부트스트랩 스크립트, 구성 관리 도구 중 하나로 전 노드에 같은 파일을 배포하고 새 노드 체크리스트에 넣어 두세요.

원인 7: no match for platform — 아키텍처 불일치

CODE
failed to pull and unpack image ...: no match for platform in manifest: not found

태그는 있지만 그 태그에 노드 아키텍처(amd64/arm64)용 이미지가 없을 때 납니다. arm64 노드를 섞어 쓰는 클러스터에 amd64 전용 이미지를 배포하면 흔히 보입니다.

Bash
kubectl get nodes -L kubernetes.io/arch
docker buildx imagetools inspect <registry>/<image>:<tag>   # 포함된 플랫폼 목록

해결은 둘 중 하나입니다. 멀티 플랫폼 이미지로 다시 빌드하거나(docker buildx build --platform linux/amd64,linux/arm64 ... --push, Docker 문서), 이미지가 지원하는 아키텍처의 노드에만 배치되도록 nodeSelector를 겁니다.

YAML
spec:
  nodeSelector:
    kubernetes.io/arch: amd64

3단계 흔한 함정과 클라우드 레지스트리

함정 1: secret 네임스페이스 불일치

가장 많이 밟는 지뢰입니다. 시크릿을 default에만 만들고 앱은 prod 네임스페이스에서 도는 경우, 인증은 절대 적용되지 않습니다. imagePullSecrets는 Pod와 같은 네임스페이스의 시크릿만 참조하거든요.

Bash
# prod ns에 똑같이 만들어 줘야 함
kubectl create secret docker-registry regcred ... -n prod

함정 2: latest + imagePullPolicy: Always의 디버깅 지옥

latest 태그에 Always 정책을 쓰면, 매 재시작마다 다른 이미지를 받아올 수 있어 "어제는 됐는데 오늘은 안 되는" 재현 불가 장애가 생깁니다. 권장 전략은 불변 태그(immutable tag) 입니다.

  • ❌ myapp:latest + imagePullPolicy: Always
  • ✅ myapp:1.4.2 또는 myapp:git-a1b2c3d (커밋/빌드 기반 고정 태그)

불변 태그를 쓰면 imagePullPolicy: IfNotPresent로도 안전하고, 캐시 적중률도 올라갑니다.

함정 3: secret에 저장된 레지스트리 주소가 이미지 호스트와 다름

imagePullSecrets를 붙였는데도 unauthorized가 계속되면, secret에 들어 있는 레지스트리 주소부터 확인합니다.

Bash
kubectl get secret regcred -n <namespace> \
  -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq '.auths | keys'

auths 아래 키(레지스트리 주소)가 매니페스트 image:의 호스트와 같아야 합니다. harbor.example.com:8443처럼 포트를 쓰는 레지스트리라면 포트까지 맞춰야 하고, --docker-server에는 스킴 없이 호스트만 적는 편이 안전합니다. jq 없이 전체를 출력하면 base64로 인코딩된 자격증명(auth)이 그대로 보이므로 키만 확인하세요.

노드에서 직접 재현: crictl로 kubelet 우회

고친 뒤에는 kubelet을 거치지 않고 노드에서 풀을 재현해 보면 원인이 노드 쪽인지 Kubernetes 설정 쪽인지 갈립니다.

Bash
# 노드 셸에서
sudo crictl pull --creds '<user>:<password>' harbor.example.com/app/image:v1.2.3

crictl pull이 성공하면 노드의 CA·네트워크·자격증명은 정상이고, 남은 원인은 secret 연결(네임스페이스·ServiceAccount)이나 매니페스트입니다. crictl pull도 실패하면 원인 4·6 같은 노드 수준 문제가 남아 있습니다. 자격증명이 셸 기록에 남지 않도록 실행 후 기록을 정리하세요.

클라우드별 토큰 만료 자동 갱신

매니지드 레지스트리는 토큰이 만료됩니다. 어제 잘 되던 인증이 오늘 unauthorized로 바뀌면 토큰 만료를 의심하세요.

레지스트리인증 명령만료/권장
AWS ECRaws ecr get-login-password | docker login ...토큰 12시간 만료 → IRSA로 노드/Pod에 IAM 부여가 정석. 부득이하면 CronJob이 시크릿 재생성
GCP GCR/ARgcloud auth configure-dockerWorkload Identity 권장, 토큰 단기
Azure ACRaz acr login --name <registry>AAD 토큰 단기 → AKS는 --attach-acr로 자동 인증

ECR을 정적 시크릿(regcred)으로 쓰면 12시간 뒤 무조건 깨집니다. 가능하면 IRSA / Workload Identity로 키 없는 인증을 구성하고, 그게 어렵다면 토큰 갱신 CronJob을 두세요.

YAML
# ECR 토큰 갱신 CronJob 핵심 로직 (11시간마다)
schedule: "0 */11 * * *"
# 컨테이너 command 예시
# kubectl delete secret regcred -n app --ignore-not-found
# kubectl create secret docker-registry regcred \
#   --docker-server=<acct>.dkr.ecr.<region>.amazonaws.com \
#   --docker-username=AWS \
#   --docker-password=$(aws ecr get-login-password --region <region>) -n app

결론: 재발 방지 체크리스트

  • ✅ 첫 액션은 무조건 kubectl describe pod | grep -A10 Events
  • ✅ 시크릿은 앱과 같은 네임스페이스에 생성, ServiceAccount에 붙여 전파
  • ✅ latest 금지, 불변 태그 + IfNotPresent 표준화
  • ✅ Docker Hub 직접 풀 대신 인증 계정 + 풀스루 캐시
  • ✅ ECR/AR/ACR은 IRSA/Workload Identity 우선, 정적 시크릿이면 갱신 자동화
  • ✅ 사설 레지스트리 CA·hosts.toml은 노드 프로비저닝에 넣어 새 노드에도 자동 배포
  • ✅ 아키텍처가 섞인 클러스터라면 멀티 플랫폼 이미지 또는 kubernetes.io/arch nodeSelector

다음 18편에서는 이미지를 잘 받아와 컨테이너는 떴는데 CrashLoopBackOff로 무한 재시작하는 단계 — 로그·exit code·probe로 원인을 가르는 법을 다룹니다.

참고: 공식 문서

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

자주 묻는 질문 (FAQ)

Q. ImagePullBackOff와 ErrImagePull, 뭐가 더 심각한 건가요? A. 심각도 차이가 아니라 진행 단계 차이입니다. ErrImagePull은 풀 실패 직후, ImagePullBackOff는 실패가 반복돼 재시도 대기 중인 상태예요. 원인과 해결법은 완전히 동일하니 곧장 Events 메시지를 확인하면 됩니다.

Q. imagePullSecrets를 분명히 만들었는데 계속 unauthorized가 떠요. A. 십중팔구 네임스페이스 불일치입니다. 시크릿은 Pod와 같은 네임스페이스에 있어야 참조됩니다. kubectl get secret regcred -n <앱-네임스페이스>로 존재를 확인하고, ECR이라면 토큰 12시간 만료도 함께 의심하세요.

Q. Docker Hub toomanyrequests, 인증만 붙이면 끝인가요? A. 급한 상황은 인증 계정으로 완화되지만, CI와 운영이 같은 출구 IP를 공유하면 재발합니다. Harbor proxy cache나 ECR pull-through cache 같은 미러를 두고 이미지 경로를 캐시로 돌리는 것이 근본 해결책입니다.

출처 · 확인일 2026-10-04

한도는 Docker 정책에 따라 바뀔 수 있으니 운영 설정 전에 위 문서를 다시 확인하세요.

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

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

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

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

댓글

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