Published on

Kubernetes ImagePullBackOff - 401·TLS·DNS 원인별 해결

Authors

서버가 멀쩡한데 Pod만 ImagePullBackOff로 멈추면, 대부분은 “이미지 자체”가 아니라 레지스트리로 가는 경로(인증·TLS·DNS·네트워크) 어딘가가 깨진 경우입니다. 이 글은 증상을 원인별로 분류하고, 현장에서 바로 써먹을 수 있는 점검 순서와 수정 예시를 제공합니다.

참고로 ImagePullBackOff는 “이미지 pull이 반복 실패했고, kubelet이 백오프를 걸었다”는 상태입니다. 즉, 먼저 실패 원인 메시지를 정확히 읽는 게 핵심입니다.

1) 가장 먼저 볼 것: 이벤트와 실제 에러 문자열

아래 두 가지를 먼저 확보하면, 10분 안에 원인 범주(401/TLS/DNS/기타)가 거의 결정됩니다.

# Pod 이벤트(가장 중요)
kubectl describe pod -n <namespace> <pod-name>

# 컨테이너 상태만 간단히
kubectl get pod -n <namespace> <pod-name> -o wide
kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.status.containerStatuses[*].state.waiting.message}'

Events 섹션에서 흔히 보이는 패턴은 다음과 같습니다.

  • Failed to pull image ...: rpc error: code = Unknown desc = Error response from daemon: unauthorized: authentication required
    • 인증(401/403) 계열
  • x509: certificate signed by unknown authority / tls: handshake failure
    • TLS/CA/프로토콜 계열
  • dial tcp: lookup ... on ...: no such host / i/o timeout
    • DNS/네트워크 계열

이 글은 이 3가지를 중심으로 해결합니다.

2) 401/403 인증 문제: ImagePullSecret, 토큰, 권한

2-1. 가장 흔한 원인: imagePullSecrets 누락 또는 네임스페이스 불일치

Private registry를 쓰는데 Pod spec에 imagePullSecrets가 없거나, Secret이 다른 네임스페이스에 있으면 바로 401로 떨어집니다.

kubectl get secret -n <namespace>
kubectl get secret -n <namespace> <secret-name> -o yaml

Pod 또는 ServiceAccount에 imagePullSecrets를 연결합니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: myns
imagePullSecrets:
  - name: regcred

Deployment에서 ServiceAccount를 사용:

spec:
  template:
    spec:
      serviceAccountName: app-sa
      containers:
        - name: app
          image: registry.example.com/team/app:1.2.3

2-2. docker-registry Secret 생성/갱신 방법

레지스트리 인증이 바뀌었는데 Secret이 옛날 값이면 계속 401이 납니다. 아래로 재생성하는 게 가장 확실합니다.

kubectl create secret docker-registry regcred \
  -n myns \
  --docker-server=registry.example.com \
  --docker-username='<username>' \
  --docker-password='<password-or-token>' \
  --docker-email='dev@example.com' \
  --dry-run=client -o yaml | kubectl apply -f -
  • GitHub Container Registry는 보통 username은 GitHub ID, password는 PAT 토큰(패키지 읽기 권한 포함)입니다.
  • ECR/GCR/ACR은 토큰이 만료되는 방식이므로, 토큰 갱신 자동화가 필요할 수 있습니다.

2-3. “로그인은 되는데 pull이 안 됨” 권한 문제

개발 PC에서는 pull이 되는데 클러스터에서는 안 된다면, 대개 다음 중 하나입니다.

  • 토큰 스코프 부족(예: read 권한 없음)
  • 레포가 private인데 조직 정책으로 차단
  • 레지스트리 URL이 미묘하게 다름(예: registry.example.com vs registry.example.com:443)

이때는 Secret의 --docker-server이미지 레퍼런스의 registry 호스트와 정확히 일치하는지 확인하세요.

권한 추적 방식은 IAM 403 분석과 유사합니다. 이벤트에 unauthorized가 찍힐 때는 “인증 값이 없거나 틀림”, denied/forbidden이면 “권한 부족”인 경우가 많습니다. 비슷한 접근으로 권한을 좁혀가는 방법은 이 글도 참고할 만합니다: AWS IAM AccessDenied 403, 정책 시뮬레이터로 추적

2-4. 노드 런타임별 주의점: containerd 크리덴셜 캐시

