Cloud

Azure Container Registry : diagnostiquer un échec de pull d'image avant de redéployer

Un runbook de production pour qualifier un échec de pull ACR avec identité, réseau, DNS, pare-feu, logs, cache de nœud, validation et rollback avant de relancer le déploiement.

28 juin 2026 azureacrcontainer-registryakscontainer-appsidentitydnsfirewallkqldevopsrunbookrollback

Un échec de pull d’image ressemble souvent à un problème applicatif : un pod reste en ImagePullBackOff, une révision Container Apps ne démarre pas, un runner privé échoue avant le déploiement, ou une release AKS est annulée parce que l’image attendue n’arrive jamais sur le nœud. La tentation est de republier l’image, relancer le pipeline ou redéployer le workload.

En production, ce réflexe masque le vrai risque. Un pull ACR traverse plusieurs couches : nom d’image, tag ou digest, identité, droits AcrPull, résolution DNS, pare-feu du registre, réseau du nœud ou du runner, disponibilité du registre, cache local et logs de l’orchestrateur. Le bon runbook ne cherche pas seulement à “faire passer le pull”. Il doit prouver quelle couche bloque, quelle correction est minimale, et comment revenir à une image connue si la correction déplace l’incident.

Le cas d’usage est une plateforme Azure avec AKS, Azure Container Apps ou des runners CI privés qui tirent leurs images depuis Azure Container Registry. Certains environnements utilisent un accès public filtré, d’autres un chemin privé ou un firewall de sortie. Private Endpoint peut exister dans le chemin, mais il n’est qu’un composant possible : l’échec peut tout aussi bien venir d’une identité, d’un tag absent, d’une règle ACR ou d’une route sortante.

Nommer le symptôme exact

Avant de modifier le registre ou le cluster, qualifier le message d’erreur. ImagePullBackOff, ErrImagePull, 401 Unauthorized, 403 Forbidden, manifest unknown, connection timed out et no such host ne pointent pas vers les mêmes causes.

text acr-pull-symptom-map.txt
Symptôme observé
ImagePullBackOff ou ErrImagePull
  Lire l'événement Kubernetes ou Container Apps avant de relancer

401 Unauthorized
  Vérifier l'identité utilisée et le mécanisme d'authentification

403 Forbidden
  Vérifier AcrPull, scope, firewall ACR et accès réseau

manifest unknown ou tag not found
  Vérifier le repository, le tag, le digest et la promotion d'image

no such host
  Vérifier DNS depuis le nœud, le runner ou l'environnement Container Apps

connection timed out
  Vérifier routage, firewall de sortie, NAT, proxy et règles réseau ACR

Cette étape évite une erreur classique : donner AcrPull à une identité déjà correcte alors que le tag n’existe pas, ou republier une image alors que les nœuds ne peuvent plus résoudre le registre.

Fixer l’image attendue

Un tag mutable rend le diagnostic fragile. Le premier contrôle consiste à écrire l’image exacte attendue, puis à vérifier si elle existe dans ACR. Quand c’est possible, diagnostiquer avec un digest plutôt qu’avec un tag.

bash 01-check-acr-image.sh
ACR_NAME="acrprod01"
REPOSITORY="payments/api"
TAG="2026.06.28.4"

az acr repository show-tags --name "$ACR_NAME" --repository "$REPOSITORY" --orderby time_desc --output table

az acr manifest list-metadata --registry "$ACR_NAME" --name "$REPOSITORY" --query "[?tags[?@=='$TAG']].{digest:digest,tags:tags,createdTime:createdTime}" --output table

Si le digest attendu est absent, le problème est en amont : build, push, promotion ou rétention. Relancer le déploiement ne corrigera pas un registre qui ne contient pas l’objet demandé.

Identifier l’identité réelle du pull

Le pull n’est pas toujours exécuté par l’identité que l’équipe imagine. AKS peut utiliser une kubelet identity ou une managed identity dédiée. Container Apps peut utiliser une managed identity configurée au niveau de l’application ou de l’environnement. Un runner privé peut utiliser OIDC, un service principal, az acr login ou un secret Docker.

bash 02-check-pull-identity.sh
AKS_RG="rg-aks-prod"
AKS_NAME="aks-prod"
ACR_ID="$(az acr show --name acrprod01 --query id -o tsv)"

az aks show --resource-group "$AKS_RG" --name "$AKS_NAME" --query "{kubeletIdentity:identityProfile.kubeletidentity.objectId,managedIdentity:identity.principalId}" --output json

KUBELET_OBJECT_ID="$(az aks show --resource-group "$AKS_RG" --name "$AKS_NAME" --query "identityProfile.kubeletidentity.objectId" -o tsv)"

az role assignment list --assignee "$KUBELET_OBJECT_ID" --scope "$ACR_ID" --query "[].{role:roleDefinitionName,scope:scope}" --output table

Le bon critère n’est pas “une identité a AcrPull”. C’est “l’identité qui exécute réellement le pull a AcrPull sur le bon registre ou le bon scope”. Si le registre est partagé entre environnements, éviter d’élargir au groupe de ressources complet sans preuve.

Tester le chemin réseau depuis le bon endroit

Tester depuis un poste d’administration ou Cloud Shell ne suffit pas. Le pull part du nœud, de l’environnement Container Apps ou du runner. Il faut donc tester DNS, TLS et sortie réseau depuis ce chemin.

