Published on

AKS ImagePullBackOff - ACR 권한·DNS·프록시 진단

Authors

AKS에서 ImagePullBackOff는 “컨테이너 이미지 다운로드가 실패했다”는 결과만 보여줄 뿐, 원인은 크게 세 갈래(권한, DNS, 프록시/네트워크)로 나뉩니다. 특히 Azure 환경에서는 ACR 인증 방식(Managed Identity, imagePullSecrets), 프라이빗 DNS, 방화벽/프록시 정책이 서로 얽히면서 같은 증상이 다른 원인으로 나타나는 경우가 많습니다.

이 글은 다음 목표로 구성합니다.

  • 이벤트 메시지로 실패 지점을 빠르게 분류
  • ACR 권한(클러스터/노드/워크로드) 문제를 확실히 확인
  • DNS 해석과 네트워크 경로(프록시, 방화벽, 프라이빗 엔드포인트)를 단계적으로 검증
  • “고쳤는지”를 재현 가능한 방법으로 최종 확인

관련해서 네트워크/DNS 이슈를 분리 진단하는 사고방식은 AWS 사례지만 구조가 비슷합니다: EKS Pod→S3 504 타임아웃 - VPC 엔드포인트·NAT·DNS 진단


1) 먼저: 이벤트 메시지로 원인 분류하기

가장 먼저 해야 할 일은 “어디에서 막혔는지”를 이벤트로 확인하는 겁니다.

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

Events 섹션에서 자주 보이는 패턴은 아래와 같습니다.

  • 인증/권한 계열
    • unauthorized: authentication required
    • pull access denied
    • denied: requested access to the resource is denied
  • DNS/이름해석 계열
    • dial tcp: lookup <registry> on <dns-ip>:53: no such host
    • Temporary failure in name resolution
  • 네트워크/프록시/방화벽 계열
    • i/o timeout
    • context deadline exceeded
    • connect: connection refused
    • TLS 계열: x509: certificate signed by unknown authority

여기서 핵심은 “이미지 이름이 맞는데도 인증이 안 되는지”, “레지스트리 도메인을 못 찾는지”, “찾았는데 연결이 안 되는지”로 분기하는 것입니다.

추가로 노드가 실제로 어떤 이미지를 받으려 했는지(태그/다이제스트 포함)도 확인하세요.

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

2) ACR 권한 문제 진단: AKS가 ACR에 로그인할 수 있는가

AKS에서 ACR을 당겨오는 대표 방식은 다음 둘입니다.

  • AKS와 ACR을 az aks update --attach-acr로 연결(일반적으로 클러스터/노드의 Managed Identity에 AcrPull 부여)
  • 네임스페이스/서비스어카운트에 imagePullSecrets로 Docker registry secret을 붙임

둘 중 무엇을 쓰는지부터 확인해야 합니다.

2-1) 이미지가 ACR인지 먼저 확인

ACR은 보통 이런 형태입니다.

  • myacr.azurecr.io/myrepo/myapp:tag

레지스트리 호스트가 ACR이 아니라 Docker Hub, GHCR, 다른 사설 레지스트리라면 이후 권한/네트워크 체크가 달라집니다.

2-2) AKS가 ACR에 attach 되어 있는지 확인

az aks show -g <rg> -n <aks> --query "{acrProfile:acrProfile, identity:identity, kubeletIdentity:identityProfile.kubeletidentity}" -o jsonc

환경에 따라 kubeletidentity(kubelet이 이미지 풀에 사용하는 ID)가 따로 존재합니다. 이 ID가 실제로 AcrPull을 갖고 있는지 확인합니다.

2-3) ACR에 AcrPull 권한이 있는지 확인

먼저 ACR 리소스 ID를 얻습니다.

ACR_ID=$(az acr show -g <acr-rg> -n <acr-name> --query id -o tsv)

그 다음 kubelet identity(또는 클러스터 identity)의 principal ID를 확인하고 role assignment를 조회합니다.

KUBELET_PRINCIPAL_ID=$(az aks show -g <rg> -n <aks> --query identityProfile.kubeletidentity.objectId -o tsv)

az role assignment list --assignee $KUBELET_PRINCIPAL_ID --scope $ACR_ID -o table

AcrPull이 없다면 붙입니다.

az role assignment create \
  --assignee $KUBELET_PRINCIPAL_ID \
  --role AcrPull \
  --scope $ACR_ID

이미 attach-acr을 쓰는 경우에는 아래가 더 간단합니다.

az aks update -g <rg> -n <aks> --attach-acr <acr-name>

주의할 점:

  • role assignment 반영까지 수십 초에서 수 분 지연될 수 있습니다.
  • ACR이 다른 구독/테넌트에 있으면 권한 부여 방식이 달라지거나 제한이 있을 수 있습니다.

2-4) imagePullSecrets를 쓰는 경우: Secret과 ServiceAccount 연결 확인

Pod 스펙에서 imagePullSecrets가 있는지 확인합니다.

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

Secret이 실제로 존재하고 타입이 맞는지 확인합니다.

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

