Published on

K8s ImagePullBackOff·ErrImagePull 9원인 해결

Authors

Kubernetes에서 ImagePullBackOff 또는 ErrImagePull은 파드가 컨테이너 이미지를 내려받는 단계에서 막혔다는 신호입니다. 증상은 단순해 보이지만 원인은 인증, 태그, 네트워크, 정책, 런타임 설정 등으로 다양합니다. 이 글은 “어디서부터 확인해야 하는지”를 사건 수사처럼 정리합니다. 핵심은 kubectl describe 이벤트와 노드 런타임 로그를 함께 보며, 원인을 9가지 범주로 빠르게 분류하는 것입니다.

먼저: 2분 안에 원인 힌트 뽑기

가장 먼저 이벤트를 봅니다. 대부분의 단서는 여기에 있습니다.

kubectl get pod -n <namespace>
kubectl describe pod <pod-name> -n <namespace>

Events 섹션에서 다음 문자열을 찾으세요.

  • Failed to pull image / rpc error / not found
  • unauthorized / authentication required
  • x509: certificate signed by unknown authority
  • i/o timeout / TLS handshake timeout
  • no basic auth credentials

그리고 실제로 어떤 이미지를 당기려 하는지 확인합니다.

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[*].image}'

노드에서만 재현되는 경우도 많습니다. 가능하면 문제가 난 노드 이름을 확인해 해당 노드에서 런타임 로그도 봅니다.

kubectl get pod <pod-name> -n <namespace> -o wide
# 출력의 NODE 컬럼 확인

EKS 등에서 자주 같이 터지는 이슈로는 메트릭/오토스케일링 관련 장애가 있는데, 이는 파드가 뜬 뒤의 문제인 경우가 많습니다. 이미지 풀 단계에서 막힌다면 우선순위는 아래 9가지 원인입니다. (참고로 런타임 이후 장애 패턴은 EKS CrashLoopBackOff - OOMKilled·Exit 137 원인과 해결도 함께 보면 분리가 쉬워집니다.)

원인 1) 이미지 이름·태그 오타 또는 레포지토리 미존재

가장 흔합니다. 이벤트에 보통 manifest unknown 또는 not found가 나옵니다.

  • 잘못된 예: myapp:lates 처럼 태그 오타
  • 레지스트리 경로 오타: registry.example.com/team/app에서 team 누락 등

해결

  1. 로컬에서 실제로 풀 되는지 확인
docker pull registry.example.com/team/app:1.2.3
  1. 쿠버네티스 매니페스트의 이미지 문자열을 정확히 고정
containers:
  - name: app
    image: registry.example.com/team/app:1.2.3
    imagePullPolicy: IfNotPresent
  1. CI에서 푸시 태그 전략을 명확히
  • latest에만 의존하지 말고, 커밋 SHA 또는 빌드 번호 태그를 병행
  • 배포 매니페스트에는 불변 태그를 사용

원인 2) Private Registry 인증 실패 (imagePullSecrets 누락)

이벤트에 unauthorized: authentication required, no basic auth credentials가 흔합니다.

해결 A: Docker registry secret 생성

kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=<user> \
  --docker-password=<password> \
  --docker-email=<email> \
  -n <namespace>

그리고 파드 또는 서비스어카운트에 연결합니다.

spec:
  imagePullSecrets:
    - name: regcred

해결 B: ServiceAccount에 기본으로 붙이기

여러 워크로드가 같은 레지스트리를 쓰면 서비스어카운트에 붙이는 편이 운영이 쉽습니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: <namespace>
imagePullSecrets:
  - name: regcred
spec:
  serviceAccountName: app-sa

점검 포인트

  • secret이 파드와 같은 네임스페이스에 있는지
  • 레지스트리 주소가 정확히 일치하는지 (https 포함 여부, 포트 포함 여부)

원인 3) ECR·GCR·ACR 등 클라우드 레지스트리 권한·토큰 갱신 문제

