Aller au contenu
Diagnostiquer les pannes courantes

Diagnostiquer les pannes courantes

200 Pratiquer ⏱ 1 h 30 kuberneteskubectl

À la fin, vous saurez

  • Remonter d'un symptôme au bon objet : pod, contrôleur, Service, nœud
  • Lire les états d'un pod et les raisons d'attente d'un conteneur, et savoir ce que chacun signifie
  • Utiliser les événements, les journaux du conteneur précédent et les codes de sortie
  • Vérifier qu'un Service a des points de terminaison et que le DNS du cluster répond
  • Déboguer un pod sans shell avec un conteneur éphémère
  • Diagnostiquer et corriger six pannes courantes sur Signalements

Prérequis

Testé avec kubectl 1.36.0 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

Un vendredi en fin d'après-midi, un message arrive : « Signalements ne répond plus ». Le réflexe, quand on découvre Kubernetes, est de supprimer les pods pour voir s'ils reviennent, puis de relancer le dernier kubectl apply, puis de chercher le message d'erreur sur un moteur de recherche. Parfois cela marche, et l'on ne sait pas pourquoi. Le plus souvent, on perd une heure, et l'on détruit au passage les indices : les journaux du conteneur qui a planté, les événements qui expliquaient pourquoi.

Kubernetes est pourtant un système bavard. Chaque contrôleur écrit ce qu'il fait dans le statut des objets et dans des événements ; le kubelet garde les journaux du conteneur précédent et la raison de sa mort ; le planificateur explique pourquoi il n'a pas pu placer un pod. Presque toutes les pannes courantes se diagnostiquent en quelques minutes avec quatre commandes, à condition de savoir où regarder et dans quel ordre.

Cette leçon donne cette méthode, puis l'applique à six pannes de Signalements, celles que l'on rencontre réellement : une image introuvable, un conteneur qui manque de mémoire, un Secret absent, un sélecteur faux, un pod trop gourmand, une base indisponible. Elle prolonge la méthode réseau du cours Le modèle TCP/IP, qui reste valable à l'intérieur du cluster.

Les concepts

La méthode : du symptôme à l'objet