타입은 보통 kubernetes.io/dockerconfigjson 이어야 합니다.

예시 생성:

kubectl create secret docker-registry acr-pull \
  --docker-server=<acr-name>.azurecr.io \
  --docker-username=<username-or-spn-id> \
  --docker-password=<password-or-spn-secret> \
  -n <namespace>

그리고 ServiceAccount에 기본으로 붙이면 운영이 편합니다.

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

자주 하는 실수:

  • --docker-serverhttps://를 붙여버림
  • Secret은 만들었는데 Pod가 다른 ServiceAccount를 사용
  • ACR admin user 비활성화 상태에서 admin 계정으로 시도

3) DNS 진단: 레지스트리 도메인을 노드/파드가 해석 가능한가

lookup ... no such host류는 거의 DNS 문제입니다. AKS에서는 CoreDNS 설정, VNet DNS, Private DNS Zone 연결(특히 ACR Private Endpoint)이 대표 원인입니다.

3-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

CoreDNS가 CrashLoop거나, 로그에 upstream DNS 타임아웃이 보이면 먼저 클러스터 DNS 기반부터 흔들린 겁니다.

3-2) 파드에서 DNS 질의 테스트

운영 클러스터에 디버그 파드를 띄워 테스트합니다.

kubectl run -n <namespace> dns-debug \
  --image=busybox:1.36 \
  --restart=Never \
  --command -- sleep 3600

kubectl exec -n <namespace> -it dns-debug -- nslookup <acr-name>.azurecr.io

여기서 핵심은 “해석이 되는가”와 “어떤 IP로 해석되는가”입니다.

  • 퍼블릭 ACR이면 공인 IP 대역으로 해석
  • Private Endpoint를 쓴 ACR이면 사설 IP로 해석

3-3) Private Endpoint 사용 시: Private DNS Zone 연결 확인

ACR을 Private Endpoint로 붙였는데 DNS가 퍼블릭으로 해석되면, 노드는 공인으로 나가려다 방화벽/라우팅에서 막힐 수 있습니다. 반대로 퍼블릭 접근을 막아둔 ACR이라면 무조건 Private DNS가 맞아야 합니다.

점검 포인트:

  • Private DNS Zone에 privatelink.azurecr.io 레코드가 있는지
  • AKS 노드가 속한 VNet에 해당 Private DNS Zone이 링크되어 있는지

Azure CLI로는 구성에 따라 조회 커맨드가 달라질 수 있지만, 결론은 “노드가 질의하는 DNS 경로에서 privatelink 레코드가 보이느냐”입니다.


4) 프록시/방화벽/라우팅 진단: DNS는 되는데 연결이 안 된다

DNS는 정상인데 i/o timeout, context deadline exceeded, TLS 에러가 나면 네트워크 경로 문제일 가능성이 큽니다.

AKS에서 이미지 풀은 기본적으로 노드(kubelet)가 수행합니다. 즉, “파드에서 curl이 된다”와 “노드가 이미지를 풀 수 있다”는 동일하지 않을 수 있습니다.

4-1) 노드 레벨에서 막히는지 의심해야 하는 신호

  • 특정 노드에서만 ImagePullBackOff가 발생
  • 같은 Pod를 재스케줄하면 어떤 노드에서는 성공

노드별 편차를 보려면 파드를 강제로 다른 노드로 보내거나, 실패한 파드가 올라간 노드를 확인합니다.

kubectl get pod -n <namespace> <pod-name> -o wide

4-2) ACR 방화벽 설정 확인(퍼블릭 네트워크 차단 여부)

ACR에서 퍼블릭 네트워크 접근을 막아두면, Private Endpoint 또는 허용된 네트워크에서만 접근 가능합니다.

az acr show -n <acr-name> --query "networkRuleSet" -o jsonc

여기서 퍼블릭 차단인데 Private Endpoint/DNS 구성이 불완전하면 전형적으로 타임아웃이 납니다.

4-3) 프록시 환경에서의 흔한 문제

기업망에서 egress가 프록시를 강제하는 경우:

  • 노드는 프록시를 타야 하는데 kubelet/container runtime이 프록시를 모르고 직접 나가다 실패
  • 프록시가 TLS MITM을 하면서 노드의 신뢰 저장소에 사설 CA가 없어 x509 실패

이 경우는 “노드 OS / containerd / kubelet의 프록시 환경변수”와 “신뢰 CA 배포”가 핵심입니다.

예: systemd drop-in으로 프록시 설정(개념 예시)

sudo mkdir -p /etc/systemd/system/containerd.service.d
cat << 'EOF' | sudo tee /etc/systemd/system/containerd.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:8080"
Environment="HTTPS_PROXY=http://proxy.example.com:8080"
Environment="NO_PROXY=localhost,127.0.0.1,.cluster.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
EOF

sudo systemctl daemon-reload
sudo systemctl restart containerd