containerd 환경에서 Secret을 바꿨는데도 계속 실패하면, 다음을 의심합니다.

  • 기존 Pod가 같은 노드에서 재시도하며 이전 상태를 끌고 감
  • 레지스트리 측에서 토큰/세션 정책이 꼬임

가장 단순한 복구는 Pod를 새로 띄우고(ReplicaSet 롤링), 필요하면 문제가 난 노드에서 런타임을 재시작해 캐시를 비우는 것입니다(운영 정책에 따라 신중히).

3) TLS 문제: 사설 CA, 미들박스, 프로토콜 불일치

TLS는 x509 또는 handshake 류 메시지로 드러납니다. 특히 사내 레지스트리, 프록시, SSL 가로채기(inspection) 환경에서 자주 발생합니다.

3-1. x509: certificate signed by unknown authority

의미는 단순합니다. 노드의 컨테이너 런타임이 레지스트리 인증서를 신뢰하지 않는다 입니다.

해결은 크게 두 가지입니다.

  1. 레지스트리 인증서를 공인 CA로 교체
  2. 노드에 사설 CA를 신뢰하도록 설치

containerd 기준으로는 보통 certs.d에 CA를 배치합니다(배포판/설치 방식에 따라 경로가 다를 수 있음).

# 예시: 호스트에 CA 배치(경로는 환경마다 다름)
# /etc/containerd/certs.d/registry.example.com/ca.crt

sudo mkdir -p /etc/containerd/certs.d/registry.example.com
sudo cp ca.crt /etc/containerd/certs.d/registry.example.com/ca.crt

sudo systemctl restart containerd

kubelet 이벤트가 그대로라면, 노드 전체에 반영되었는지 확인해야 합니다. 노드가 여러 대면 특정 노드에서만 실패하는 경우가 흔합니다.

3-2. tls: handshake failure / remote error: tls

이건 더 복잡합니다.

  • 레지스트리가 TLS 1.2 이상만 허용하는데, 중간 프록시가 구버전
  • SNI 기반 가상호스팅인데, 중간 장비가 SNI를 깨뜨림
  • 회사 프록시가 TLS를 가로채며 다른 인증서를 내밈

진단 포인트:

  • 노드에서 직접 레지스트리로 TLS 연결을 재현합니다.
# 노드에 접속해서 실행(또는 debug pod로 실행)
# openssl이 없다면 배포판에 맞게 설치

openssl s_client -connect registry.example.com:443 -servername registry.example.com -showcerts

여기서 인증서 체인, 만료, CN/SAN, 중간 인증서 누락 등을 확인합니다.

3-3. 최후의 수단: insecure registry는 신중히

운영에서 insecure 설정은 보안상 비용이 큽니다. 그래도 내부망 테스트/임시 복구로 쓰는 경우가 있어, 개념만 짚겠습니다.

containerd는 config.toml에서 registry 설정으로 insecure를 줄 수 있지만, 설치 방식에 따라 설정 블록이 다릅니다. 적용 전에는 반드시 변경 관리와 롤백 플랜을 준비하세요.

4) DNS 문제: no such host, CoreDNS, 노드 resolv.conf

DNS 문제는 메시지가 비교적 명확합니다.

  • lookup registry.example.com on 10.96.0.10:53: no such host
  • dial tcp: lookup ...: i/o timeout

여기서 중요한 포인트는 “누가 DNS를 하느냐”입니다.

  • 이미지 pull은 보통 노드의 런타임이 수행합니다.
  • 따라서 Pod 내부 DNS만 정상이어도, 노드 레벨 DNS가 깨지면 pull은 실패할 수 있습니다.

4-1. CoreDNS 상태 점검

kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=200
kubectl get configmap -n kube-system coredns -o yaml

CoreDNS가 CrashLooping이면 원인(업스트림 DNS, 설정 오류, 리소스 부족)을 먼저 해결해야 합니다.

4-2. 노드에서 직접 DNS 확인

노드에 접속 가능하다면 아래가 가장 빠릅니다.

cat /etc/resolv.conf

# dig/nslookup이 없으면 getent로 대체 가능
getent hosts registry.example.com
  • /etc/resolv.conf가 엉뚱한 네임서버를 가리키는지
  • 방화벽이 53/udp를 막는지
  • 사내 DNS가 split-horizon인데 외부망에서 내부 도메인을 못 푸는지

를 확인합니다.

4-3. 클러스터 내부에서 재현: debug pod

노드 접근이 어렵다면, 최소한 클러스터 안에서 DNS를 재현해 범위를 좁힙니다.