bash 03-network-preflight-from-runner.sh
REGISTRY="acrprod01.azurecr.io"

getent hosts "$REGISTRY"

timeout 5 bash -c "cat < /dev/null > /dev/tcp/$REGISTRY/443"

openssl s_client -connect "$REGISTRY:443" -servername "$REGISTRY" </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer

Si ACR est exposé par Private Endpoint, vérifier la zone privatelink.azurecr.io, les liens VNet, les forwarders et la résolution depuis le réseau consommateur. Si le registre reste public mais filtré, vérifier l’IP sortante réelle, NAT Gateway, Azure Firewall, proxy et allowlist ACR.

Lire les événements de l’orchestrateur

Les événements Kubernetes et les logs Container Apps donnent souvent la cause brute. Ils doivent être conservés dans le ticket avant toute correction, car un redéploiement peut effacer le contexte.

bash 04-read-orchestrator-events.sh
NAMESPACE="payments"
DEPLOYMENT="payments-api"

kubectl -n "$NAMESPACE" get pods -l app="$DEPLOYMENT" -o wide

kubectl -n "$NAMESPACE" describe pod "$(kubectl -n "$NAMESPACE" get pod -l app="$DEPLOYMENT" -o jsonpath='{.items[0].metadata.name}')" | sed -n '/Events:/,$p'

kubectl -n "$NAMESPACE" get events --sort-by=.lastTimestamp --field-selector type=Warning

Pour Container Apps, lire la révision active, les erreurs système et les logs console. Le symptôme peut être un pull impossible, mais aussi une révision qui démarre avec une ancienne image parce que la nouvelle n’a jamais été disponible.

Corréler ACR, activité Azure et logs réseau

Le registre peut prouver si la requête arrive, quelle identité est vue, et si le refus vient du service ou du réseau. Selon la configuration, les logs ACR, AzureActivity, Firewall ou Application Insights donnent l’indice utile.

kusto 05-acr-pull-evidence.kql
let Start = datetime(2026-06-28 08:00:00);
let End = datetime(2026-06-28 09:00:00);
ContainerRegistryRepositoryEvents
| where TimeGenerated between (Start .. End)
| where Repository has "payments/api"
| project TimeGenerated, OperationName, Repository, Tag, Digest, ResultType, CallerIpAddress, Identity
| order by TimeGenerated desc

Adapter la requête aux tables disponibles dans le workspace. Si aucun log ACR n’apparaît pendant la fenêtre de pull, le trafic n’atteint probablement pas le registre ou les diagnostics ne couvrent pas l’événement. Dans ce cas, les logs firewall, les événements nœud et les tests DNS deviennent prioritaires.

Choisir la correction minimale

La correction dépend du diagnostic, pas du symptôme global. Un pull bloqué ne justifie pas automatiquement un redéploiement complet, un accès public temporaire ou un rôle trop large.

text acr-pull-decision.txt
Corriger sans rollback applicatif
Le tag ou digest existe et l'identité réelle n'a pas AcrPull
Une règle firewall ACR bloque l'IP sortante attendue
Un lien DNS ou forwarder manque pour le registre privé
Le runner utilise un ancien secret Docker remplaçable

Rollback de workload
La nouvelle image est absente ou corrompue
La révision active ne peut pas démarrer dans la fenêtre de changement
Le digest attendu ne correspond pas à l'image validée
Plusieurs environnements critiques ne peuvent plus tirer depuis ACR

Refuser une correction trop large
Ouvrir publicNetworkAccess sans fenêtre et sans preuve
Donner AcrPull au groupe entier par défaut
Remplacer le tag sans attacher le digest attendu
Purger le cache nœud avant d'avoir capturé les événements

Le rollback le plus propre est souvent de revenir à un digest déjà validé ou à une révision précédente, tout en gardant le diagnostic ACR ouvert. Cela rétablit le service sans supprimer la preuve.

Valider après correction

Après correction, prouver trois choses : le registre contient l’image, le chemin de pull fonctionne depuis l’environnement réel, et le workload démarre avec le digest attendu.

bash 06-validate-after-fix.sh
NAMESPACE="payments"
DEPLOYMENT="payments-api"
EXPECTED_DIGEST="sha256:0123456789abcdef..."

kubectl -n "$NAMESPACE" rollout status deploy/"$DEPLOYMENT" --timeout=120s

kubectl -n "$NAMESPACE" get pods -l app="$DEPLOYMENT" -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.containerStatuses[*].imageID}{"\n"}{end}'

kubectl -n "$NAMESPACE" get pods -l app="$DEPLOYMENT" -o jsonpath='{range .items[*]}{.status.containerStatuses[*].ready}{"\n"}{end}'

Si les pods démarrent mais avec un digest différent, la chaîne CI/CD ou la stratégie de tag reste ambiguë. La validation doit alors bloquer la clôture de l’incident même si le service semble revenu.

Conclusion

Un échec de pull ACR est un incident de chaîne d’exécution, pas seulement un problème de conteneur. Le diagnostic doit partir du symptôme exact, vérifier l’existence de l’image, identifier l’identité réelle, tester le réseau depuis le bon endroit et corréler les logs avant de corriger.

La décision saine est directe : corriger la couche prouvée quand l’image est bonne, revenir à un digest validé quand la release est incertaine, et refuser les ouvertures larges qui transforment un incident de pull en dette de sécurité durable.