/인프라/ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단
InfrastructureImagePullBackOffimagePullSecrets

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

This article is not yet available in English. Showing the original Korean version.

Pod가 ImagePullBackOff·ErrImagePull에서 멈췄나요? x509 unknown authority·unauthorized 등 Harbor·Nexus 에러를 Events로 원인별 분기하고 imagePullSecrets·사설 CA·crictl 복붙 명령어로 5분 만에 해결합니다.

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

K8s_Troubleshooting_Guide 15편

배포 매니페스트는 분명히 잘 적용됐는데, Pod가 ImagePullBackOff에서 한 발짝도 못 나가고 빨갛게 멈춰 있는 상황. 특히 Harbor, Nexus, 사내 registry 같은 사설 레지스트리를 쓰면 이 에러가 유독 자주 터집니다. 이 글은 에러 문구를 그대로 검색해 들어온 분들을 위해, kubectl describe pod Events 한 줄로 원인을 분기하고 원인별 복붙 명령어로 5분 안에 푸는 루틴을 정리했습니다.

ErrImagePull과 ImagePullBackOff는 무엇이 다른가

둘은 같은 문제의 서로 다른 단계입니다.

  • ErrImagePull: kubelet이 이미지를 당기다가 방금 실패한 상태.
  • ImagePullBackOff: 실패가 반복돼 kubelet이 재시도 간격을 지수적으로 늘리며 대기 중인 상태(최대 5분).

즉 ImagePullBackOff는 "계속 실패 중"이라는 뜻이지 새로운 원인이 아닙니다. 진짜 원인은 항상 Events의 Failed 메시지 안에 있습니다.

사설 레지스트리에서 자주 터지는 이유는 세 가지가 겹치기 때문입니다. ① 사내 PKI 사설 CA로 발급한 인증서를 노드가 신뢰하지 않음(x509), ② 인증 정보(imagePullSecrets) 누락(401), ③ K8s 1.24+에서 dockershim이 제거되고 containerd가 표준이 되면서 레지스트리 설정 위치가 /etc/docker에서 /etc/containerd/certs.d로 바뀐 점을 놓침. 여기에 Docker Hub 무료 티어 레이트리밋 강화로 사내 미러를 도입하면서 설정이 한 겹 더 늘어났죠.

1단계 — Events에서 정확한 실패 사유 식별

가장 먼저 할 일은 단 하나입니다.

Bash
kubectl describe pod <pod-name> -n <namespace> | sed -n '/Events:/,$p'

Warning Failed 줄에 나오는 단어로 원인을 분기합니다.

Events에 보이는 메시지원인분기
manifest unknown / manifest for ... not found이미지 태그·경로 오타
pull access denied ... unauthorized / 401 Unauthorized인증 누락
x509: certificate signed by unknown authority사설 CA 미신뢰
toomanyrequests: You have reached your pull rate limitDocker Hub 레이트리밋
dial tcp: lookup harbor.example.com: no such host / i/o timeoutDNS·네트워크·프록시

이 표만 옆에 띄워 두면 "이 줄이 보이면 ③으로" 식으로 곧장 점프할 수 있습니다.

① manifest unknown — 태그·경로 오타

가장 허무하지만 가장 흔합니다. 레지스트리에 실제로 그 태그가 있는지부터 확인하세요.

Bash
# 태그 목록 조회 (Harbor/Nexus의 v2 API)
curl -sk -u <user>:<pw> https://harbor.example.com/v2/app/image/tags/list

latest로 밀어 놓고 정작 푸시는 v1.2.3만 한 경우, 프로젝트 경로(app/)를 빠뜨린 경우가 대부분입니다. 매니페스트의 image: 값을 위 응답과 한 글자씩 대조하세요.

② unauthorized — imagePullSecrets 생성

401이 보이면 인증 정보가 없거나 틀린 겁니다. docker-registry 타입 secret을 만듭니다.

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