특히 ECR은 “노드 IAM 역할” 또는 “IRSA 기반 풀” 설계에 따라 실패 양상이 다릅니다.

  • 이벤트에 denied: User is not authorized
  • 특정 노드에서만 실패하면 노드 역할/네트워크/캐시 문제 가능

해결 (ECR 예시)

  • 노드 IAM Role에 최소 권한 부여
    • ecr:GetAuthorizationToken
    • ecr:BatchGetImage
    • ecr:GetDownloadUrlForLayer

EKS에서 노드가 ECR에 접근할 수 있는지 확인합니다.

aws ecr get-login-password --region <region> | \
  docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.com

컨트롤 플레인에서 되는 것과 “노드에서 되는 것”은 다릅니다. 노드 서브넷 라우팅, NAT, VPC 엔드포인트 설정도 함께 봐야 합니다.

원인 4) 네트워크/DNS 문제 (NAT, 프록시, 방화벽, CoreDNS)

이벤트에 i/o timeout, TLS handshake timeout, dial tcp, no such host가 자주 나옵니다.

빠른 분류

  • no such host면 DNS 가능성이 큼
  • i/o timeout이면 라우팅/NAT/방화벽/프록시 가능성이 큼

해결 체크리스트

  1. 노드가 외부로 나갈 수 있는지
  • 프라이빗 서브넷이면 NAT 게이트웨이 또는 레지스트리 VPC 엔드포인트 필요
  1. CoreDNS 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns
  1. 네트워크 정책이 egress를 막는지
  • NetworkPolicy가 기본 거부라면 레지스트리 도메인으로의 egress 허용 필요
  1. 프록시 환경
  • 노드 또는 런타임에 HTTP_PROXY, HTTPS_PROXY, NO_PROXY 설정이 필요할 수 있음

원인 5) 레지스트리 TLS 인증서 문제 (사설 CA, MITM 프록시)

이벤트에 x509: certificate signed by unknown authority가 대표적입니다.

해결

  • 올바른 인증서 체인을 레지스트리에 구성
  • 노드 런타임에 사설 CA를 신뢰하도록 추가
    • containerd 사용 시 OS 신뢰 저장소 및 containerd 설정을 함께 확인

운영 환경에서는 insecure registry로 우회하기보다, 사설 CA를 배포해 신뢰 체인을 맞추는 것이 장기적으로 안전합니다.

원인 6) 이미지 아키텍처 불일치 (ARM64 노드에 AMD64 이미지 등)

Graviton 같은 ARM 노드에서 흔합니다. 이벤트에 exec format error가 뜨는 경우도 있지만, 풀 단계에서 실패로 보이는 케이스도 있습니다.

해결

  • 멀티 아키텍처 이미지로 빌드하고 푸시
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/team/app:1.2.3 \
  --push .
  • 혹은 노드 아키텍처에 맞는 이미지 태그를 분리
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.architecture}{"\n"}{end}'

원인 7) imagePullPolicy와 태그 전략 충돌 (캐시 때문에 다른 이미지가 뜸)

엄밀히는 ImagePullBackOff의 직접 원인이라기보다, “분명 푸시했는데 안 바뀐다”로 시작해 재배포를 반복하다가 레이트리밋/인증 문제로 번지는 패턴이 많습니다.

  • IfNotPresent + latest 조합은 운영에서 사고 확률이 큼

해결

  • 불변 태그 사용 (권장)
image: registry.example.com/team/app:sha-3b1f2c7
imagePullPolicy: IfNotPresent
  • latest를 써야 한다면 Always
image: registry.example.com/team/app:latest
imagePullPolicy: Always

빌드/캐시가 꼬여서 “실제로는 레지스트리에 원하는 레이어가 없다” 같은 상황도 생깁니다. CI에서 캐시 미스로 레이어가 빠지거나, 잘못된 캐시가 재사용되는 문제는 Docker 빌드 cache miss? BuildKit 캐시 진단법도 같이 점검하면 좋습니다.

원인 8) 레지스트리 Rate Limit 또는 동시 풀 폭주