주의:

  • NO_PROXY에 클러스터 CIDR, 서비스 CIDR, *.cluster.local 등을 포함하지 않으면 내부 통신까지 프록시로 나가 장애가 확대됩니다.
  • AKS 관리형 노드는 업데이트/재이미징으로 설정이 날아갈 수 있어, 공식 지원되는 방식(예: AKS의 HTTP proxy 기능, 노드 이미지 커스터마이징 전략)을 검토해야 합니다.

4-4) x509 인증서 에러의 해석

x509: certificate signed by unknown authority는 대개 아래 중 하나입니다.

  • 프록시가 TLS를 가로채며 사설 CA로 재서명
  • 사내 레지스트리(또는 중간 장비)가 공인되지 않은 인증서를 제공

해결은 “노드의 신뢰 저장소에 사설 CA를 배포”하거나 “TLS interception 예외 처리”입니다. 이 또한 파드가 아니라 노드 런타임 기준으로 맞춰야 합니다.


5) 실제로 가장 많이 걸리는 케이스 3가지와 빠른 처방

케이스 A: unauthorized / denied가 뜬다

  • AKS kubelet identity에 AcrPull이 없다
  • ACR이 다른 구독에 있고 권한이 엇갈렸다
  • imagePullSecrets가 잘못되었거나 Pod에 연결되지 않았다

처방:

  • az role assignment list로 scope가 ACR 리소스인지 확인
  • az aks update --attach-acr로 표준 경로로 연결
  • Secret 방식이라면 ServiceAccount까지 포함해 연결 재검증

케이스 B: lookup ... no such host

  • CoreDNS 문제
  • Private Endpoint인데 Private DNS Zone 링크가 빠짐
  • 커스텀 DNS 서버가 privatelink 레코드를 모름

처방:

  • 디버그 파드에서 nslookup <acr>.azurecr.io 결과 IP 확인
  • Private Endpoint면 사설 IP가 나오는지 확인

케이스 C: DNS는 되는데 i/o timeout

  • ACR 퍼블릭 접근 차단 + Private Endpoint 경로 불완전
  • 노드 egress가 방화벽/UDR/NAT/프록시 정책에 의해 차단

처방:

  • ACR networkRuleSet 확인
  • 노드 서브넷의 라우팅/방화벽 정책 점검

네트워크/라우팅/엔드포인트 이슈는 클라우드가 달라도 진단 순서가 중요합니다. 비슷한 접근을 다룬 글로 EKS Pod→S3 504 타임아웃 - VPC 엔드포인트·NAT·DNS 진단도 함께 참고하면 분리 진단에 도움이 됩니다.


6) 재현 가능한 “최종 확인” 체크리스트

문제를 수정한 뒤에는 “우연히 됐다”가 아니라 아래를 통과해야 합니다.

6-1) 새 파드로 이미지 풀 강제

kubectl delete pod -n <namespace> <pod-name>
kubectl apply -f <manifest>.yaml
kubectl get pod -n <namespace> -w

6-2) 이벤트에서 성공 확인

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

Pulled 이벤트가 정상적으로 찍히는지 봅니다.

6-3) 노드 편차가 없는지 확인

레플리카를 늘려 여러 노드로 분산시키고, 특정 노드에서만 실패가 재발하는지 확인합니다.

kubectl scale deploy -n <namespace> <deploy-name> --replicas=5
kubectl get pod -n <namespace> -o wide

노드별로 성공/실패가 갈리면 권한이 아니라 네트워크/프록시/라우팅 계열일 확률이 높습니다.


7) 운영 팁: 원인 분리 속도를 높이는 관측 포인트

  • kubectl describe pod 이벤트는 1차 분류에 가장 빠름
  • nslookup 결과가 퍼블릭 IP인지 사설 IP인지로 Private Endpoint/DNS 구성 오류를 즉시 감별
  • “파드에서 통신됨”이 “노드에서 이미지 풀 됨”을 보장하지 않음(이미지 풀은 kubelet)
  • 동일 배포가 특정 노드에서만 실패하면 네트워크 정책/프록시/방화벽/라우팅의 노드 단위 차이를 의심

또한, 이미지 풀 이슈가 해결된 뒤에도 애플리케이션 단계에서 CrashLoopBackOff로 넘어가는 경우가 흔합니다. 그때는 메모리 OOM 등 런타임 원인 추적이 필요할 수 있는데, 이런 흐름 전환을 염두에 두면 장애 대응 시간이 줄어듭니다: K8s CrashLoopBackOff에서 OOMKilled 원인 추적


결론

AKS의 ImagePullBackOff는 증상 자체보다 “실패 지점”을 분리하는 게 핵심입니다.

  • unauthorized/denied면 ACR 권한(Managed Identity 또는 imagePullSecrets)부터
  • no such host면 CoreDNS 및 Private DNS Zone/프라이빗 엔드포인트 연동부터
  • timeout/x509면 노드 egress 경로(방화벽, 프록시, 라우팅, ACR 네트워크 정책)부터

위 순서대로 확인하면, 불필요하게 ACR을 재생성하거나 클러스터를 뜯어고치지 않고도 원인을 빠르게 좁힐 수 있습니다.