ImagePullBackOff — 이미지를 가져오지 못할 때

컨테이너가 시작조차 못 하는 ImagePullBackOff와 ErrImagePull을 다룹니다. 이미지 풀 흐름과 실패 지점, Events 메시지로 원인을 가르는 법, 그리고 이름·태그 오타, 프라이빗 레지스트리 인증, Docker Hub 레이트 리밋, 아키텍처 불일치, imagePullSecrets 설정까지 단골 원인별 진단과 해결책을 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 다룬 CrashLoopBackOff가 “컨테이너가 실행은 됐는데 자꾸 죽는” 문제였다면, ImagePullBackOff는 그보다 한 단계 앞에서 막히는 문제다. 컨테이너가 단 한 번도 실행되지 못한다. kubelet이 이미지를 레지스트리에서 받아오는 데 실패했기 때문이다. CrashLoop처럼 앱 로그를 봐도 아무것도 없다 — 애초에 앱이 돌지 않았으니까. 그래서 진단의 출발점도 다르다. 로그가 아니라 Events가 거의 모든 답을 쥐고 있다.

ImagePullBackOff와 ErrImagePull

이 둘은 같은 문제의 두 단계다. kubelet이 이미지를 받으려다 실패하면 먼저 ErrImagePull 상태가 된다. 곧바로 다시 시도하지 않고, CrashLoop과 마찬가지로 지수 백오프로 재시도 간격을 늘려가며 대기하는데, 이 대기 상태가 ImagePullBackOff다. 즉 ErrImagePull은 “방금 풀에 실패했다”, ImagePullBackOff는 “실패가 반복돼서 재시도를 미루는 중”이라는 뜻이다.

이미지 풀 흐름과 실패 지점

CrashLoop과 결정적으로 다른 점은, 문제가 컨테이너 실행 단계가 아니라 이미지를 가져오는 단계에 있다는 것이다. 그래서 kubectl logs는 거의 쓸모가 없고, kubectl describe의 Events에 적힌 풀 에러 메시지가 진단의 전부다.

# 가장 먼저 — Events의 풀 에러 메시지를 읽는다
kubectl describe pod web-7d9f-abcde | grep -A5 Events

Events 메시지로 원인을 가른다

다행히 레지스트리가 돌려주는 에러 메시지는 꽤 정직하다. Events에 찍힌 문구만 봐도 어느 갈래인지 거의 정해진다.

ImagePullBackOff 단골 원인

1) 이름·태그 오타 (manifest unknown / not found)

가장 흔하고 가장 허무한 원인이다. 이미지 이름을 잘못 쓰거나, 존재하지 않는 태그를 가리킨다. myapp:v1.2.3이라고 했는데 실제로는 v1.2.0까지만 푸시됐다든가, :latest를 썼는데 그 태그가 갱신되지 않았다든가 하는 경우다.

# 매니페스트의 이미지 문자열을 정확히 확인
kubectl get pod web-7d9f-abcde -o jsonpath='{.spec.containers[*].image}'

:latest는 디버깅을 어렵게 만든다. 어느 시점의 이미지인지 추적이 안 되고, 노드에 캐시된 옛 이미지를 쓰는 함정도 있다. 운영에서는 항상 명시적 태그나 다이제스트(@sha256:...)를 쓰는 편이 안전하다.

2) 프라이빗 레지스트리 인증 실패 (unauthorized / denied)

공개 이미지는 잘 받는데 사내 레지스트리 이미지에서만 막힌다면 인증 문제다. kubelet에게 레지스트리 자격증명을 알려주는 imagePullSecrets가 없거나 잘못된 것이다. docker-registry 타입 시크릿을 만들고 Pod(또는 ServiceAccount)에 연결한다.

# 레지스트리 자격증명 시크릿 생성
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=ci-bot \
  --docker-password='********'
spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: web
      image: registry.example.com/team/web:1.4.2

ServiceAccount에 imagePullSecrets를 붙여두면 그 SA를 쓰는 모든 Pod가 자동으로 자격증명을 물려받아, 매번 Pod 스펙에 적지 않아도 된다.

3) Docker Hub 레이트 리밋 (toomanyrequests)

익명 풀에는 IP당 시간당 풀 횟수 제한이 걸린다. CI나 오토스케일로 노드가 동시에 같은 이미지를 당기면 금세 한도에 닿아 toomanyrequests가 뜬다. 인증된 계정으로 풀하거나, 사내 미러·캐시 레지스트리(pull-through cache)를 두어 외부 풀 자체를 줄이는 것이 근본 해법이다.

4) 아키텍처 불일치·네트워크 차단

amd64로 빌드한 이미지를 arm64 노드(예: Graviton, Apple Silicon 기반 노드)에서 받으려 하면 no match for platform이 뜬다. 멀티아키텍처 이미지로 빌드하거나 노드 아키텍처에 맞는 태그를 지정해야 한다. 또 노드가 프록시·방화벽 뒤에 있어 레지스트리에 아예 도달하지 못하면 timeout이 난다 — 이때는 노드에서 레지스트리로의 네트워크 도달성부터 확인한다.

빠른 검증 — 노드에서 직접 풀해보기

원인이 모호할 때는 문제의 노드에 들어가 같은 이미지를 직접 당겨보면 단숨에 갈린다. 인증 문제인지, 네트워크 문제인지, 이름 문제인지 에러 메시지가 더 구체적으로 나온다.

# 노드 디버그 컨테이너에서 도달성 확인
kubectl debug node/worker-1 -it --image=busybox \
  -- wget -qO- https://registry.example.com/v2/ || echo unreachable

정리 — 그리고 다음

ImagePullBackOff는 컨테이너가 실행 전에 막히는 문제라, 로그가 아니라 describe의 Events가 핵심이다. 레지스트리가 돌려주는 메시지(manifest unknown / unauthorized / toomanyrequests / no match for platform)가 원인을 거의 그대로 알려주므로, 그 문구를 읽고 해당 갈래의 조치를 취하면 된다. 이미지 이름과 태그를 다시 확인하고, 프라이빗 레지스트리라면 imagePullSecrets를 점검하는 것이 출발점이다. 다음 글에서는 컨테이너가 메모리 한도를 넘겨 강제 종료되는 OOMKilled를 깊게 파고든다.


지난 글: CrashLoopBackOff — 컨테이너가 계속 재시작될 때

다음 글: OOMKilled — 메모리 한도를 넘어 강제 종료될 때


읽어주셔서 감사합니다. 😊