Published on

K8s ImagePullBackOff 해결 - 인증·레지스트리·태그

Authors

Kubernetes에서 ImagePullBackOff 는 컨테이너를 실행하기 전, 노드가 이미지를 내려받는 단계에서 반복적으로 실패할 때 나타나는 대표적인 장애 신호입니다. 겉보기에는 단순히 “이미지 못 받음”이지만, 실제 원인은 인증 토큰 만료, 레지스트리 DNS 또는 네트워크 차단, 태그 오타, 아키텍처 불일치, Rate limit 등으로 다양합니다.

이 글은 이벤트를 기준으로 원인을 분류하고, 가장 흔한 세 축인 인증, 레지스트리, 태그를 중심으로 재현 가능한 진단 절차와 실전 해결책을 제공합니다.

관련해서 파드가 아예 뜨지 못하고 대기 중이라면 스케줄링 관점도 함께 확인해야 합니다. 필요하면 K8s Pod가 Pending? 스케줄링 실패 12가지도 같이 보세요.

1) 먼저 증상을 이벤트로 확정하기

ImagePullBackOff 는 결과 상태이고, 원인은 이벤트 메시지에 드러납니다. 가장 먼저 아래 순서로 정확한 에러 문자열을 확보하세요.

kubectl get pod -n <NAMESPACE>

kubectl describe pod -n <NAMESPACE> <POD_NAME>

# 이벤트만 빠르게 보고 싶다면
kubectl get events -n <NAMESPACE> --sort-by=.lastTimestamp

describe 출력에서 보통 아래 패턴 중 하나가 보입니다.

  • Failed to pull image ... 와 함께 unauthorized: authentication required
  • manifest unknown 또는 not found
  • dial tcp: lookup ... no such host (DNS)
  • i/o timeout, connection refused (네트워크)
  • toomanyrequests: You have reached your pull rate limit (Rate limit)

이제부터는 이 문자열을 기준으로 원인을 3갈래로 쪼개면 해결 속도가 빨라집니다.

2) 인증 문제: unauthorizeddenied 계열

2.1 가장 흔한 원인: imagePullSecrets 누락

Private registry 에서 이미지를 받는데 파드 또는 서비스어카운트에 imagePullSecrets 가 없으면 바로 실패합니다.

다음 YAML은 파드에 직접 Secret 을 붙이는 가장 단순한 방식입니다.

apiVersion: v1
kind: Pod
metadata:
  name: app
  namespace: default
spec:
  containers:
    - name: app
      image: registry.example.com/team/app:1.2.3
  imagePullSecrets:
    - name: regcred

Secret 생성은 보통 아래처럼 합니다.

kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=<USERNAME> \
  --docker-password=<PASSWORD> \
  --docker-email=<EMAIL> \
  -n default

검증 포인트:

kubectl get secret regcred -n default -o yaml

여기서 .data[.dockerconfigjson] 가 존재해야 합니다.

2.2 운영에서 권장: ServiceAccount 에 기본 주입

워크로드가 많아질수록 파드마다 imagePullSecrets 를 붙이는 것은 실수 유발 포인트입니다. 네임스페이스 기본 ServiceAccount 에 붙이면 해당 SA 를 쓰는 파드에 자동 적용됩니다.

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

적용 확인:

kubectl get sa default -n default -o yaml

2.3 토큰 만료, ECR 같은 클라우드 레지스트리 특수 케이스

ECR 은 도커 로그인 토큰이 주기적으로 만료됩니다. 토큰을 Secret 에 박아두면 시간이 지나 unauthorized 가 터질 수 있습니다.

  • EKS 에서는 보통 IRSA 기반으로 노드 또는 파드가 ECR 권한을 갖게 구성하거나
  • 외부 컨트롤러를 통해 ECR 토큰을 주기적으로 갱신합니다.

EKS 환경에서 토큰/STS 만료 계열 이슈를 함께 겪고 있다면 EKS Pod에서 STS 403 ExpiredToken 해결법도 원인 분리에 도움이 됩니다.

2.4 denied: requested access to the resource is denied

이 메시지는 인증은 되었지만 권한이 부족하거나 리포지토리 경로가 틀린 경우가 많습니다.

체크리스트:

  • 이미지 경로가 실제 레지스트리 리포지토리와 일치하는가
  • 조직/프로젝트 단위 권한이 읽기 권한을 포함하는가
  • 레지스트리에서 appapp/ 같은 경로 차이를 엄격히 구분하는가
  • 멀티 테넌시 환경에서 네임스페이스와 프로젝트 매핑이 올바른가

3) 레지스트리 접근 문제: DNS, 네트워크, 프록시

인증이 맞는데도 i/o timeout 이나 no such host 가 나오면, 파드 문제가 아니라 노드에서 레지스트리까지의 네트워크 경로가 막힌 경우가 많습니다. 이미지 풀은 기본적으로 노드의 컨테이너 런타임이 수행하므로 “파드 네트워크”보다 “노드 네트워크”를 먼저 의심해야 합니다.

3.1 DNS 문제: lookup ... no such host

노드에서 레지스트리 도메인을 해석 못 하는 상황입니다.

확인 방법:

  • CoreDNS 장애 여부
  • 노드의 /etc/resolv.conf 와 클러스터 DNS 정책
  • 사설 레지스트리 도메인이 내부 DNS 에만 있고, 노드가 그 DNS 를 못 보는 구조인지

CoreDNS 가 의심되면 다음으로 상태를 확인합니다.

kubectl -n kube-system get deploy coredns
kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs deploy/coredns

3.2 네트워크 차단: i/o timeout, connection refused

주요 원인:

  • 노드가 NAT 없이 프라이빗 서브넷에 있고, 외부 레지스트리로 나갈 경로가 없음
  • 사내 방화벽에서 레지스트리 도메인 또는 포트 443 차단
  • 프록시 환경인데 컨테이너 런타임에 프록시 설정이 누락됨

노드 레벨에서 레지스트리 통신을 확인해야 합니다. (관리형 환경이면 SSM, Bastion 등을 사용)

# 노드에서
curl -v https://registry-1.docker.io/v2/

응답이 401 Unauthorized 라면 오히려 정상입니다. 최소한 네트워크는 통과했다는 뜻입니다.

3.3 사설 CA 또는 TLS 검사 장비로 인한 인증서 오류

레지스트리가 사설 인증서이거나, 중간에 TLS 프록시가 끼어 인증서 체인이 바뀌면 이미지 풀에서 인증서 오류가 납니다. 이벤트에 x509: certificate signed by unknown authority 같은 문구가 나타납니다.

해결 방향:

  • 노드의 컨테이너 런타임이 신뢰하는 CA 번들에 사설 CA 추가
  • 레지스트리 인증서 체인을 공인 CA 로 교체
  • TLS 검사 장비를 우회하도록 네트워크 정책 조정

인증서 체인 문제는 언어 런타임에서도 동일한 형태로 터집니다. 패턴이 익숙하지 않다면 Python SSL CERTIFICATE_VERIFY_FAILED 10분 해결의 접근법을 참고해도 좋습니다.

3.4 Docker Hub Rate limit

특히 새 노드가 많이 뜨는 오토스케일 상황에서 toomanyrequests 로 터질 수 있습니다.

대응:

  • Docker Hub 유료 계정으로 인증 풀
  • 사내 프록시 레지스트리(예: Harbor)로 캐싱
  • 이미지 미러링을 ECR, GCR, ACR 등으로 이전

4) 태그 문제: manifest unknown, not found

4.1 단순 오타와 배포 순서 문제

manifest unknown 은 대부분 “그 태그가 레지스트리에 없다”는 뜻입니다.

  • CI 가 푸시하기 전에 CD 가 먼저 배포함
  • 태그 규칙이 변경되었는데 매니페스트가 업데이트되지 않음
  • latest 를 믿고 배포했는데 실제로는 다른 태그로만 푸시됨

가장 빠른 확인은 레지스트리 UI 또는 API 로 태그 존재 여부를 보는 것입니다.

4.2 멀티 아키텍처 불일치: ARM 노드에서 AMD64 이미지를 당기는 경우

EKS Graviton 같은 ARM 노드가 섞여 있는데 이미지가 linux/amd64 만 제공되면 풀 단계에서 실패할 수 있습니다. 이벤트에 no matching manifest for linux/arm64 같은 문구가 나옵니다.

해결:

  • 멀티 아키텍처 이미지로 빌드하여 매니페스트 리스트를 푸시
  • 노드풀을 아키텍처별로 분리하고 워크로드에 nodeSelector 또는 affinity 적용

멀티 아키텍처 빌드 예시(개발 머신에서):

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/team/app:1.2.3 \
  --push .

4.3 imagePullPolicy 로 인한 “태그는 같은데 내용이 바뀐” 문제

태그를 재사용하면 노드 캐시에 남아 있는 예전 이미지가 실행될 수 있습니다. 이 자체가 ImagePullBackOff 를 만들진 않지만, 디버깅을 매우 어렵게 만들어 “분명 푸시했는데 왜 반영이 안 되지” 상황을 만듭니다.