kubectl run dns-debug -n myns --rm -it \
  --image=busybox:1.36 \
  --restart=Never -- sh

# 컨테이너 안에서
nslookup registry.example.com
wget -S -O - https://registry.example.com/v2/ 2>&1 | head

여기서도 안 되면 CoreDNS 또는 네트워크 정책 이슈일 확률이 큽니다.

5) 네트워크/프록시/방화벽: DNS는 되는데 i/o timeout

DNS는 되는데 i/o timeout이면, 대개 443/tcp 경로 문제입니다.

  • 노드 서브넷에서 레지스트리로 라우팅이 없음
  • 보안그룹/방화벽에서 egress 차단
  • 프록시 필수 환경인데 노드/런타임에 프록시 미설정

5-1. 노드 egress 확인

# 노드에서
curl -vk https://registry.example.com/v2/

# 연결만 확인
nc -vz registry.example.com 443

응답이 401 Unauthorized로 오면 오히려 좋은 신호입니다. 네트워크와 TLS는 통과했고, 이제 인증만 보면 됩니다.

5-2. 프록시 환경에서의 kubelet/containerd 설정

기업망에서 프록시가 필수인데 노드 서비스에 환경변수가 누락되면, pull이 실패합니다. systemd drop-in으로 설정하는 방식이 흔합니다.

sudo systemctl edit containerd
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=127.0.0.1,localhost,.cluster.local,10.0.0.0/8,registry.example.com"
sudo systemctl daemon-reload
sudo systemctl restart containerd

프록시/서비스 재시작이 얽히면 원인 파악이 어려워집니다. 서비스 재시작 루프나 로그 확인 관점은 아래 글의 진단 순서가 그대로 도움이 됩니다: systemd 서비스 재시작 루프 진단 - 로그·유닛·쉘

6) 체크리스트: 원인별로 “가장 빨리” 좁히는 방법

6-1. 1분 컷 분류

  • 이벤트에 unauthorized / authentication required
    • imagePullSecrets 및 Secret 내용/서버명 일치부터 확인
  • 이벤트에 x509 / handshake
    • 노드 런타임이 신뢰하는 CA, 프록시 TLS 가로채기 여부 확인
  • 이벤트에 no such host / lookup ... timeout
    • 노드 DNS 및 CoreDNS 상태 확인
  • 이벤트에 i/o timeout
    • 443 egress, 라우팅, 프록시, 방화벽 확인

6-2. 재현 가능한 최소 커맨드 세트

# 1) 이벤트
kubectl describe pod -n <namespace> <pod>

# 2) 같은 네임스페이스에 regcred 존재?
kubectl get secret -n <namespace>

# 3) 클러스터 내부 DNS/HTTPS 재현
kubectl run net-debug -n <namespace> --rm -it \
  --image=busybox:1.36 --restart=Never -- sh

# 4) 노드에서 curl로 /v2/ 확인(가능하다면)
# curl -vk https://registry.example.com/v2/

7) 자주 하는 실수들

  • Secret을 만들었는데 Deployment가 다른 네임스페이스에 있음
  • image: registry.example.com/team/app:tag 인데 Secret의 --docker-serverhttps://registry.example.com로 넣음(문자열 불일치)
  • 레지스트리 인증서를 갱신했는데, 노드에 배포된 CA/체인이 그대로임
  • Pod DNS만 보고 “DNS 정상”이라고 결론냄(실제 pull은 노드 런타임이 수행)
  • 프록시 환경에서 NO_PROXY에 레지스트리를 빼먹어 프록시로 보내다 실패

8) 마무리: 401을 만들고 401을 받아라

ImagePullBackOff 트러블슈팅의 목표는 “정상 pull”이 아니라, 우선 정상 연결을 의미하는 응답을 받는 것입니다.

  • curl -vk https://.../v2/ 결과가 401이면
    • 네트워크/TLS는 통과했고, 이제 인증만 해결하면 됩니다.
  • 반대로 x509, handshake, no such host, timeout이면
    • 인증 이전 단계에서 막힌 것이므로, 원인 범주를 잘못 잡고 Secret만 만지작거리지 않게 됩니다.

운영 환경에서는 “특정 노드에서만 실패”하는 패턴이 특히 흔하니, Pod가 스케줄된 노드(kubectl get pod -o wide)를 꼭 확인하고 노드 단위로 DNS/CA/프록시 설정 편차가 없는지 점검하세요.