Un symptôme se manifeste presque toujours au bout de la chaîne (l'utilisateur ne reçoit pas de réponse), et sa cause est presque toujours plus haut. On remonte la chaîne dans l'ordre où Kubernetes construit les choses :

    flowchart TB
  A["Symptôme<br/>(erreur, délai, 503)"] --> B["Les pods existent-ils ?<br/>kubectl get pods"]
  B -->|"aucun pod"| C["Le contrôleur<br/>describe deployment / replicaset"]
  B -->|"pods présents"| D["Quel état ?<br/>Pending, Waiting, CrashLoopBackOff, pas prêt"]
  D --> E["describe pod + événements<br/>logs --previous"]
  B -->|"pods prêts"| F["Le Service a-t-il<br/>des points de terminaison ?"]
  F -->|"oui"| G["DNS, entrée (Gateway), réseau"]
  E --> H["Le nœud<br/>describe node"]
  

Trois règles accompagnent cette méthode :

  • Observer avant d'agir. Supprimer un pod efface ses journaux et ses événements ; redémarrer un Deployment efface l'état qui aurait expliqué la panne. On lit d'abord, on corrige ensuite.
  • Lire le message entier. Les messages de Kubernetes sont longs et précis : celui du planificateur dit combien de nœuds ont été écartés, et pour quelle raison chacun. La réponse est souvent dans la seconde moitié de la ligne.
  • Changer une seule chose à la fois, et vérifier l'effet avant de passer à la suivante.

Les commandes de base

CommandeCe qu'elle donne
kubectl get pods -o wideétat, nombre de redémarrages, âge, nœud et adresse de chaque pod
kubectl describe pod <pod>spécification résolue, état de chaque conteneur (avec la raison et le code de sortie du précédent), conditions, et les événements récents en bas
kubectl events --for pod/<pod>les événements d'un objet, triés ; kubectl get events --sort-by=.lastTimestamp dans les versions plus anciennes
kubectl logs <pod> --previousles journaux du conteneur précédent, celui qui a planté
kubectl logs <pod> -c <conteneur> --since=10mles journaux d'un conteneur précis, sur une période
kubectl exec -it <pod> -- <commande>une commande dans le conteneur, s'il contient la commande
kubectl debug -it <pod> --image=<image> --target=<conteneur>un conteneur éphémère dans le pod, avec ses propres outils
kubectl port-forward <pod> 8000:8000un tunnel depuis le poste vers un port du pod, sans passer par le Service ni l'entrée
kubectl auth can-i <verbe> <ressource>vérifier ses propres droits avant de conclure qu'un objet n'existe pas

Deux remarques sur les événements. Ils sont stockés dans l'API, mais pour une durée limitée : l'option --event-ttl de l'API vaut une heure par défaut. Une panne survenue la nuit n'a souvent plus d'événements le matin ; d'où l'intérêt de les exporter vers le système de journaux, sujet du cours Journaux centralisés avec Loki. Et ils sont par objet : l'événement qui explique qu'un pod n'a pas pu être créé est sur le ReplicaSet, pas sur le Deployment ni sur un pod qui n'existe pas.

Lire l'état d'un pod

La colonne STATUS de kubectl get pods mélange deux informations : la phase du pod (Pending, Running, Succeeded, Failed) et, quand un conteneur attend, la raison de cette attente. Les raisons que l'on rencontre le plus souvent sont définies dans le code du kubelet et du planificateur :

AffichageOù en est le podCauses habituellesOù regarder
Pendingpas encore placé sur un nœudressources insuffisantes, PVC non liée, teintes, affinités, quotasévénement FailedScheduling dans describe pod
ContainerCreating (qui dure)placé, conteneur en préparationvolume qui ne s'attache pas, réseau du podévénements FailedMount, FailedAttachVolume
ErrImagePull, ImagePullBackOffplacé, image introuvableétiquette fausse, registre privé sans identifiants, limite de débit du registreévénement Failed avec le message du registre
CreateContainerConfigErrorplacé, configuration impossible à construireSecret ou ConfigMap absent, clé absentemessage dans describe pod
CrashLoopBackOffle conteneur démarre puis meurt, en boucleerreur au démarrage, mémoire insuffisante (OOMKilled), commande fausselogs --previous, Last State dans describe pod
Running, mais READY 0/1le conteneur tourne, la sonde de disponibilité échouedépendance absente, mauvais port ou chemin de sondeévénement Unhealthy
Terminating (qui dure)suppression demandée, non achevéefinalizer non retiré, nœud injoignablemetadata.finalizers, état du nœud

Pour ImagePullBackOff et CrashLoopBackOff, le mot BackOff signale que le kubelet espace ses tentatives. Pour les redémarrages de conteneur, la documentation décrit un délai exponentiel de 10 s, 20 s, 40 s, plafonné à 300 secondes, et remis à zéro après dix minutes de fonctionnement sans incident. Un pod en CrashLoopBackOff depuis une heure ne réessaie donc plus que toutes les cinq minutes : après une correction, inutile d'attendre, on supprime le pod pour qu'il soit recréé immédiatement.

Les codes de sortie

describe pod affiche, pour un conteneur qui a redémarré, l'état du précédent (Last State: Terminated), avec sa raison et son code de sortie. Les conventions sont celles du shell, vues dans Linux : premiers pas :

  • 0 : sortie normale. Pour un conteneur de Deployment, c'est quand même un problème (le processus principal n'aurait pas dû s'arrêter).
  • 1 ou autre petit nombre : erreur de l'application, à lire dans les journaux.
  • 137 = 128 + 9 : tué par SIGKILL. Avec la raison OOMKilled, c'est le noyau qui a tué le processus parce que le conteneur a dépassé sa limite de mémoire (leçon 8). Sans cette raison, c'est souvent un arrêt qui a dépassé le délai de grâce.
  • 143 = 128 + 15 : arrêté par SIGTERM, en général à la demande de Kubernetes.

Les Services et le DNS

Un pod qui fonctionne ne suffit pas : le trafic l'atteint par un Service. La première question est toujours la même, et la documentation Debug Services la pose avant toutes les autres : le Service a-t-il des points de terminaison ? Un Service dont le sélecteur ne correspond aux étiquettes d'aucun pod prêt n'a aucun point de terminaison, et toute connexion échoue. Trois causes, dans l'ordre de fréquence : le sélecteur ne correspond pas aux étiquettes (une faute de frappe, une étiquette renommée), les pods ne sont pas prêts (un pod non prêt est retiré des points de terminaison), ou le targetPort ne correspond pas au port sur lequel le conteneur écoute.

Côté DNS, chaque pod a un fichier /etc/resolv.conf qui désigne le service DNS du cluster et une liste de domaines de recherche. La documentation en donne un exemple :

search default.svc.cluster.local svc.cluster.local cluster.local google.internal c.gce_project_id.internal
nameserver 10.0.0.10
options ndots:5

L'option ndots:5 signifie qu'un nom qui contient moins de cinq points est d'abord essayé avec chacun des domaines de recherche : postgresql devient postgresql.signalements.svc.cluster.local au premier essai, ce qui est voulu. Mais un nom externe comme api.exemple.fr (deux points) est lui aussi essayé avec chaque domaine de recherche avant d'être demandé tel quel : plusieurs requêtes inutiles par résolution. Pour les appels externes fréquents, un nom terminé par un point (api.exemple.fr.) court-circuite la recherche.

Les objets bloqués en Terminating

Un objet qui porte des finalizers (metadata.finalizers) ne disparaît pas quand on le supprime : l'API lui ajoute un deletionTimestamp, puis attend que chaque contrôleur concerné ait fait son travail et retiré son finalizer. La documentation cite l'exemple de kubernetes.io/pv-protection, qui empêche de supprimer un volume encore utilisé. Un objet bloqué en Terminating attend presque toujours un contrôleur qui n'existe plus (désinstallé avant ses objets) ou qui échoue. Pour un pod, la documentation cite aussi les webhooks d'admission qui refusent la mise à jour retirant le finalizer, et le cas d'un nœud injoignable : le kubelet n'est plus là pour confirmer l'arrêt.

Retirer un finalizer à la main (kubectl patch ... -p '{"metadata":{"finalizers":null}}') débloque l'objet, mais court-circuite ce que le contrôleur devait faire : libérer un volume, supprimer un répartiteur chez le fournisseur. On ne le fait qu'après avoir compris quel contrôleur est en cause, et en acceptant de faire son travail à sa place.

Le conteneur éphémère

L'image de Signalements, comme beaucoup d'images de production, ne contient ni shell ni outils : kubectl exec -it <pod> -- sh échoue. Un conteneur éphémère (stable depuis Kubernetes 1.25) résout ce problème : kubectl debug ajoute au pod en marche un conteneur supplémentaire, avec l'image de votre choix, sans redémarrer le pod. L'option --target le place dans l'espace de noms de processus du conteneur visé : ps y voit Gunicorn, et /proc/<pid>/root donne accès à son système de fichiers.

La documentation en fixe les limites : un conteneur éphémère n'a ni ports, ni sondes, ni ressources propres, ne redémarre jamais, et ne peut être ni modifié ni retiré une fois ajouté ; il disparaît avec le pod. Sous kubectl 1.36, le profil de débogage par défaut est general (l'aide de la commande l'indique) ; les profils baseline et restricted produisent un conteneur compatible avec les profils de sécurité de même nom, utile dans un namespace protégé comme celui de Signalements.

Deux variantes complètent l'outil : kubectl debug <pod> --copy-to=<copie> crée une copie du pod (par exemple avec une autre commande, pour démarrer un conteneur qui plante aussitôt), et kubectl debug node/<nœud> lance un pod de débogage sur un nœud, avec le système de fichiers du nœud monté sous /host.

En pratique

Les six scénarios suivants partent du déploiement de la leçon 11, dans le namespace signalements. Pour chacun, reproduisez la panne, appliquez la méthode avant de lire la cause, puis corrigez. Les commandes demandent un cluster ; ce que vous devez observer est décrit d'après la documentation et le code de Kubernetes 1.36.

Pour alléger les commandes :

$ kubectl config set-context --current --namespace=signalements

Scénario 1 : la version qui n'existe pas

Reproduire. kubectl set image deployment/signalements api=ghcr.io/lyneko-formation/signalements:1.2.1, une étiquette qui n'existe pas dans le registre.

Symptôme. L'application répond toujours : grâce à maxUnavailable: 0, les anciens pods n'ont pas été retirés. Mais kubectl rollout status deployment/signalements ne se termine pas.

Diagnostic.

$ kubectl get pods -o wide
$ kubectl describe pod <le pod neuf>

Le pod neuf est en ErrImagePull, puis ImagePullBackOff. En bas de describe, les événements Failed reprennent le message du registre : l'image ou l'étiquette est introuvable (not found). Si le message parlait d'autorisation (unauthorized, denied), l'image existerait mais le cluster n'aurait pas le droit de la tirer : registre privé sans imagePullSecrets. S'il parlait de trop de requêtes (toomanyrequests), ce serait la limite de débit d'un registre public, typiquement Docker Hub pour les tirages anonymes.

Corriger.

$ kubectl rollout undo deployment/signalements
$ kubectl rollout status deployment/signalements

rollout undo revient au ReplicaSet précédent (leçon 5). La vraie correction est en amont : un pipeline qui ne déploie qu'une image effectivement publiée, désignée par son empreinte.

Scénario 2 : le conteneur qui manque de mémoire

Reproduire. Abaissez la limite de mémoire de l'API à 32Mi dans deployment.yaml et appliquez.

Symptôme. Les pods neufs passent en CrashLoopBackOff, la colonne RESTARTS grimpe.

Diagnostic.

$ kubectl describe pod <pod>
$ kubectl logs <pod> --previous
$ kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'

Dans describe, la section du conteneur montre Last State: Terminated, Reason: OOMKilled, Exit Code: 137. Les journaux du conteneur précédent s'arrêtent net, sans message d'erreur de l'application : le processus n'a pas eu le temps d'écrire quoi que ce soit, le noyau l'a tué. C'est la signature d'un OOMKilled, et la raison pour laquelle on regarde describe avant de chercher dans les journaux. La commande jsonpath extrait cette raison seule, pratique dans un script.

Corriger. Rétablir une limite cohérente avec la consommation observée (kubectl top pod, si le serveur de métriques est installé, ce qui n'est pas le cas par défaut sur kind) et appliquer. Une limite de mémoire se dimensionne sur la consommation de pointe, avec une marge : deux workers Gunicorn chargés de Flask et psycopg ne tiennent pas dans 32 Mio.

Scénario 3 : le Secret absent

Reproduire. Supprimez le Secret signalements (kubectl delete secret signalements), puis redémarrez le Deployment (kubectl rollout restart deployment/signalements).

Symptôme. Les nouveaux pods restent en CreateContainerConfigError ; les anciens continuent de servir, puisque la mise à jour ne peut pas progresser.

Diagnostic. kubectl describe pod <pod neuf> : le message indique que le Secret signalements est introuvable (not found). Si le Secret existait mais sans la clé demandée par un secretKeyRef, le message du kubelet serait couldn't find key DATABASE_URL in Secret signalements/signalements.

Corriger. Recréer le Secret comme à la leçon 11. Les pods en attente le prennent en compte à la tentative suivante du kubelet, sans autre action. Cette panne est typique d'un namespace recréé (les Secrets créés à la main ne sont pas dans la kustomization) ou d'une synchronisation d'External Secrets en échec : dans ce second cas, c'est l'objet ExternalSecret dont il faut lire le statut.

Scénario 4 : le Service sans points de terminaison

Reproduire. Dans service.yaml, remplacez app.kubernetes.io/component: api par app.kubernetes.io/component: web, et appliquez.

Symptôme. curl à travers la Gateway reçoit une erreur 503 de Traefik ; les pods sont pourtant Running et READY 1/1.

Diagnostic. On suit la documentation Debug Services, dans l'ordre :

$ kubectl get endpointslices -l kubernetes.io/service-name=signalements
$ kubectl get service signalements -o jsonpath='{.spec.selector}{"\n"}'
$ kubectl get pods -l app.kubernetes.io/name=signalements,app.kubernetes.io/component=web
$ kubectl get pods --show-labels

L'EndpointSlice du Service ne contient aucune adresse. Le sélecteur demande component: web ; aucun pod ne porte cette étiquette (la troisième commande ne renvoie rien) ; la quatrième montre les étiquettes réelles des pods. C'est le diagnostic le plus rentable de tout Kubernetes : un Service sans points de terminaison est un problème de sélecteur ou de disponibilité, jamais de réseau.

Pour isoler l'application du reste, un tunnel direct vers un pod :

$ kubectl port-forward deployment/signalements 8000:8000
$ curl -s http://localhost:8000/sante

Si cette requête fonctionne, l'application est saine, et le problème est entre le pod et l'utilisateur : Service, route, Gateway.

Corriger. Rétablir le sélecteur et appliquer. Si les adresses étaient présentes mais marquées non prêtes, la cause serait la sonde de disponibilité (scénario 6).

Scénario 5 : le pod qui ne trouve pas sa place

Reproduire. Passez la demande de processeur de l'API à cpu: "16" et appliquez.

Symptôme. Le pod neuf reste Pending, sans adresse IP ni nœud.

Diagnostic. kubectl describe pod <pod> se termine par un événement FailedScheduling du planificateur. Son message commence par 0/3 nodes are available: (la forme est définie dans le code du planificateur), suivi du décompte des raisons : ici, des nœuds écartés pour Insufficient cpu, et, sur kind, le nœud du plan de contrôle écarté pour sa teinte (untolerated taint), puisque les applications n'y vont pas. La même structure explique les autres causes de Pending : une PVC qui n'est pas liée, une affinité qu'aucun nœud ne satisfait, un sélecteur de nœud trop précis.

$ kubectl describe nodes | grep -A 8 'Allocated resources'

La section Allocated resources de chaque nœud montre la somme des demandes déjà réservées, par rapport à la capacité allouable : c'est ce que regarde le planificateur, et non la consommation réelle (leçon 8).

Corriger. Rétablir une demande réaliste. Dans un cluster avec mise à l'échelle automatique des nœuds (Kapsule le propose), un pod Pending pour ressources insuffisantes déclenche l'ajout d'un nœud, sauf si la demande dépasse ce qu'aucun type de nœud ne peut offrir : le pod reste alors en attente pour toujours.

Scénario 6 : la base indisponible

Reproduire. Mettez la base à zéro réplique : kubectl scale statefulset/postgresql --replicas=0.

Symptôme. Après quelques secondes, l'application renvoie des erreurs 503 à travers la Gateway. Les pods de l'API sont Running, ne redémarrent pas, mais sont READY 0/1.

Diagnostic.

$ kubectl get pods
$ kubectl events --for deployment/signalements
$ kubectl describe pod <pod de l'API>
$ kubectl logs <pod de l'API> --since=5m

describe montre des événements Unhealthy : la sonde de disponibilité échoue sur /sante. Les journaux de l'application montrent les erreurs de connexion à PostgreSQL. Les pods de la base ont disparu. Le comportement est exactement celui que l'on a conçu à la leçon 11 : la sonde de disponibilité retire les pods du Service, la sonde de vie, qui ne teste que le port, ne les redémarre pas. Si la sonde de vie interrogeait /sante, on verrait en plus les pods de l'API redémarrer en boucle, sans aucun bénéfice.

Pour vérifier la résolution du nom de la base depuis le cluster, un conteneur éphémère avec des outils réseau, sans toucher à l'image de l'application :

$ printf 'securityContext:\n  runAsUser: 65534\n' > debug-non-root.yaml
$ kubectl debug -it <pod de l'API> --image=busybox:1.37 --target=api \
    --profile=restricted --custom=debug-non-root.yaml -- sh
/ # nslookup postgresql.signalements.svc.cluster.local
/ # cat /etc/resolv.conf

Avec la base à zéro réplique, le Service headless n'a plus aucune adresse à renvoyer, et la résolution échoue : le symptôme DNS est ici une conséquence, pas la cause.

Le profil restricted est nécessaire parce que le namespace l'impose : sans lui, l'API refuserait d'ajouter le conteneur éphémère. Mais ce profil se contente d'exiger un utilisateur non root (runAsNonRoot: true, d'après le code de kubectl 1.36), et l'image BusyBox tourne en root par défaut : le kubelet refuserait de la démarrer. Le fichier passé à --custom, un morceau de spécification de conteneur que kubectl debug fusionne avec le profil, fixe donc un utilisateur non privilégié (65534, l'utilisateur nobody).

Corriger. kubectl scale statefulset/postgresql --replicas=1. Dès que la base est prête, les sondes de disponibilité de l'API réussissent, et les pods reviennent dans le Service sans redémarrer.

Deux réflexes pour les autres cas

Aucun pod du tout. kubectl get pods ne montre rien pour un Deployment qui en demande deux : le problème est avant le pod. kubectl describe replicaset -l app.kubernetes.io/name=signalements montre des événements FailedCreate, par exemple un refus de l'admission de sécurité des pods (le profil restricted du namespace) ou un dépassement de quota, avec le détail des règles enfreintes.

Un nœud NotReady. kubectl get nodes puis kubectl describe node <nœud> : la section Conditions dit si le kubelet ne répond plus, ou si le nœud manque de mémoire, de disque (DiskPressure) ou de numéros de processus. Après un délai, les pods d'un nœud injoignable sont replanifiés ailleurs ; ceux d'un StatefulSet attendent, eux, la confirmation que l'ancien pod est bien arrêté, pour ne jamais avoir deux postgresql-0 en même temps.

Sous le capot

D'où vient chaque information. kubectl get pods lit le statut que le kubelet du nœud écrit dans l'API : la phase, l'état de chaque conteneur (waiting, running, terminated, avec raison et code), le nombre de redémarrages. Les raisons d'attente (ErrImagePull, ImagePullBackOff, CreateContainerConfigError, CrashLoopBackOff) sont des constantes du code du kubelet ; le message 0/N nodes are available est une constante du planificateur. Ce ne sont pas des textes libres : on peut s'y fier et les chercher dans un script.

Les événements sont des objets. Chaque événement est un objet de l'API (groupe events.k8s.io), qui référence l'objet concerné, porte une raison, un message, un type (Normal ou Warning), un compteur et des dates. Les composants les agrègent : un événement répété incrémente son compteur au lieu de créer un nouvel objet, d'où les mentions du type « x12 over 5m ». Ils expirent après --event-ttl, une heure par défaut.

logs --previous lit le fichier du conteneur mort. Le kubelet garde, sur le nœud, les journaux du conteneur courant et ceux du précédent de chaque conteneur du pod. Quand le pod est supprimé, ces fichiers le sont aussi. C'est pourquoi on ne supprime pas un pod en CrashLoopBackOff avant d'avoir lu --previous : on détruirait la seule trace de l'erreur.

kubectl debug passe par une sous-ressource. Les conteneurs éphémères ne s'ajoutent pas en modifiant la spécification du pod (qui est en grande partie non modifiable) : kubectl debug appelle la sous-ressource ephemeralcontainers du pod. Le kubelet démarre alors le conteneur dans le pod existant, avec ses volumes et son réseau. Avec --target, le conteneur rejoint l'espace de noms de processus du conteneur cible, si le moteur de conteneurs le prend en charge.

Pourquoi un Service sans points de terminaison n'est pas un problème réseau. Le contrôleur des EndpointSlices calcule en permanence, pour chaque Service, la liste des pods qui correspondent au sélecteur, avec leur état de disponibilité. kube-proxy et Traefik ne font que suivre cette liste. Si elle est vide, aucun composant réseau n'a de destination : le problème est dans les étiquettes ou dans les sondes, ce que la liste vide indique immédiatement.

Pièges courants

Supprimer les pods pour « voir ». On perd les journaux du conteneur précédent et les événements. Lire d'abord.

Désactiver les sondes « pour que ça démarre ». La sonde n'est pas la cause, elle est le messager. Sans sonde de disponibilité, des requêtes partent vers un pod qui ne peut pas les servir ; sans sonde de vie, un processus bloqué n'est jamais redémarré. On corrige la cause, ou le paramètre de la sonde s'il est faux (mauvais port, délai trop court), jamais en la retirant.

Chercher dans le mauvais objet. Pas de pod : les événements sont sur le ReplicaSet. Un Job en échec : sur le Job et ses pods. Une route non prise en compte : dans le statut de la HTTPRoute et de la Gateway, pas dans les journaux de l'application.

Oublier le namespace. kubectl get pods sans -n ne regarde que le namespace courant : un pod « disparu » est souvent ailleurs. kubectl config view --minify montre le contexte et le namespace en cours.

Conclure à l'absence quand c'est un refus. Avec des droits limités, kubectl get renvoie une erreur Forbidden ; dans d'autres outils, une liste vide. kubectl auth can-i list pods -n signalements lève le doute.

Retirer un finalizer sans comprendre. L'objet disparaît, et laisse derrière lui un volume, un répartiteur ou un enregistrement DNS chez le fournisseur, que plus aucun contrôleur ne suit.

Lire kubectl top comme une vérité. Il montre la consommation récente, pas les demandes que le planificateur réserve ; un pod Pending pour Insufficient cpu sur un cluster dont top montre des nœuds peu chargés n'a rien de contradictoire.

Sécurité

  • exec, debug et port-forward sont des accès. kubectl exec donne un shell dans le conteneur, avec ses secrets montés et ses variables d'environnement ; kubectl debug ajoute un conteneur au pod, éventuellement privilégié avec le profil sysadmin ; kubectl debug node/ donne accès au système de fichiers du nœud. Ces droits (sous-ressources pods/exec, pods/ephemeralcontainers, pods/portforward) se donnent avec parcimonie, et leur usage se journalise. Le cours Sécurité de Kubernetes détaille le RBAC correspondant.
  • Les journaux contiennent des données. Une erreur de connexion peut afficher une chaîne de connexion complète, mot de passe compris, si l'application la journalise. Vérifiez ce que l'application écrit avant de copier des journaux dans un ticket.
  • Un profil de débogage restreint dans un namespace restreint. Le profil restricted de kubectl debug respecte les règles du namespace ; contourner l'admission de sécurité pour déboguer, c'est ouvrir précisément ce qu'elle ferme.
  • Ne pas affaiblir la configuration pour diagnostiquer. Retirer readOnlyRootFilesystem, passer en root ou désactiver les sondes « le temps de comprendre » finit trop souvent en production. Le conteneur éphémère et la copie de pod (--copy-to) existent pour déboguer sans toucher au pod de production.

En production

  • Exporter les événements. Avec une rétention d'une heure, les événements d'un incident nocturne ont disparu au matin. Un exportateur d'événements vers le système de journaux (Loki, ou Cockpit chez Scaleway) les conserve et permet de les chercher.
  • Alerter sur les symptômes que voit l'utilisateur, puis sur les causes fréquentes : pods en CrashLoopBackOff, pods non prêts depuis plusieurs minutes, Deployment dont la mise à jour n'avance plus, Jobs en échec. Le cours Prometheus et le cours Concevoir une alerte utile en traitent.
  • Préparer une image de débogage. Une image interne, avec les outils réseau et PostgreSQL utiles (nslookup, curl, psql), épinglée par version et analysée comme les autres, évite de tirer une image publique inconnue en plein incident.
  • Écrire les runbooks. Chacun des six scénarios de cette leçon est un runbook en puissance : symptôme, commandes, causes possibles, correction. C'est ce que l'équipe de Lyneko consigne pour ses applications du cluster lyneko-apps.
  • Passer par Git pour corriger. En GitOps, une correction faite à chaud avec kubectl est annulée par la prochaine synchronisation (GitOps avec Argo CD, leçon 3) : on diagnostique dans le cluster, on corrige dans le dépôt.

Exercices

1. Lire un état (niveau 100). Pour chaque ligne de kubectl get pods, dites où chercher et quelle est la cause la plus probable : (a) signalements-... 0/1 ImagePullBackOff 0 4m ; (b) signalements-... 0/1 CrashLoopBackOff 6 9m ; (c) signalements-... 0/1 Running 0 3m ; (d) signalements-... 0/1 Pending 0 10m.

Solution

(a) Image introuvable ou inaccessible : kubectl describe pod, événements Failed, qui disent si l'étiquette n'existe pas, si l'accès est refusé ou si la limite de débit est atteinte. (b) Le conteneur meurt au démarrage : describe pod pour Last State (raison, code de sortie, OOMKilled ou non), puis kubectl logs --previous pour le message de l'application. (c) Le conteneur tourne mais la sonde de disponibilité échoue : événements Unhealthy dans describe pod, puis la dépendance testée par la sonde (ici la base). (d) Pas de nœud pour ce pod : événement FailedScheduling dans describe pod, avec la raison pour chaque nœud (ressources, PVC, teintes, affinités).

2. Un code de sortie (niveau 200). Après une mise à jour, les pods redémarrent avec Last State: Terminated, Reason: Error, Exit Code: 137, sans OOMKilled. Les journaux du conteneur précédent se terminent par une requête en cours. Que s'est-il probablement passé, et que vérifiez-vous ?

Solution

137, c'est 128 + 9 : le processus a reçu SIGKILL. Sans la raison OOMKilled, ce n'est pas la limite de mémoire. L'hypothèse la plus probable est la sonde de vie : elle a échoué plusieurs fois (application trop lente à répondre, ou délais trop courts), le kubelet a arrêté le conteneur, et comme le processus ne s'est pas arrêté dans le délai de grâce, il l'a tué. On le vérifie dans les événements (Unhealthy sur la sonde de vie, puis Killing), et l'on regarde si la nouvelle version démarre plus lentement que ce que la sonde de démarrage tolère, ou si Gunicorn ignore SIGTERM plus longtemps que terminationGracePeriodSeconds.

3. Le 503 (niveau 200). Après un renommage des étiquettes du Deployment de app à app.kubernetes.io/name, l'application renvoie 503. Décrivez les trois commandes qui le démontrent en moins d'une minute, et la correction.

Solution

kubectl get endpointslices -l kubernetes.io/service-name=signalements : aucune adresse. kubectl get service signalements -o jsonpath='{.spec.selector}' : le sélecteur utilise encore l'ancienne étiquette app. kubectl get pods --show-labels : les pods portent la nouvelle. Correction : mettre à jour le sélecteur du Service (et celui du PodDisruptionBudget, qui a le même problème et ne protège plus rien). Le sélecteur d'un Deployment n'étant pas modifiable, un renommage d'étiquettes demande de recréer le Deployment : à planifier, pas à improviser.

4. Bloqué en Terminating (niveau 200). Vous supprimez le namespace d'un environnement d'essai ; une heure plus tard, il est toujours Terminating. Comment trouvez-vous la cause, et que ne faites-vous surtout pas en premier ?

Solution

kubectl describe namespace <nom> : ses conditions de statut indiquent quels objets restent et quels finalizers les retiennent. On liste ensuite ces objets et leurs metadata.finalizers. La cause habituelle est un contrôleur désinstallé avant ses objets (par exemple un opérateur ou External Secrets supprimés avant leurs ressources), ou un service d'API agrégé indisponible, qui empêche de lister certains types d'objets. On ne commence pas par retirer les finalizers à la main : on réinstalle ou on répare le contrôleur concerné pour qu'il fasse son nettoyage, et l'on ne force le retrait qu'en dernier recours, en nettoyant soi-même ce que le contrôleur aurait dû supprimer (un répartiteur, un volume, une entrée DNS chez le fournisseur).

Récapitulatif

  • Méthode : du symptôme à l'objet (pods, contrôleur, Service, nœud), observer avant d'agir, lire les messages en entier, une modification à la fois.
  • Quatre commandes couvrent l'essentiel : get pods -o wide, describe (et ses événements), kubectl events --for, logs --previous.
  • Les événements expirent après une heure par défaut et sont attachés à l'objet concerné : pas de pod, regarder le ReplicaSet.
  • États : Pending (placement, FailedScheduling), ImagePullBackOff (image), CreateContainerConfigError (Secret ou ConfigMap), CrashLoopBackOff (démarrage ; OOMKilled et code 137 pour la mémoire), Running non prêt (sonde de disponibilité), Terminating qui dure (finalizers).
  • Un Service sans points de terminaison est un problème de sélecteur ou de disponibilité, jamais de réseau ; port-forward isole l'application.
  • Le conteneur éphémère (kubectl debug --target, profil adapté au namespace) débogue une image sans shell sans la modifier.
  • exec et debug sont des accès privilégiés ; on ne désactive ni les sondes ni la sécurité pour diagnostiquer.

Pour aller plus loin

  • Les pages Debug Pods, Debug Running Pods et Debug Services de la documentation de Kubernetes, qui suivent la même logique que cette leçon, avec des exemples de sorties.
  • La page Debugging DNS Resolution, pour les problèmes de DNS du cluster lui-même (CoreDNS).
  • Le cours Le modèle TCP/IP, dont la méthode réseau s'applique à l'intérieur des pods.
  • Le cours Kubernetes : administrer un cluster, pour les pannes de nœuds et du plan de contrôle.
  • Le quiz du cours, puis le lab, qui reprend le déploiement de la leçon 11.
Voir ma constellation →

Sources