권장:

  • 운영에서는 불변 태그(예: Git SHA) 사용
  • latest 지양
  • 필요 시 imagePullPolicy: Always 를 사용하되, 레지스트리 부하와 Rate limit 을 고려
containers:
  - name: app
    image: registry.example.com/team/app:git-9f3a1c2
    imagePullPolicy: IfNotPresent

5) 실전 트러블슈팅 플로우(10분 안에 끝내기)

아래 순서대로 보면 대부분의 케이스를 빠르게 종결할 수 있습니다.

5.1 이벤트에서 에러 문자열을 한 줄로 캡처

kubectl describe pod -n <NAMESPACE> <POD_NAME>
  • unauthorized 또는 denied 이면 5.2 로
  • no such host 또는 i/o timeout 이면 5.3 로
  • manifest unknown 또는 not found 이면 5.4 로

5.2 인증 축

  • Secret 존재 확인: kubectl get secret -n <NAMESPACE>
  • 파드 또는 SA 에 연결 확인: kubectl get pod -o yaml, kubectl get sa -o yaml
  • 레지스트리 계정 권한 확인
  • 클라우드 레지스트리라면 토큰 만료/갱신 구조 점검

5.3 레지스트리 접근 축

  • 노드에서 curl -v https://<REGISTRY>/v2/ 확인
  • DNS, 방화벽, NAT, 프록시 설정 확인
  • 사설 CA 인지, 인증서 체인 문제인지 확인

5.4 태그 축

  • 레지스트리에 태그가 실제로 존재하는지 확인
  • CI 푸시와 CD 배포 순서 점검
  • ARM, AMD64 혼재 여부 확인

6) 재발 방지 체크리스트

6.1 배포 파이프라인에서 “푸시 완료”를 강제하기

  • CI 단계에서 이미지 푸시 성공 후에만 매니페스트 업데이트
  • CD 는 레지스트리 태그 존재 확인을 게이트로 추가

간단한 태그 존재 확인 예시(레지스트리별로 방식은 다름):

# 개념 예시: 실제로는 레지스트리 API/권한에 맞게 조정
# 실패하면 배포 중단
curl -fsS https://registry.example.com/v2/team/app/manifests/1.2.3 > /dev/null

6.2 Secret 과 ServiceAccount 표준화

  • 네임스페이스별로 regcred 같은 표준 이름 사용
  • 기본 ServiceAccount 에 imagePullSecrets 를 패치하거나, 전용 SA 를 만들어 워크로드가 명시적으로 사용
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: default
imagePullSecrets:
  - name: regcred

6.3 이미지 태그 정책

  • 운영: Git SHA 또는 빌드 번호 기반 불변 태그
  • 개발: 브랜치 태그를 쓰더라도 일정 기간 후 자동 정리
  • latest 는 사람에게는 편하지만, 시스템에는 불확실성을 늘립니다.

7) 자주 묻는 질문

7.1 ErrImagePullImagePullBackOff 차이는?

  • ErrImagePull 은 “이번 시도에서 풀에 실패함”
  • ImagePullBackOff 는 “반복 실패로 인해 재시도 간격을 늘리며 대기 중”

즉, ImagePullBackOff 는 같은 원인이 누적된 상태이므로 이벤트의 최초 실패 메시지를 보는 게 중요합니다.

7.2 파드에서 curl 로 레지스트리 접속되는데도 왜 실패하나?

이미지 풀은 파드가 아니라 노드의 컨테이너 런타임이 수행합니다. 파드 내부 네트워크와 노드 네트워크 경로가 다를 수 있어, 파드에서 접속된다고 노드에서 된다는 보장이 없습니다.

마무리

ImagePullBackOff 는 증상 자체보다 이벤트에 찍힌 한 줄의 에러 메시지가 핵심입니다. 그 한 줄을 기준으로 인증, 레지스트리 접근, 태그 및 매니페스트의 세 갈래로 분류하면 대부분 10분 안에 원인을 좁힐 수 있습니다.

정리하면,

  • unauthorized 는 Secret 과 권한, 토큰 만료 구조를 본다
  • no such hosttimeout 은 노드 네트워크와 DNS 를 본다
  • manifest unknown 은 태그 존재와 아키텍처 매칭을 본다

이 세 축을 표준 운영 체크리스트로 만들어두면, 새 노드가 늘어나는 순간에도 이미지 풀 장애를 크게 줄일 수 있습니다.