Docker Hub는 익명/무료 계정에 레이트리밋이 있습니다. 이벤트에 toomanyrequests: You have reached your pull rate limit 같은 문구가 뜹니다.

해결

  • Docker Hub 로그인 시크릿 적용
  • 사내 레지스트리 미러/프록시 캐시(예: Harbor proxy cache, Nexus) 도입
  • 노드 이미지 프리풀(daemonset)로 배포 타이밍에 풀 폭주 완화

간단한 프리풀 DaemonSet 예시:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: prepull-myapp
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: prepull-myapp
  template:
    metadata:
      labels:
        app: prepull-myapp
    spec:
      containers:
        - name: prepull
          image: registry.example.com/team/app:1.2.3
          command: ["sh", "-c", "sleep 3600"]
      terminationGracePeriodSeconds: 0

원인 9) 노드 런타임(containerd) 설정·디스크 부족·이미지 스토리지 오류

이벤트가 모호하게 rpc error로만 보이거나, 특정 노드에서만 반복되면 런타임/스토리지 계층을 의심해야 합니다.

대표 케이스

  • 노드 디스크 부족: 이미지 레이어를 쓸 공간이 없음
  • containerd content store 손상
  • /etc/containerd/config.toml의 registry mirror 설정 오류

해결

  1. 노드 디스크 확인
# 노드에 접속 후
sudo df -h
sudo du -sh /var/lib/containerd 2>/dev/null || true
  1. 불필요한 이미지 정리
# crictl 사용 예
sudo crictl images
sudo crictl rmi --prune
  1. containerd 로그 확인
sudo journalctl -u containerd -n 200 --no-pager
  1. 최후의 수단: 문제 노드 드레이닝 후 교체
kubectl cordon <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data

노드 자체가 불안정하면 이미지 풀뿐 아니라 다른 증상도 연쇄적으로 터집니다. 런타임 이후에 재시작 루프가 동반된다면 systemd 재시작 루프(StartLimitHit) 해결법 같은 관점도 함께 도움이 됩니다.

실전 트러블슈팅 플로우(요약)

아래 순서대로 보면 대부분 10분 내로 범주가 좁혀집니다.

  1. kubectl describe pod 이벤트에서 에러 문자열 확보
  2. 이미지 문자열, 태그, 레포 경로가 존재하는지 확인
  3. unauthorizedimagePullSecrets 또는 클라우드 레지스트리 권한 점검
  4. no such host면 DNS(CoreDNS), timeout이면 NAT/방화벽/프록시 점검
  5. x509면 사설 CA 신뢰 체인 정리
  6. ARM/AMD 혼재면 멀티아치 빌드
  7. 레이트리밋이면 미러/캐시/인증 도입
  8. 특정 노드만 문제면 디스크 및 containerd 로그 확인, 노드 교체

자주 쓰는 점검 커맨드 모음

# 이벤트만 빠르게 보기
kubectl get events -n <namespace> --sort-by=.metadata.creationTimestamp | tail -n 30

# 파드가 참조하는 SA 및 imagePullSecrets 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.serviceAccountName}{"\n"}{.spec.imagePullSecrets}{"\n"}'

# 디플로이먼트가 참조하는 이미지 확인
kubectl get deploy <deploy-name> -n <namespace> -o jsonpath='{.spec.template.spec.containers[*].image}{"\n"}'

마무리

ImagePullBackOffErrImagePull은 “이미지를 못 가져왔다”로 끝나지 않고, 왜 못 가져왔는지를 이벤트 문구로 분류하면 해결 속도가 급격히 빨라집니다. 운영에서 재발을 줄이려면 다음 3가지를 표준화하는 것이 효과가 큽니다.

  • 불변 태그(커밋 SHA) 기반 배포
  • 레지스트리 인증을 서비스어카운트 단위로 일원화
  • 프라이빗 서브넷은 NAT 또는 레지스트리 엔드포인트를 설계 단계에서 확정

위 9가지 체크리스트를 팀 런북에 그대로 넣어두면, 야간 장애에서 kubectl describe 한 번으로 원인 범주를 잡고 바로 조치할 수 있습니다.