Symptoms
- Pods show
ErrImagePullorImagePullBackOff; deployments can't roll out and node replacements make it worse. kubectl describe podshows401 Unauthorized,403 Forbidden,not foundori/o timeout.
1. Read the exact pull error
kubectl get pods -A | grep -E "ImagePull|ErrImage"
kubectl describe pod <pod> -n <ns> | sed -n '/Events/,$p'
| Message | Cause | Go to |
|---|---|---|
401 Unauthorized / unauthorized: authentication required | Kubelet identity lost AcrPull, or wrong pull secret | Step 2 |
403 Forbidden / client with IP ... is not allowed | ACR firewall or public access disabled | Step 3 |
dial tcp ... i/o timeout / no such host | DNS or private endpoint path | Step 3 |
manifest unknown / not found | Tag deleted or retention policy purged it | Step 4 |
2. Fix authentication
AKS pulls with the kubelet identity, which needs AcrPull on the registry.
az aks check-acr -g rg-aks -n aks-prod --acr acrprod.azurecr.io
This command runs a test pod and reports exactly what fails. If the role is missing, reattach:
az aks update -g rg-aks -n aks-prod --attach-acr acrprod
Or grant it directly to the kubelet identity:
KUBELET=$(az aks show -g rg-aks -n aks-prod --query identityProfile.kubeletidentity.objectId -o tsv)
ACR_ID=$(az acr show -n acrprod --query id -o tsv)
az role assignment create --assignee-object-id $KUBELET --assignee-principal-type ServicePrincipal \
--role AcrPull --scope $ACR_ID
If the workload uses an imagePullSecrets secret with an ACR token or admin password, it may have been rotated. Recreate the secret:
kubectl create secret docker-registry acr-pull -n app \
--docker-server=acrprod.azurecr.io --docker-username=<token-name> --docker-password='<password>' \
--dry-run=client -o yaml | kubectl apply -f -
3. Fix network access
az acr show -n acrprod --query "{public:publicNetworkAccess, rules:networkRuleSet}"
az acr check-health -n acrprod --yes
- If public access was disabled, the cluster must reach ACR through a private endpoint. Check both
acrprod.azurecr.ioand the data endpoint (acrprod.<region>.data.azurecr.io) resolve to private IPs from a node:
kubectl run dnscheck --rm -it --image=busybox:1.36 --restart=Never -- nslookup acrprod.azurecr.io
- The
privatelink.azurecr.ioprivate DNS zone must be linked to the AKS VNet (or the DNS forwarders it uses). - If the cluster egresses through Azure Firewall, allow ACR FQDNs and the data endpoint.
Tip
If the registry you pull from is not reachable but a geo-replica is, Premium registries route to the nearest replica. Check replica status with
az acr replication list -r acrprod -o table.
4. Fix missing tags
az acr repository show-tags -n acrprod --repository orders-api --orderby time_desc --top 10 -o table
az acr config retention show -r acrprod
If a purge task or retention policy removed the tag, rebuild or re-push from CI, or roll back the deployment to a tag that still exists:
kubectl rollout undo deployment/orders-api -n app
5. Unblock the rollout
After the fix, delete the stuck pods so they retry immediately instead of waiting for back-off:
kubectl delete pod -n app -l app=orders-api --field-selector=status.phase=Pending
kubectl rollout status deployment/orders-api -n app
Prevent it next time
- Deploy by immutable digest or unique tags, and lock production tags (
az acr repository update --write-enabled false). - Exclude production repositories from purge tasks.
- Run
az aks check-acrin the pipeline after cluster or registry changes. - Use Premium with geo-replication for multi-region clusters.