주의: --docker-server에는 스킴(https://)을 빼고 레지스트리 호스트만 적습니다. Docker Hub라면 https://index.docker.io/v1/이 표준값입니다.

생성한 secret을 Pod가 쓰게 연결하는 방법은 두 가지입니다.

방법 A — Deployment(Pod spec)에 직접 지정:

YAML
spec:
  template:
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: app
          image: harbor.example.com/app/image:v1.2.3

방법 B — ServiceAccount에 붙여 네임스페이스 전체에 적용:

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

방법 B를 추천합니다. 워크로드마다 매번 imagePullSecrets를 적지 않아도 같은 ServiceAccount를 쓰는 모든 Pod에 자동 적용되거든요. 단, 기존에 떠 있던 Pod는 재생성해야 반영됩니다.

③ x509 certificate signed by unknown authority — 사설 CA 등록

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

이 줄이 보이면 인증이 아니라 인증서 신뢰 문제입니다. 사내 PKI로 발급한 인증서를 노드의 containerd가 신뢰하지 않는 것이죠. K8s 1.24+ containerd 표준 환경에서는 certs.d 디렉터리를 사용합니다.

먼저 /etc/containerd/config.toml에 config_path가 설정돼 있는지 확인합니다.

TOML
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

그다음 호스트별 디렉터리에 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  ← 인증서 검증을 끄는 옵션. 운영 환경에서는 절대 비권장!

사내 CA의 루트 인증서를 위 ca.crt 경로에 복사한 뒤 containerd를 재시작합니다.

Bash
cp my-internal-ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt
systemctl restart containerd

구버전 Docker(dockershim) 환경이라면 경로가 다릅니다. 포트까지 디렉터리명에 포함된다는 점에 주의하세요.

Bash
mkdir -p /etc/docker/certs.d/harbor.example.com:443
cp my-internal-ca.crt /etc/docker/certs.d/harbor.example.com:443/ca.crt
systemctl restart docker

registry.mirrors(미러 호스트 지정)와 config_path(certs.d 사용)는 목적이 다릅니다. 미러는 풀 트래픽을 다른 호스트로 보내는 것이고, certs.d는 호스트별 인증서·인증 설정을 담는 곳입니다. 둘을 혼동하면 "분명 미러를 걸었는데 여전히 x509"가 나옵니다.

④ toomanyrequests — Docker Hub 레이트리밋

CODE
toomanyrequests: You have reached your pull rate limit

익명 IP당 6시간 100~200회 제한에 걸린 경우입니다. 임시방편으로는 Docker Hub 인증 secret을 붙여 한도를 늘리고, 근본 해결로는 Harbor proxy cache 프로젝트를 만들어 harbor.example.com/dockerhub-proxy/library/nginx처럼 사내 미러를 경유시키세요. 한 번 당긴 이미지는 캐시되어 외부 풀 자체가 사라집니다.

⑤ no such host / i/o timeout — 네트워크·프록시

DNS나 방화벽, 프록시 문제입니다. 노드에서 직접 끊어 봅니다.

Bash
getent hosts harbor.example.com          # DNS 해석 확인
curl -vk https://harbor.example.com/v2/  # TLS 핸드셰이크·연결 확인

containerd가 사내 프록시 뒤에 있다면 systemd drop-in에 HTTPS_PROXY/NO_PROXY를 설정하고, 사내 레지스트리는 반드시 NO_PROXY에 넣어야 합니다.

노드에서 직접 검증 — crictl로 kubelet 우회 재현

원인을 고쳤다면 kubelet을 거치지 않고 노드에서 바로 풀을 재현해 보는 게 가장 확실합니다.

Bash
# 인증·인증서·네트워크를 한 번에 검증
crictl pull --creds <user>:<pw> harbor.example.com/app/image:v1.2.3

# 캐시에 들어왔는지 확인
crictl images | grep harbor.example.com

crictl pull이 성공하면 인증서·인증·네트워크는 모두 정상이고 문제는 secret 연결이나 매니페스트 쪽이라는 뜻입니다. 반대로 crictl pull도 실패하면 노드 레벨(③⑤) 문제가 남아 있는 겁니다. 이 분리 진단이 시간을 가장 많이 아껴 줍니다.

secret이 실제로 맞게 들어갔는지 확인

imagePullSecrets를 붙였는데도 401이 계속되면, secret 내용이 실제 레지스트리와 일치하는지 디코드해서 눈으로 확인하세요.

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

출력의 auths 키 아래 server 주소가 매니페스트 image:의 호스트와 정확히 같은지, auth 값(base64 인코딩된 user:pw)이 유효한지 봅니다. 흔한 실수는 secret은 harbor.example.com인데 이미지 경로는 harbor.example.com:443처럼 포트가 붙어 둘이 매칭되지 않는 경우입니다.

실무 경험 한 토막

현장에서 가장 많이 본 함정은 "노드 한 대만 안 되는" 케이스였습니다. 새로 조인한 워커 노드에 사내 CA 배포 자동화가 빠져, 그 노드에 스케줄된 Pod만 x509로 죽었죠. Events의 노드명을 꼭 확인하고, CA·hosts.toml 배포는 Ansible이나 DaemonSet, 또는 노드 부트스트랩 스크립트로 전 노드에 강제하는 걸 권합니다. 한 대만 손으로 고치면 다음 스케일아웃 때 똑같은 사고가 재발합니다.

5분 진단 체크리스트

단계명령어/행동판단
1kubectl describe pod Events 확인메시지로 ①~⑤ 분기
2manifest unknown → 태그 v2 API 조회오타·경로 수정
3unauthorized → create secret docker-registry + imagePullSecretsSA에 붙이고 Pod 재생성
4x509 → certs.d/hosts.toml에 CA 등록 후 restart containerd전 노드 적용
5crictl pull --creds로 노드 직접 재현성공 시 secret/매니페스트 문제

재발 방지 팁: 사내 CA·certs.d는 노드 프로비저닝 자동화에 포함, Docker Hub 의존 이미지는 Harbor proxy cache로 일원화, ServiceAccount에 imagePullSecrets를 기본 탑재하세요.

다음 편(16편)에서는 kubectl top·OOMKilled — Pod가 메모리 한계로 죽을 때 진단을 다룹니다.

참고: 공식 문서

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

자주 묻는 질문 (FAQ)

Q. ErrImagePull과 ImagePullBackOff 중 어느 쪽을 봐야 하나요? A. 둘은 같은 실패의 다른 단계라 원인이 동일합니다. 무조건 kubectl describe pod의 Events Failed 메시지를 보고 분기하세요. 상태 이름 자체는 진단 정보가 아닙니다.

Q. 인증서 검증을 끄는 skip_verify를 써도 되나요? A. 급할 때 재현용으로만 쓰고 운영 환경엔 절대 남기지 마세요. 중간자 공격에 노출됩니다. 사내 CA의 루트 인증서를 ca 키로 등록하는 것이 정석입니다.

Q. secret을 만들었는데도 계속 unauthorized가 나옵니다. A. ① secret이 Pod와 같은 네임스페이스에 있는지, ② imagePullSecrets가 실제로 연결됐는지, ③ base64 디코드한 server 주소가 이미지 호스트(포트 포함 여부까지)와 일치하는지 확인하세요. 기존 Pod는 secret 적용 후 재생성해야 반영됩니다.

✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 ·

Comments

Be the first to comment.