- Published on
Kubernetes ImagePullBackOff·ErrImagePull 해결 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 파드가 ImagePullBackOff 또는 ErrImagePull로 멈추면, 대부분은 “이미지를 못 가져온다”는 한 문장으로 요약되지만 원인은 인증, 네트워크, DNS, 태그/매니페스트, 노드 런타임 설정 등으로 갈립니다. 중요한 건 무작정 재배포가 아니라 이벤트 메시지에서 단서를 뽑고, 가장 가능성이 높은 축부터 빠르게 제거하는 것입니다.
이 글은 현장에서 바로 적용 가능한 순서로, 확인 명령과 대표 증상, 해결 포인트를 체크리스트 형태로 정리합니다.
1) 먼저 증상 확정: 어떤 컨테이너가, 왜 실패했나
가장 먼저 “어느 파드의 어느 컨테이너가 어떤 메시지로 실패했는지”를 이벤트로 확정해야 합니다.
kubectl get pod -n <namespace>
kubectl describe pod -n <namespace> <pod-name>
kubectl get events -n <namespace> --sort-by=.lastTimestamp | tail -n 50
describe의 Events 영역에서 자주 보는 키워드는 다음과 같습니다.
Failed to pull image/ErrImagePull: 실제 pull 단계에서 실패Back-off pulling image/ImagePullBackOff: 실패가 반복되어 백오프rpc error: code = Unknown: containerd/docker 내부 에러, 네트워크/인증/프록시 등 다양manifest unknown: 태그가 없거나 아키텍처 매니페스트가 맞지 않음unauthorized: authentication required: 인증/권한 문제x509: certificate signed by unknown authority: 사설 레지스트리 TLS/CA 문제
여기서 메시지 한 줄이 이후 분기점을 결정합니다. 이벤트가 빈약하면 노드 런타임 로그까지 내려가야 합니다(아래 8절).
2) 이미지 레퍼런스(이름/태그/레지스트리) 오타부터 제거
의외로 가장 흔합니다. 특히 latest 의존, 태그 오타, 레지스트리 호스트명 오타, 리포지토리 경로 오타.
kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.spec.containers[*].image}'; echo
kubectl get deploy -n <namespace> <deploy-name> -o jsonpath='{.spec.template.spec.containers[*].image}'; echo
체크 포인트:
- 태그가 실제로 존재하는가
- 레지스트리 호스트명이 정확한가(사내 DNS 별칭 포함)
- 이미지 경로가 조직/프로젝트 단위로 바뀌지 않았는가
레지스트리에서 직접 확인할 수 있다면, 로컬에서 docker pull 또는 crane 같은 도구로도 검증합니다.
docker pull <registry>/<repo>:<tag>
3) 레지스트리 인증: imagePullSecrets 와 서비스어카운트 연결
이벤트에 unauthorized 또는 authentication required가 보이면 1차로 인증입니다.
3-1) imagePullSecrets가 파드에 붙어 있는지
kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.spec.imagePullSecrets}'; echo
kubectl get sa -n <namespace> <serviceaccount> -o jsonpath='{.imagePullSecrets}'; echo
- 파드 spec에 직접
imagePullSecrets를 넣었는지 - 또는 서비스어카운트에 기본
imagePullSecrets를 걸어두었는지
3-2) 도커 레지스트리 시크릿 형식 점검
kubectl get secret -n <namespace> <secret-name> -o yaml
- 타입이
kubernetes.io/dockerconfigjson인지 - 키가
.dockerconfigjson인지
생성 예시:
kubectl create secret docker-registry regcred \
-n <namespace> \
--docker-server=<registry> \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email>
3-3) (ECR/GCR/ACR) 클라우드 레지스트리 권한
- ECR: 노드 IAM Role 또는 IRSA로 토큰을 받아야 함
- GCR/Artifact Registry: Workload Identity 또는 노드 SA 권한
- ACR: Managed Identity 또는 SP 권한
클라우드 레지스트리는 “시크릿이 있는데도 만료/권한 부족”이 자주 나옵니다. 특히 ECR은 토큰이 주기적으로 갱신되는 구조라, 노드/애드온/컨트롤러 구성이 꼬이면 갑자기 당합니다.
4) 네트워크: 노드에서 레지스트리까지 라우팅/방화벽/프록시
인증이 맞아도 노드가 레지스트리에 연결을 못 하면 동일 증상이 납니다.
4-1) 노드가 외부로 나갈 수 있는지
- 프라이빗 서브넷인데 NAT 게이트웨이/라우팅이 빠짐
- 보안 그룹/네트워크 ACL에서
443차단 - 사내 프록시 강제 환경인데 런타임 프록시 설정이 누락
가능하다면 문제 파드가 뜨는 노드에서 직접 확인합니다(노드 접근이 어렵다면 kubectl debug node 또는 DaemonSet 디버그도 고려).
# 노드에서
curl -I https://<registry-host>
# DNS도 같이
nslookup <registry-host>
4-2) 프록시 환경에서 containerd/docker 프록시 설정
HTTP_PROXY/HTTPS_PROXY/NO_PROXY가 런타임 서비스에 적용되어야 합니다.NO_PROXY에 클러스터 내부 CIDR, 서비스 도메인(예:cluster.local)이 빠지면 내부 통신도 꼬일 수 있습니다.
환경별로 설정 위치가 다르지만, systemd drop-in으로 containerd에 프록시를 주는 패턴이 흔합니다.
# 예: systemd drop-in 파일 확인(노드)
systemctl cat containerd
5) DNS 이슈: 레지스트리 도메인 해석 실패/간헐 실패
이벤트에 dial tcp: lookup <host> on <dns-ip>: no such host 류가 보이면 DNS입니다. 특히 “간헐적으로만” 재현되면 더 골치 아픕니다.
- CoreDNS 부하/스케일 부족
- 노드 로컬 DNS 캐시 부재
- VPC DNS, 사내 DNS 포워딩, split-horizon 구성 문제
EKS라면 NodeLocal DNSCache로 간헐 실패를 줄이는 접근이 실무에서 효과가 큽니다. 관련해서는 내부 글인 EKS NodeLocal DNSCache로 DNS 간헐 실패 잡기도 함께 참고하면 좋습니다.
파드 내에서 DNS 확인(이미지 pull 단계는 파드 실행 전이라 직접 확인이 어렵지만, 같은 노드/네임스페이스에서 디버그 파드로 재현을 돕습니다):
kubectl run -n <namespace> dns-debug \
--image=busybox:1.36 \
--restart=Never \
--command -- sh -c 'nslookup <registry-host>; wget -S -O- https://<registry-host> || true'
6) TLS/사설 레지스트리: 인증서 체인과 insecure 설정
사내 Harbor, Nexus, Artifactory 등을 쓰면 다음이 흔합니다.
x509: certificate signed by unknown authority- 중간 인증서 누락
- 레지스트리 도메인과 인증서 SAN 불일치
해결 방향:
- 노드 OS 또는 containerd가 신뢰하는 CA 번들에 사내 CA 추가
- 레지스트리 인증서 체인을 올바르게 구성
- 정말 불가피할 때만
insecure레지스트리 설정(보안 리스크 큼)
containerd는 레지스트리별 설정을 hosts.toml로 두는 패턴이 많습니다.
# 예: containerd registry 설정 경로(환경에 따라 다름)
ls -al /etc/containerd/certs.d/
ls -al /etc/containerd/certs.d/<registry-host>/
7) 아키텍처/매니페스트 문제: manifest unknown, no matching manifest
노드가 ARM64인데 이미지가 AMD64만 있거나(또는 반대), 멀티아치 매니페스트가 잘못 올라가면 발생합니다.
이벤트 예:
no matching manifest for linux/arm64...manifest unknown...
체크:
- 노드 아키텍처 확인
kubectl get node -o wide
kubectl get node <node-name> -o jsonpath='{.status.nodeInfo.architecture}'; echo
- 이미지가 멀티아치인지 확인(로컬에서 확인 가능한 경우)
docker manifest inspect <registry>/<repo>:<tag>
해결:
- 멀티스테이지 빌드와
buildx로 멀티아치 푸시 - 특정 노드풀에만 스케줄되도록
nodeSelector/affinity로 아키텍처를 분리
8) 노드 런타임(containerd/docker) 상태와 디스크, 이미지 캐시
이벤트가 애매하게 rpc error로 뭉뚱그려지면 노드 런타임을 봐야 합니다.
8-1) containerd 상태/로그
# 노드에서
systemctl status containerd
journalctl -u containerd --since "30 min ago" | tail -n 200
8-2) 디스크 부족
이미지 pull은 디스크를 많이 씁니다. no space left on device는 매우 전형적입니다.
# 노드에서
df -h
sudo du -sh /var/lib/containerd 2>/dev/null || true
정리 예시(환경에 따라 명령이 다릅니다):
# containerd 환경에서 이미지/스냅샷 정리 도구 예시
crictl images
crictl rmi <image-id>
또는 kubelet eviction 설정이 너무 느슨해 디스크가 꽉 차기 전까지 버티는 경우도 있어, evictionHard/imageGCHighThresholdPercent 등을 점검합니다.
9) imagePullPolicy와 태그 전략: IfNotPresent가 만든 함정
ImagePullBackOff 자체는 “못 당겨옴”이지만, 반대로 “당겨오지 않아서” 문제가 생기는 경우도 운영에서 자주 봅니다.
imagePullPolicy: IfNotPresent- 태그를
latest또는 고정 태그로 계속 덮어쓰기
이 조합이면 노드에 캐시된 오래된 이미지가 계속 실행됩니다. 장애 대응 중에는 혼선을 키웁니다.
권장:
- 태그는 커밋 SHA 등 불변값 사용
latest는 개발 환경 외 지양- 꼭 필요하면
imagePullPolicy: Always를 명시
spec:
containers:
- name: app
image: <registry>/<repo>:<git-sha>
imagePullPolicy: IfNotPresent
10) 레이트 리밋과 동시 풀: Docker Hub, 공용 레지스트리 제한
특정 시간대에만 대량으로 터지면 레이트 리밋/쓰로틀링도 의심합니다.
- Docker Hub는 익명/무료 계정에 제한이 존재
- 노드가 동시에 스케일아웃되며 동일 이미지를 한꺼번에 pull
대응:
- 사내 레지스트리로 미러링
- 프리풀(pre-pull) DaemonSet 운영
- 레지스트리 인증 적용(익명 pull 회피)
간단한 프리풀 DaemonSet 예시:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: image-prepull
namespace: kube-system
spec:
selector:
matchLabels:
app: image-prepull
template:
metadata:
labels:
app: image-prepull
spec:
containers:
- name: prepull
image: <registry>/<repo>:<tag>
command: ["sh", "-c", "sleep 3600"]
terminationGracePeriodSeconds: 0
11) 리소스/노드 상태: 이미지 풀 전에 노드가 불안정한 경우
간혹 원인이 “이미지”가 아니라 “노드 상태 불안정”인 경우도 있습니다.
- 노드가
NotReady에 가까운 상태로 flapping - CNI 문제로 네트워크가 순간적으로 끊김
- kubelet이 압박 상태(
MemoryPressure,DiskPressure)
kubectl describe node <node-name>
kubectl get node <node-name> -o jsonpath='{.status.conditions}'; echo
배포 파이프라인에서 멈추는 형태로 보이면, 배포 관점에서의 디버깅도 같이 보면 좋습니다. 예를 들어 GitHub Actions에서 쿠버네티스 배포가 진행 상태에 멈출 때의 관찰 포인트는 GitHub Actions Kubernetes 배포 stuck in Progress 디버깅에 정리해 두었습니다.
12) 빠른 결론을 위한 “우선순위 체크리스트”
아래 순서대로 보면 대부분 10분 내에 원인 범주를 좁힐 수 있습니다.
kubectl describe pod이벤트에서 에러 문자열을 정확히 복사해 분류- 이미지 이름/태그/레지스트리 경로 오타 제거
unauthorized면imagePullSecrets와 서비스어카운트 연결 확인lookup/타임아웃이면 노드에서 DNS와443연결 확인x509면 사설 CA/체인/SAN 점검no matching manifest면 노드 아키텍처와 멀티아치 매니페스트 확인rpc error면 containerd 상태/로그, 디스크 용량, 프록시 설정 확인- 특정 시간대/스케일아웃에만 발생하면 레이트 리밋/동시 pull 완화
13) 현장에서 자주 쓰는 “원인별 이벤트 메시지” 예시
아래는 트러블슈팅 회의에서 커뮤니케이션을 빠르게 하는 데 도움이 되는 매핑입니다.
unauthorized: authentication required- 시크릿 누락, 잘못된 계정, ECR 권한/토큰 갱신 문제
dial tcp: lookup <registry-host> ... no such host- DNS 문제(CoreDNS, 포워딩, NodeLocal DNSCache 부재)
net/http: request canceled while waiting for connection- 네트워크 경로/NAT/방화벽/프록시
x509: certificate signed by unknown authority- 사설 CA 미신뢰, 체인 누락
no matching manifest for linux/arm64- 이미지 아키텍처 불일치, 멀티아치 미지원
마무리: “이벤트 한 줄”을 중심으로 수사하라
ImagePullBackOff는 결과이고, 원인은 이벤트에 이미 드러나 있습니다. kubectl describe의 이벤트를 기준으로 인증, 네트워크/DNS, TLS, 아키텍처, 런타임/디스크 순으로 가지치기하면 재시작/재배포로 시간을 버리지 않고 빠르게 복구할 수 있습니다.
DNS가 의심될 때는 특히 간헐 장애로 번지기 쉬우니, EKS 환경이라면 EKS NodeLocal DNSCache로 DNS 간헐 실패 잡기처럼 구조적으로 흔들림을 줄이는 개선도 함께 고려해 보세요.