Aller au contenu
Conteneurs & Orchestration medium

Diagnostiquer un ImagePullBackOff Kubernetes

15 min de lecture

logo kubernetes

ImagePullBackOff signifie que le kubelet n'arrive pas à télécharger l'image du conteneur depuis le registre. Le pod est assigné à un nœud (il n'est plus Pending), mais la création du conteneur échoue car l'image est introuvable, inaccessible ou refusée. Ce guide vous donne une méthodologie en 4 étapes pour identifier la cause et débloquer le déploiement.

  • Comprendre la différence entre ErrImagePull et ImagePullBackOff
  • Identifier les 7 causes principales d'un échec de pull d'image
  • Appliquer une méthodologie de diagnostic reproductible en 4 étapes
  • Configurer imagePullSecrets pour un registre privé
  • Reproduire et résoudre un ImagePullBackOff sur un cluster de test
  • Un cluster Kubernetes fonctionnel (v1.28+)
  • kubectl configuré et connecté au cluster
  • Des droits de lecture sur les pods et événements
  • Connaissances de base sur les Pods et les images de conteneurs

Quand le kubelet essaie de télécharger une image et échoue, il passe par deux états successifs :

ÉtatSignification
ErrImagePullLe pull vient d'échouer, c'est la première tentative (ou une nouvelle tentative après un backoff)
ImagePullBackOffLe kubelet a déjà échoué et attend avant de réessayer, c'est le backoff exponentiel

Ces deux états alternent en boucle : le kubelet essaie (ErrImagePull), échoue, attend (ImagePullBackOff), puis réessaie. Le délai d'attente augmente progressivement (backoff exponentiel), mais la documentation officielle ne détaille pas les timings exacts de ce backoff aussi précisément que pour CrashLoopBackOff.

Quand le kubelet doit démarrer un conteneur, il suit ces étapes :

  1. Vérifier le cache local, l'image existe-t-elle déjà sur le nœud ?
  2. Résoudre le nom de l'image, ajouter docker.io/library/ si pas de registre explicite, et :latest si pas de tag
  3. S'authentifier, si un imagePullSecret est configuré, le kubelet l'utilise pour s'authentifier auprès du registre
  4. Télécharger l'image, pull des layers manquants
  5. Vérifier l'intégrité, validation du digest SHA256

Si une de ces étapes échoue, le kubelet émet un événement Failed avec le détail de l'erreur, et le pod passe en ErrImagePull.

La colonne à exploiter en priorité est Message d'erreur typique : c'est elle qui fait le tri, pas la description de la cause. Le kubelet recopie verbatim la réponse du registre dans l'événement du pod, ce qui rend le diagnostic quasi mécanique. Notez que les causes 1 et 6 produisent le même message (manifest unknown), le registre ne faisant pas la différence entre un tag qui n'a jamais existé et un tag supprimé ; c'est l'historique de votre chaîne de publication qui tranche. La cause 7 est la plus trompeuse : l'image existe bien, le tag aussi, mais aucune variante ne correspond à l'architecture du nœud, un cas fréquent depuis l'arrivée des nœuds arm64 dans les parcs mixtes.

#CauseMessage d'erreur typiqueOù chercher
1Tag ou nom d'image incorrectmanifest unknown, not foundkubectl describe pod → Events
2Registre privé sans authentificationunauthorized, authentication requiredkubectl describe pod → Events
3imagePullSecret absent ou invalideno basic auth credentials, unauthorizedkubectl describe pod → Events + kubectl get secret
4Registre inaccessible (réseau)dial tcp: i/o timeout, no such hostkubectl describe pod → Events
5Limite de pull Docker Hubtoomanyrequests, rate limit exceededkubectl describe pod → Events
6Image ou tag supprimémanifest unknown, not foundkubectl describe pod → Events
7Architecture incompatibleno matching manifest for linux/amd64kubectl describe pod → Events
  1. Identifier les pods en ImagePullBackOff

    Fenêtre de terminal
    kubectl get pods -A | grep -E 'ImagePull|ErrImage'

    Vous pouvez aussi filtrer les événements de type pull échoué :

    Fenêtre de terminal
    kubectl get events -A --field-selector reason=Failed --sort-by='.lastTimestamp' | grep -i pull

    Notez le nom du pod, son namespace et depuis combien de temps il est dans cet état.

  2. Lire le message d'erreur exact

    Fenêtre de terminal
    kubectl describe pod <pod> -n <namespace>

    Allez directement à la section Events et cherchez les lignes Failed ou Warning. Le message contient l'information essentielle. Exemples :

    Failed to pull image "mon-registre.example.com/app:v2.1":
    rpc error: code = Unknown desc = failed to pull and unpack image:
    failed to resolve reference: pull access denied, repository does not
    exist or may require authorization

    Ce message vous dit : le registre est privé et le kubelet n'a pas les credentials.

    Failed to pull image "nginx:v99":
    rpc error: code = NotFound desc = failed to pull and unpack image:
    failed to resolve reference: docker.io/library/nginx:v99:
    not found

    Celui-ci : le tag v99 n'existe pas sur Docker Hub.

  3. Vérifier l'image et l'authentification

    Vérifier que l'image existe :

    Consultez l'interface ou l'API du registre concerné (Docker Hub, Harbor, GitLab Registry, ECR...) pour confirmer que le tag existe et qu'il est disponible pour l'architecture de vos nœuds. Pour tester un pull directement depuis un nœud, utilisez crictl :

    Fenêtre de terminal
    # Tester le pull depuis le nœud (via un Pod de debug ou en SSH)
    crictl pull mon-registre.example.com/app:v2.1

    Vérifier l'imagePullSecret :

    Fenêtre de terminal
    # Le secret existe-t-il dans le namespace du pod ?
    kubectl get secret -n <namespace> | grep docker
    # Le pod référence-t-il le secret ?
    kubectl get pod <pod> -n <namespace> -o jsonpath='{.spec.imagePullSecrets}'

    Si le pod n'a pas d'imagePullSecrets et que le registre est privé, c'est la cause du problème.

    Vérifier la connectivité réseau (depuis le contexte d'un nœud) :

    kubectl debug node/ crée un Pod de debug attaché au nœud cible. Depuis ce Pod, vous pouvez tester la résolution DNS et la connectivité réseau dans le contexte réseau du nœud :

    Fenêtre de terminal
    kubectl debug node/<node-name> -it --image=busybox:1.37
    # Une fois dans le shell du Pod de debug :
    nslookup mon-registre.example.com
    wget -q --spider https://mon-registre.example.com/v2/ && echo "OK" || echo "INACCESSIBLE"
  4. Corriger et vérifier

    Selon la cause identifiée :

    CauseAction corrective
    Tag incorrectCorriger le tag dans le manifeste (vérifier sur le registre)
    Registre privé sans authCréer un imagePullSecret et le référencer dans le pod
    Secret invalideRecréer le secret avec les bons credentials
    Registre inaccessibleVérifier le DNS, le firewall, le proxy du nœud
    Rate limit Docker HubAuthentifier les pulls ou utiliser un mirror
    Image ou tag suppriméCorriger le tag ou rebuilder/repusher l'image
    Architecture incompatibleRebuilder l'image pour l'architecture cible ou utiliser un manifest multi-arch

    Après correction, le pod retente automatiquement le pull. Si vous souhaitez forcer un re-pull immédiat :

    Fenêtre de terminal
    kubectl delete pod <pod> -n <namespace>

    Le contrôleur (Deployment, StatefulSet...) recréera un pod qui tentera le pull avec les nouveaux paramètres.

L'une des causes les plus courantes est l'absence de credentials pour un registre privé. Voici la procédure complète :

Le type docker-registry produit un Secret au format kubernetes.io/dockerconfigjson, c'est-à-dire exactement le contenu d'un fichier ~/.docker/config.json que le kubelet sait consommer. Le champ --docker-server doit contenir le même hôte que celui écrit dans le champ image du pod : si l'image est mon-registre.example.com/app:v2.1, un secret créé pour https://mon-registre.example.com/v2/ ne sera pas apparié et le pull restera refusé. Pour --docker-password, préférez un jeton d'accès révocable plutôt que le mot de passe du compte.

Fenêtre de terminal
read -rsp 'Jeton du registre : ' REGISTRY_TOKEN && echo
kubectl create secret docker-registry mon-registre-secret \
--docker-server=mon-registre.example.com \
--docker-username=mon-user \
--docker-password="$REGISTRY_TOKEN" \
--docker-email=admin@example.com \
-n <namespace>
unset REGISTRY_TOKEN

La lecture par read -rs évite d'écrire le jeton en clair dans l'historique du shell et dans la table des processus, où n'importe quel ps aux de la machine le rendrait visible. Le Secret lui-même n'est pas chiffré dans etcd par défaut, il est simplement encodé en base64 : le durcissement passe par le chiffrement au repos côté serveur d'API.

Le champ imagePullSecrets se déclare au niveau du pod, pas du conteneur, et vaut donc pour tous les conteneurs du pod, init containers compris. Le nom référencé doit correspondre à un Secret présent dans le même namespace : Kubernetes n'émet aucune erreur de validation si le Secret est absent, le pull échoue simplement comme s'il n'y avait pas d'authentification.

apiVersion: v1
kind: Pod
metadata:
name: app-privee
spec:
imagePullSecrets:
- name: mon-registre-secret
containers:
- name: app
image: mon-registre.example.com/app:v2.1

Pour éviter d'ajouter imagePullSecrets dans chaque pod, associez le secret au ServiceAccount par défaut du namespace :

Fenêtre de terminal
kubectl patch serviceaccount default -n <namespace> \
-p '{"imagePullSecrets": [{"name": "mon-registre-secret"}]}'

Tous les pods du namespace utilisant le ServiceAccount default hériteront automatiquement du secret.

Exemples pratiques : reproduire un ImagePullBackOff

Section intitulée « Exemples pratiques : reproduire un ImagePullBackOff »

Rien ne remplace un échec provoqué pour reconnaître un vrai. Les trois cas ci-dessous s'appliquent sur un cluster de test et produisent chacun un statut et un message différents dans kubectl describe. Retenez surtout que tous les échecs d'image ne débouchent pas sur ImagePullBackOff : un nom d'image syntaxiquement invalide est rejeté avant toute tentative de pull, sous le statut InvalidImageName, comme le montre le troisième cas.

Fenêtre de terminal
kubectl apply -f- <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pull-bad-tag
namespace: default
spec:
containers:
- name: app
image: nginx:version-inexistante
EOF

Diagnostic :

Fenêtre de terminal
kubectl describe pod pull-bad-tag | tail -10

Vous verrez Failed to pull image "nginx:version-inexistante" avec not found.

Pour reproduire un vrai refus d'authentification, il faut viser un registre qui répond, mais sur un dépôt qui exige des droits. Un chemin sous un espace de noms Docker Hub que vous ne possédez pas convient : le registre est joignable, mais il refuse l'accès.

Fenêtre de terminal
kubectl apply -f- <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pull-no-auth
namespace: default
spec:
containers:
- name: app
image: docker.io/stephanerobert/depot-prive-inexistant:v1
EOF

Diagnostic :

Fenêtre de terminal
kubectl describe pod pull-no-auth | tail -10

Le message contiendra pull access denied, repository does not exist or may require authorization suivi de insufficient_scope: authorization failed.

Fenêtre de terminal
kubectl apply -f- <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pull-bad-name
namespace: default
spec:
containers:
- name: app
image: "nginx ::latest"
EOF

Diagnostic :

Fenêtre de terminal
kubectl describe pod pull-bad-name | tail -10

Ce pod ne passe pas en ImagePullBackOff : son statut est InvalidImageName, avec l'événement Failed to apply default image tag "nginx ::latest": ... invalid reference format. Le nom est rejeté à la validation, avant même que le kubelet tente le moindre pull. C'est un cas voisin mais distinct, utile à connaître pour ne pas chercher un problème de registre là où il n'y a qu'une faute de frappe dans le manifeste.

Ces pods de test restent bloqués indéfiniment sans jamais démarrer, autant les supprimer une fois l'observation faite. Les options --force --grace-period=0 évitent d'attendre une terminaison propre, inutile ici puisque aucun conteneur n'a réellement démarré.

Fenêtre de terminal
kubectl delete pod pull-bad-tag pull-no-auth pull-bad-name --force --grace-period=0 2>/dev/null

Si certains pods démarrent normalement et d'autres pas, le problème est probablement local au nœud :

  • Le nœud n'a pas de connectivité réseau vers le registre (proxy manquant, DNS défaillant)
  • Le disque du nœud est plein (plus de place pour stocker les layers)
  • Le runtime de conteneur a un problème de configuration (containerd, CRI-O)

Ouvrez une session de debug sur le nœud affecté avec kubectl debug node/<node-name> puis vérifiez la résolution DNS, la connectivité réseau vers le registre et l'état du runtime CRI. Pour l'inspection du runtime, utilisez crictl :

Fenêtre de terminal
# Session de debug interactive sur le nœud
kubectl debug node/<node-name> -it --image=busybox:1.37
# Depuis le pod de debug, testez la connectivité
nslookup mon-registre.example.com
Fenêtre de terminal
# Sur le nœud (SSH ou debug), inspecter le runtime
crictl info
crictl images | grep mon-image

Docker Hub applique des limites de pull sur une fenêtre de 6 heures. Un accès non authentifié est plafonné à 100 pulls par adresse IPv4 (ou par sous-réseau IPv6 /64), un compte Personal gratuit authentifié à 200 pulls, tandis que les abonnements payants (Pro, Team, Business) sont sans limite. Dans un cluster où plusieurs nœuds sortent derrière la même IP publique, ces quotas sont vite atteints, car ils s'appliquent à l'adresse, pas au nœud.

Le message d'erreur contient toomanyrequests :

toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading:
https://www.docker.com/increase-rate-limit

Solutions :

  • Authentifier les pulls avec un compte Docker Hub pour bénéficier d'une limite plus élevée
  • Mettre en place un registry mirror (Harbor, Nexus, registry:2)
  • Utiliser imagePullPolicy: IfNotPresent pour éviter les pulls inutiles

Si un upgrade de cluster change le runtime de conteneur (par exemple de Docker à containerd), le cache d'images local est peut-être perdu. Tous les pods doivent re-puller leurs images, ce qui peut provoquer des ImagePullBackOff simultanés si le registre ou le réseau ne tient pas la charge.

Face à un pod bloqué, commencez toujours par lire l'événement exact avec kubectl describe pod : c'est lui qui distingue une image mal nommée d'un registre injoignable ou d'un refus d'authentification. Le tableau ci-dessous part du symptôme visible pour vous amener à la bonne commande de confirmation.

SymptômePremière actionCommande
ImagePullBackOff + not foundVérifier le tagkubectl describe pod <pod> → chercher le nom exact de l'image
ImagePullBackOff + unauthorizedVérifier l'imagePullSecretkubectl get pod <pod> -o jsonpath='{.spec.imagePullSecrets}'
ImagePullBackOff + i/o timeoutProblème réseauTester la connectivité depuis le nœud vers le registre
ImagePullBackOff + toomanyrequestsRate limit Docker HubAuthentifier les pulls ou mettre en place un mirror
ErrImageNeverPullimagePullPolicy: Never mais image absenteChanger la policy ou pré-charger l'image sur le nœud
Un seul nœud en erreurProblème local au nœudVérifier disque, DNS, proxy et runtime du nœud
  • ErrImagePull est l'erreur immédiate, ImagePullBackOff est l'attente entre deux tentatives, les deux apparaissent en alternance
  • kubectl describe pod est la commande clé, le message d'erreur dans les Events contient la raison exacte de l'échec
  • Le tag :latest est un piège : s'il est supprimé du registre, les nouveaux pods échouent alors que les anciens continuent de fonctionner. Utilisez des tags explicites
  • Pour un registre privé, il faut un imagePullSecret dans le même namespace que le pod, pensez à l'associer au ServiceAccount pour ne pas l'oublier
  • Docker Hub a un rate limit sur une fenêtre de 6 h, authentifiez vos pulls ou utilisez un mirror local en production
  • Si le problème ne touche qu'un seul nœud, c'est probablement un souci réseau, disque ou runtime local
  • Après correction, le kubelet retente le pull automatiquement, si vous voulez forcer un retry immédiat, supprimez le pod

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn