Guide

Kubernetes installer un cluster et comprendre ce que vous déployez

Apprendre Kubernetes depuis zéro : installer un cluster local, pods, deployments, services, stockage, RBAC, débogage. Commandes kubectl et exercices.

Par Cyril · Publié le

Sommaire

Ce guide a été construit pour vous mener aux deux certifications d'entrée de la CNCF : la KCNA (Kubernetes and Cloud Native Associate) et la KCSA (Kubernetes and Cloud Security Associate). C'est la porte d'entrée idéale : les seize chapitres posent le socle sur lequel les deux programmes s'appuient, dans l'ordre où il se comprend. La rubrique de révision dédiée aux deux certifications est en ligne, avec le programme détaillé de chacune et un quiz d'entraînement.

Kubernetes est un système qui prend des conteneurs, des machines, et décide tout seul quel conteneur tourne sur quelle machine, le redémarre quand il meurt, le déplace quand la machine disparaît, et distribue le trafic entre les copies qui répondent. Vous lui décrivez l'état que vous voulez, il se débrouille pour l'atteindre et pour y rester. C'est tout. Le reste, les quatre-vingts types de ressources, les trois cents options de kubectl, les schémas d'architecture avec quatorze boîtes, découle de cette phrase.

Trois mots reviendront à chaque page, et ils sont définis avec une comparaison au moment où ils comptent : un nœud est une machine, le control plane est ce qui pilote les machines, un pod est l'unité qu'on déploie dessus, un ou plusieurs conteneurs qui vivent ensemble.

Ce guide s'adresse à quelqu'un qui n'a jamais lancé un pod. Il reste utile à quelqu'un qui en lance tous les jours sans avoir jamais vraiment lu ce qu'il faisait, et je sais que vous êtes nombreux, parce que je l'ai été.

Il est écrit pour Kubernetes 1.36, dont le dernier correctif, 1.36.4, est sorti le 11 août 2026 et qui est supportée jusqu'au 28 juin 2027. La 1.37 existe depuis le 26 août 2026 et ne change rien à ce qui suit, mais la 1.36 est celle que vous trouverez le plus souvent sur un cluster géré cet automne : les fournisseurs proposent rarement une version mineure le jour de sa sortie.

Au programme, seize chapitres qui se lisent dans l'ordre la première fois et dans le désordre ensuite :

  • pourquoi Kubernetes existe, et quand vous n'en avez pas besoin ;
  • ce qu'est un conteneur, et comment installer Kubernetes en local en dix minutes ;
  • l'architecture, le modèle déclaratif et la boîte à outils kubectl ;
  • puis chaque type de ressource, avec son manifeste minimal, ses commandes propres, un exercice complet et le piège qui coûte une soirée ;
  • enfin le débogage, l'extension de l'API et l'outillage.

Le périmètre s'arrête au socle : ce qui relève de l'observabilité outillée, du GitOps, du threat model et du durcissement fera l'objet de documents séparés, orientés certifications.

Aucun prérequis au-delà d'un terminal et d'un moteur de conteneurs sur votre machine, Docker ou Podman, qui ne servira qu'à faire tourner le cluster local du chapitre 3.

Un mot avant de commencer : accrochez-vous, ce sera long. Le guide se lit en deux bonnes heures d'affilée, et c'est exactement la mauvaise façon de s'y prendre. Prenez un chapitre ou deux par jour, faites l'exercice de chacun sur votre propre cluster, et laissez la nuit passer entre deux. Chaque chapitre se termine par une carte de rappel : si elle ne vous dit rien le lendemain, relisez le chapitre au lieu d'attaquer le suivant. Dix jours à ce rythme valent mieux qu'un samedi après-midi, parce que vous retiendrez ce que vous avez tapé, pas ce que vous avez lu.

Chapitre 1Pourquoi Kubernetes existe, et quand vous n'en avez pas besoin

Le problème que Kubernetes résout n'est pas "faire tourner des conteneurs". Docker fait ça très bien depuis 2013, et le guide Docker vous suffit si c'est votre besoin. Le problème est : faire tourner des centaines de processus sur des dizaines de machines sans qu'un humain décide où va chaque processus, sans qu'un humain se lève la nuit quand une machine tombe, et sans qu'un humain recalcule quoi que ce soit quand il faut passer de dix copies à trente.

Google a rencontré ce problème avant tout le monde et l'a résolu en interne avec un système appelé Borg, dont l'existence n'a été documentée publiquement qu'en 2015. Kubernetes est la réécriture ouverte des idées de Borg, publiée en 2014, passée en version 1.0 le 21 juillet 2015 à l'OSCON, où fut annoncée dans le même mouvement la création de la Cloud Native Computing Foundation. Le projet y a été formellement accepté en mars 2016, et en est le premier diplômé.

Le nom vient du grec et signifie "pilote", d'où le gouvernail sur le logo, d'où les sept rayons, d'où le surnom K8s pour "K, huit lettres, s". Voilà, vous savez tout ce qu'on vous demandera un jour en soirée.

Le contre-argument, et il est sérieux

Trois conteneurs sur un VPS n'ont rien à faire dans un cluster Kubernetes. Je le dis en ouverture parce que le reste du guide va vous donner envie de tout déployer dedans, et que c'est une erreur classique, coûteuse, et que j'ai commise.

Kubernetes introduit un plan de contrôle à maintenir, une couche réseau à comprendre, un modèle de stockage à apprivoiser, et des mises à jour trois fois par an. En échange, il vous donne du placement automatique, de l'auto-réparation et de la montée en charge sur plusieurs machines. Si vous n'avez qu'une machine, vous payez tout le premier terme et vous ne touchez rien du second.

Les alternatives honnêtes, pour un serveur ou deux :

  • Docker Compose : un fichier, docker compose up -d, et vos services tournent avec leur réseau et leurs volumes. C'est le bon outil pour la grande majorité des projets personnels et des petites prods.
  • systemd : des unités bien écrites, avec Restart=on-failure, du sandboxing et des timers, font tourner un service en production sans rien installer. Le guide systemd montre comment.
  • Nomad : l'orchestrateur de HashiCorp, un seul binaire, beaucoup plus simple à opérer que Kubernetes, pour ceux qui ont vraiment plusieurs machines mais pas l'envie d'un cluster complet.
  • Une PaaS : Fly.io, Railway, Render, Clever Cloud, Scalingo. Vous poussez du code, quelqu'un d'autre gère l'orchestration. Ça coûte plus cher par unité de calcul et beaucoup moins cher en heures d'ingénieur.

Ce qu'il apporte, ce qu'il coûte

Kubernetes vous apporteKubernetes vous coûte
Le placement automatique des conteneurs sur les machinesUn plan de contrôle à héberger, sauvegarder et mettre à jour
Le redémarrage et le déplacement automatiques en cas de panneUne couche réseau virtuelle à comprendre avant de pouvoir déboguer
Le passage de 3 à 30 copies en une commandeUn modèle de stockage qui ne ressemble à rien de ce que vous connaissez
Une API unique, déclarative, versionnée, la même partoutUne courbe d'apprentissage qui se compte en semaines, pas en heures
Un écosystème immense : un outil existe pour chaque besoinUn écosystème immense : il faut choisir, et les choix vieillissent vite
Le même outillage du laptop au cloudTrois versions mineures par an, un an de support chacune

Le critère de bascule

Il ne se mesure pas en conteneurs. Il se mesure en machines, et en ce qui doit se passer quand l'une d'elles disparaît.

Si vous avez une machine, la question ne se pose pas. Si vous en avez deux et qu'un humain peut basculer à la main en cas de panne, la question ne se pose toujours pas. Si vous avez trois machines ou plus et que la perte de l'une d'elles doit être absorbée sans intervention humaine, sans coupure visible, et sans que quelqu'un se réveille, alors vous avez le problème que Kubernetes résout, et le prix commence à valoir le coup.

Il y a une seconde raison légitime, moins avouable : Kubernetes est devenu l'API commune de l'infrastructure. Les outils, les recrutements, les fournisseurs cloud et une bonne partie des offres de logiciels s'expriment en manifestes Kubernetes. L'apprendre est rentable même si votre prod n'en a pas besoin. C'est précisément la raison d'être de ce guide.

Chapitre 2Les conteneurs, la brique de base

Une mise au point d'abord, parce que la confusion est fréquente : Kubernetes n'a pas de rapport particulier avec Docker.

Kubernetes fait tourner des conteneurs, et un conteneur est une notion standardisée, portée par l'OCI (Open Container Initiative), que Docker a popularisée en 2013 mais ne possède pas. Docker est un outil qui construit des images et lance des conteneurs sur une machine ; Podman, Buildah, nerdctl ou BuildKit font la même chose, et produisent les mêmes images.

Kubernetes intervient après. Il prend des images, d'où qu'elles viennent, et les fait tourner sur plusieurs machines. Il ne construit rien, et il n'a pas besoin de Docker pour exécuter quoi que ce soit. Ce guide parle donc de conteneurs, et ne cite Docker que là où l'histoire ou l'outillage l'imposent.

Quatre mots à poser proprement, parce que la suite est incompréhensible quand ils sont flous. Si vous les connaissez déjà, passez directement au CRI.

Une image est un paquet immuable qui contient un système de fichiers complet, vos binaires, vos dépendances, et quelques métadonnées comme la commande à lancer. Elle se construit une fois, à partir d'une recette, un Dockerfile ou un Containerfile, avec l'outil de votre choix, et ne change plus jamais. Elle est faite de couches empilées, chacune correspondant à une instruction de la recette, ce qui permet de partager les couches communes entre images et de ne télécharger que ce qui change.

Un registre est le serveur qui stocke et distribue les images, dans le même format quel que soit l'outil qui les a construites. Docker Hub est le plus connu, mais chaque cloud a le sien, GitHub en a un, et vous pouvez héberger le vôtre. Une image y est désignée par registre/organisation/nom:tag, par exemple ghcr.io/rudeops/api:1.4.2. Sans registre explicite, c'est Docker Hub ; sans tag, c'est latest, et on verra plus loin pourquoi c'est une mauvaise idée.

Un conteneur est une instance en cours d'exécution d'une image. C'est un processus ordinaire de la machine, isolé par le noyau Linux : il voit son propre système de fichiers, son propre réseau, ses propres PID, mais il partage le noyau avec tous les autres. Ce n'est pas une machine virtuelle. Il démarre en millisecondes, et il meurt quand son processus principal se termine.

La confusion image / conteneur est celle que tout le monde fait, et elle empêche de comprendre Kubernetes. Vous ne déployez pas des conteneurs dans Kubernetes. Vous lui donnez des images, et vous lui demandez d'en faire tourner un certain nombre d'instances. Les conteneurs sont ce qu'il crée, détruit et recrée à sa guise pour honorer cette demande.

Le CRI, containerd et l'histoire de dockershim

Kubernetes ne lance pas les conteneurs lui-même. Sur chaque machine, un agent appelé kubelet demande à un runtime de conteneurs de le faire, à travers une interface standardisée, le CRI (Container Runtime Interface). Les deux runtimes que vous croiserez sont containerd, extrait de Docker et devenu le standard de fait, et CRI-O, porté par Red Hat. Vous ne les manipulerez à peu près jamais directement.

Jusqu'à la version 1.23, le kubelet savait aussi parler à Docker Engine, à travers un adaptateur maison appelé dockershim. Cet adaptateur a été déprécié en 1.20 et supprimé en 1.24. La nouvelle a été relayée sous la forme "Kubernetes abandonne Docker", ce qui est faux et continue de circuler.

Ce qui a disparu, c'est l'adaptateur entre le kubelet et Docker Engine. Ce qui n'a pas bougé, c'est le format des images : une image construite par Docker est une image OCI, le standard que containerd et CRI-O exécutent nativement, exactement comme une image construite par Podman ou Buildah.

Vos Dockerfiles, vos images, vos registres, votre pipeline de build : rien ne change. Docker reste l'outil de construction le plus répandu, et l'immense majorité des images qui tournent dans des clusters Kubernetes en 2026 ont été construites avec lui.

Le piègeDocker n'est pas mort

"Docker est mort" est la contre-vérité la plus répandue sur le sujet, et elle a une conséquence pratique : des équipes ont dépensé des semaines à migrer leurs builds vers d'autres outils pour rien. Docker construit les images, Kubernetes les exécute via containerd. Les deux cohabitent, et c'est même l'arrangement normal.

Chapitre 3Installer Kubernetes en local : kind, minikube, k3s ou Talos

Sans cluster, vous ne ferez rien de la suite. Il en faut un sur votre machine, jetable, que vous pourrez casser sans conséquence. Quatre options se partagent l'usage :

OutilCe qu'il simule fidèlementCe qu'il faussePour qui
kindUn vrai cluster multi-nœuds, chaque nœud étant un conteneur sur votre machine. La version exacte de Kubernetes de votre choix.Pas de LoadBalancer, pas de stockage cloud, un CNI minimalSuivre ce guide, tester des manifestes, la CI
minikubeUn cluster à un nœud, avec des addons prêts à activer (ingress, metrics-server, dashboard)Un seul nœud par défaut : rien de ce qui touche au placement multi-machinesDécouvrir sans se poser de questions
k3sUne distribution Kubernetes complète et allégée, la même en local et sur un vrai serveurQuelques composants remplacés (SQLite à la place d'etcd par défaut, Traefik intégré)Un homelab, de l'edge, une petite prod
TalosUn OS immuable conçu pour Kubernetes, sans shell ni SSH, piloté entièrement par APIRien : c'est un cluster de prod. C'est aussi plus lourd à prendre en mainCeux qui veulent voir à quoi ressemble un cluster géré proprement

Ce guide utilise kind parce qu'il est le plus proche d'un vrai cluster : plusieurs nœuds, la version de Kubernetes que vous voulez, et un cluster qui se crée et se détruit en une minute. Les exercices fonctionnent sur les trois autres, avec les adaptations signalées quand il y en a.

Installer kubectl

kubectl est le client en ligne de commande. Il parle à n'importe quel cluster, local ou distant, et c'est l'outil que vous utiliserez dans tout le guide.

# Linux amd64
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# macOS
brew install kubectl

# Vérifier
kubectl version --client

stable.txt donne la dernière version publiée, une 1.37 en septembre 2026 ; elle parle sans problème à un cluster 1.36, voir la règle ci-dessous. Sur Arch, pacman -S kubectl ; sur Debian et Ubuntu, le dépôt officiel pkgs.k8s.io est documenté sur kubernetes.io et vaut mieux que le paquet de la distribution, souvent en retard de plusieurs versions.

La règle : votre kubectl doit être à une version mineure près de votre cluster. Un kubectl 1.36 parle à un cluster 1.35 ou 1.37 ; au-delà, des commandes commencent à manquer ou à se comporter bizarrement.

Créer le cluster avec kind

kind demande Docker (ou Podman) en fonctionnement.

# Linux
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind

# macOS
brew install kind

Un cluster à un nœud se crée avec kind create cluster, mais pour suivre les chapitres sur le placement il vous faut plusieurs nœuds. Écrivez ce fichier :

# kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker

Puis :

kind create cluster --name rudeops --config kind.yaml

Une minute plus tard, vous avez trois conteneurs qui jouent chacun le rôle d'une machine, et kind a écrit dans ~/.kube/config de quoi vous y connecter. Vérifiez :

kubectl cluster-info
kubectl get nodes
NAME                    STATUS   ROLES           AGE   VERSION
rudeops-control-plane   Ready    control-plane   62s   v1.36.4
rudeops-worker          Ready    <none>          40s   v1.36.4
rudeops-worker2         Ready    <none>          40s   v1.36.4

Chaque version de kind embarque une image de nœud par défaut. Si kubectl get nodes vous affiche autre chose qu'une 1.36, précisez l'image dans le fichier de configuration avec image: kindest/node:v1.36.4 sur chaque nœud, ou laissez la version par défaut de kind si c'est une 1.37 : rien dans ce guide ne dépend de la différence. Les tags disponibles sont listés dans les notes de release de kind.

Pour tout jeter et recommencer : kind delete cluster --name rudeops.

Les variantes courtes

# minikube : un nœud, driver Docker
minikube start --driver=docker
# ou plusieurs nœuds
minikube start --driver=docker --nodes 3

# k3s : sur une machine Linux, en une ligne
curl -sfL https://get.k3s.io | sh -
# le kubeconfig est dans /etc/rancher/k3s/k3s.yaml
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

# Talos : un cluster dans Docker, piloté par talosctl
talosctl cluster create

Le piègepas de LoadBalancer en local

Un cluster local ne fournit ni LoadBalancer ni provisionneur de stockage identique à ceux d'un cloud. Concrètement : un Service de type LoadBalancer restera en <pending> indéfiniment sur kind, et une StorageClass qui réclame des disques SSD répliqués n'existera pas.

Ce n'est pas un bug, c'est l'absence du composant cloud qui fait le travail. Retenez-le maintenant, parce que la moitié des "ça ne marche pas" au chapitre réseau vient de là.

kind fournit tout de même un provisionneur de stockage local (standard), suffisant pour les exercices du chapitre stockage, et cloud-provider-kind ajoute un LoadBalancer fonctionnel si vous en avez vraiment besoin.

Chapitre 4L'architecture d'un cluster

Le control plane, c'est le cerveau. Il décide de tout et n'exécute rien. Un nœud, c'est un bras : une machine qui exécute et ne décide rien. Un cluster, c'est un cerveau et ses bras.

Trois mots à fixer avant le schéma, parce que tout le reste s'appuie dessus.

Un nœud est une machine. Un serveur physique dans un rack, une VM chez un cloud, ou, sur kind, un conteneur qui joue le rôle d'une machine. C'est là que vos conteneurs tournent réellement, avec le CPU, la mémoire et le disque de cette machine. Un cluster en a d'un seul, sur minikube, à plusieurs milliers.

Le control plane est l'ensemble des programmes qui pilotent ces machines. Il reçoit vos demandes, les enregistre, décide sur quel nœud va chaque chose, et surveille en permanence que la réalité correspond à ce qui a été demandé. Il ne fait tourner aucune de vos applications : il décide, les nœuds exécutent. Sur un cluster géré par un cloud (GKE, EKS, AKS, et les autres), le control plane vous est invisible et facturé à part ; sur kind, il tourne dans le conteneur rudeops-control-plane.

Un cluster, c'est un control plane plus ses nœuds.

Si vous voulez une image, prenez une entreprise de logistique. Le control plane est le siège, et le siège a quatre bureaux :

  • un guichet unique par lequel passent toutes les demandes, qu'elles viennent d'un client ou d'un employé (kube-apiserver) ;
  • un registre où est consigné tout ce qui existe, chaque commande, chaque entrepôt, chaque palette, et ce registre est la seule vérité de l'entreprise (etcd) ;
  • un planificateur qui, pour chaque nouvelle commande, choisit l'entrepôt qui a la place et l'équipement (kube-scheduler) ;
  • des contrôleurs qui passent leur journée à comparer le registre à la réalité et à corriger l'écart : il manque une palette quelque part, on la recrée ailleurs (kube-controller-manager).

Le siège ne stocke rien et ne livre rien lui-même.

Les nœuds sont les entrepôts. Chacun a trois personnes :

  • un chef d'entrepôt qui ne prend ses ordres que du siège et lui rend compte de ce qui se passe chez lui (kubelet) ;
  • un aiguilleur qui tient à jour le plan de routage pour que les colis arrivent au bon quai (kube-proxy) ;
  • des caristes qui déplacent physiquement les conteneurs (le runtime).

Si un entrepôt brûle, le siège le constate parce que son chef ne répond plus, et réaffecte ses commandes aux autres. Aucun entrepôt ne parle directement au registre, ni à un autre entrepôt pour se coordonner : tout remonte au guichet. C'est exactement ce que le schéma dessine.

Architecture d'un cluster KubernetesLe control plane regroupe kube-apiserver au centre, etcd qui est le seul composant à stocker l'état, kube-scheduler, kube-controller-manager et, sur un cloud uniquement, cloud-controller-manager. Deux nœuds de travail contiennent chacun kubelet, kube-proxy, un runtime de conteneurs et des pods. Toutes les flèches convergent vers kube-apiserver : kubectl, le scheduler, les controller managers et chaque kubelet lui parlent. Seul kube-apiserver lit et écrit dans etcd.CONTROL PLANENŒUD 1NŒUD 2kubectlkube-apiserverle seul point d'entréeetcdseul à stockerl'étatkube-schedulerkube-controller-managercloud-controller-managercloud uniquementkubeletagentkube-proxyréseaucontainerdruntimePodPodkubeletagentkube-proxyréseaucontainerdruntimePodPodrien d'autrene lui parle
Tout passe par kube-apiserver · seul lui parle à etcd, l'unique état du cluster

Le control plane

kube-apiserver, c'est le guichet unique : ce qui ne passe pas par lui n'existe pas. etcd, c'est la mémoire : ce qui n'y est pas écrit n'existe pas. kube-scheduler, c'est le dispatcheur : il choisit la machine, il ne lance rien. kube-controller-manager, c'est le contremaître : il compare le plan à la réalité, en boucle, toute la journée.

  • kube-apiserver est le point d'entrée unique. Tout, absolument tout, passe par lui : vos commandes kubectl, les décisions du scheduler, les rapports des nœuds. Il valide, authentifie, autorise, puis lit et écrit dans etcd. C'est le seul composant qui parle à etcd.
  • etcd est la base de données. C'est un magasin clé-valeur distribué, et il contient l'intégralité de l'état du cluster : chaque ressource que vous avez créée, chaque état rapporté, chaque secret. Rien d'autre n'y est stocké nulle part ailleurs.
  • kube-scheduler regarde les pods qui n'ont pas encore de nœud et leur en choisit un, en fonction des ressources disponibles et des contraintes que vous avez exprimées. Il ne lance rien lui-même : il écrit sa décision dans l'API, et le kubelet du nœud choisi fait le reste.
  • kube-controller-manager regroupe les contrôleurs : des boucles qui surveillent chacune un type de ressource et agissent pour que la réalité corresponde à ce qui est déclaré. Le contrôleur de Deployment, celui de ReplicaSet, celui de Job, celui de Node, et une trentaine d'autres tournent dans ce seul processus.
  • cloud-controller-manager n'existe que sur un cloud. Il fait le lien entre l'API Kubernetes et l'API du fournisseur : créer un load balancer, attacher un disque, étiqueter un nœud avec sa zone. Sur kind, il n'y en a pas, et c'est pour ça que les Services LoadBalancer y restent en attente.

Les nœuds

kubelet, c'est le chef d'entrepôt : il ne prend ses ordres que du siège et lui rend compte. kube-proxy, c'est l'aiguilleur : il fait arriver le trafic au bon quai. Le runtime, c'est le cariste : il déplace physiquement les conteneurs.

  • kubelet est l'agent qui tourne sur chaque nœud. Il surveille l'API server, y lit les pods qui lui sont assignés, demande au runtime de lancer les conteneurs correspondants, et rapporte leur état. Il ne parle qu'à l'API server, jamais à etcd.
  • kube-proxy programme les règles réseau du nœud pour que le trafic destiné à un Service arrive aux bons pods. On y revient au chapitre réseau. Certains CNI le remplacent entièrement.
  • Le runtime de conteneurs, containerd ou CRI-O, lance et arrête les conteneurs sur ordre du kubelet.

Pourquoi l'API server est au centre de tout

Le schéma dit une chose et une seule : tout converge vers kube-apiserver, et rien ne parle directement à etcd.

Ce n'est pas un détail d'implémentation, c'est ce qui rend tout le reste possible. Puisqu'il n'y a qu'une porte, c'est sur cette porte qu'on met l'authentification, les autorisations (RBAC, chapitre sécurité), les contrôleurs d'admission qui valident ou modifient les objets avant leur écriture, et la possibilité d'ajouter vos propres types de ressources (chapitre extension).

Un composant qui contournerait l'API server contournerait tout ça. Il n'y en a pas.

Encart etcd. etcd est le seul état du cluster. Les nœuds n'ont pas de copie, le control plane n'a pas de cache durable, kubectl n'a rien en local.

Le jour où vous perdez etcd sans sauvegarde, vous perdez la définition de tout ce qui tournait. Les conteneurs continuent de tourner sur les nœuds tant que leur kubelet ne reçoit pas d'ordre contraire, mais plus rien ne les répare ni ne les remplace, et vous redéployez tout depuis vos manifestes, en espérant qu'ils étaient bien tous dans un dépôt Git.

Sur un cluster géré par un cloud, la sauvegarde d'etcd est le problème du fournisseur. Sur un cluster que vous administrez vous-même, etcdctl snapshot save planifié et testé est la seule sauvegarde qui compte vraiment ; tout le reste se reconstruit.

Les commandes pour inspecter le cluster

CommandePour quoi
kubectl cluster-infoL'adresse de l'API server et du DNS du cluster
kubectl get nodes -o wideLes nœuds, leur version, leur OS, leur runtime et leur IP
kubectl describe node rudeops-workerTout sur un nœud : capacité, ressources allouées, conditions, pods hébergés
kubectl get --raw /readyzL'API server est-il prêt à servir, en un mot. /readyz?verbose détaille chaque vérification

Vous croiserez kubectl get componentstatuses dans des tutoriels anciens. La commande est dépréciée depuis 1.19 et ne rapporte plus grand-chose de fiable. Ne la cherchez pas ; kubectl get pods -n kube-system vous dit la même chose, en mieux.

Chapitre 5Parler à l'API : le modèle déclaratif et kubectl

Déclaratif, c'est donner la destination, pas l'itinéraire. Vous écrivez l'état voulu, un contrôleur trouve le chemin et y reste. Un manifeste, c'est cette destination écrite en YAML.

C'est le chapitre pivot du guide. Ce qui suit vaut pour toutes les ressources, et ne sera plus réexpliqué : à partir du chapitre suivant, chaque chapitre ne montre que ce qui est propre à sa ressource.

Déclarer, pas ordonner

Avec la plupart des outils, vous donnez des ordres : lance ce processus, ouvre ce port, copie ce fichier. Avec Kubernetes, vous décrivez un état : "je veux trois copies de cette image, exposées sur ce port". Vous écrivez cette description dans l'API, et un contrôleur se charge d'y arriver, puis d'y rester.

Chaque ressource a deux parties qui se répondent. spec est ce que vous voulez ; c'est vous qui l'écrivez. status est ce qui est ; c'est le système qui le remplit, et vous ne le modifiez jamais. Le travail d'un contrôleur tient en une boucle : lire spec, lire status, comparer, agir pour réduire l'écart, recommencer.

La boucle de réconciliationQuatre temps : vous déclarez l'état voulu dans la spec, l'API server l'enregistre dans etcd, un contrôleur compare la spec au status, puis agit pour réduire l'écart. Les temps 3 et 4 se répètent en permanence, pas une seule fois : c'est une boucle fermée, pas un pipeline qui se termine.en permanence1. Vous déclarezl'état vouluspec2. L'API serverenregistredans etcd3. Le contrôleurcomparespec ≠ status ?4. Le contrôleuragit sur l'écartcrée ou supprimeune seule foisla boucle 3 → 4 tourne tant que le cluster tourne
Vous écrivez spec, le contrôleur poursuit status· un pod supprimé revient parce que la boucle ne s'arrête jamais

Cette boucle ne s'arrête jamais. C'est ce qui explique le comportement de tout le reste : un pod que vous supprimez à la main revient, parce que le Deployment qui le possède a une spec qui dit "trois copies" et un status qui n'en compte plus que deux. Une machine qui tombe voit ses pods recréés ailleurs, pour la même raison. Vous ne redémarrez pas un service ; vous changez sa description, et le système converge. Une fois que ce modèle est en place dans votre tête, Kubernetes cesse d'être magique.

Anatomie d'un manifeste

Un manifeste est un fichier YAML qui décrit une ressource. Tous, sans exception, ont la même structure :

apiVersion: apps/v1        # le groupe d'API et sa version
kind: Deployment           # le type de ressource
metadata:                  # ce qui identifie l'objet
  name: api
  namespace: production
  labels:
    app: api
spec:                      # ce que vous voulez
  replicas: 3
  # ...

apiVersion mérite d'être compris une fois pour toutes. L'API Kubernetes est découpée en groupes, chacun versionné indépendamment.

  • Le groupe historique n'a pas de nom et s'écrit simplement v1 : Pod, Service, ConfigMap, Secret, Namespace, PersistentVolumeClaim.
  • apps/v1 : Deployment, StatefulSet, DaemonSet.
  • batch/v1 : Job et CronJob.
  • networking.k8s.io/v1 : Ingress et NetworkPolicy.
  • rbac.authorization.k8s.io/v1 : les rôles.

La version indique la maturité : v1alpha1 peut changer ou disparaître, v1beta1 est en voie de stabilisation, v1 est stable et le restera. Quand un tutoriel de 2019 vous montre apiVersion: extensions/v1beta1, c'est un fossile.

kind est le type. metadata.name est le nom, unique par type et par namespace. metadata.labels sont des étiquettes clé-valeur qu'on utilise pour sélectionner ; on y consacre le chapitre suivant. spec dépend entièrement du type, et c'est là que kubectl explain devient indispensable.

apply, create, edit, replace

Il y a quatre façons d'écrire une ressource dans l'API, et une seule bonne.

  • kubectl create -f fichier.yaml crée l'objet et échoue s'il existe déjà.
  • kubectl replace -f fichier.yaml remplace l'objet entier et échoue s'il n'existe pas.
  • kubectl edit deployment/api ouvre l'objet dans votre éditeur ; la modification est immédiate et ne laisse aucune trace dans vos fichiers.
  • kubectl apply -f fichier.yaml crée l'objet s'il n'existe pas, le met à jour sinon, en ne touchant qu'aux champs que votre fichier déclare.

apply est le seul qui se rejoue. Lancez-le dix fois sur le même fichier, le résultat est le même. Modifiez le fichier et relancez, seule la différence est appliquée. Lancez-le sur un dossier entier, ça marche aussi.

C'est le verbe que vous utiliserez dans 95 % des cas, et le seul compatible avec l'idée que vos manifestes vivent dans Git et que le cluster n'est qu'une projection de ce dépôt. create sert à générer des manifestes, on va le voir. edit sert à réparer en urgence à trois heures du matin, en sachant que vous devrez reporter la modification dans le fichier ensuite.

Les commandes transversales

À apprendre ici, et jamais répétées dans la suite du guide.

CommandePour quoi
kubectl explain pod.spec.containers.resourcesLa documentation de l'API, lue dans le schéma de votre cluster, donc à sa version exacte et sans Internet. --recursive pour tout l'arbre
kubectl create deployment api --image=nginx --dry-run=client -o yamlGénérer un manifeste valide au lieu de l'écrire de mémoire. Fonctionne avec create pour la plupart des types
kubectl diff -f manifeste.yamlCe que l'apply va changer, avant de l'appliquer
kubectl apply -f fichier.yaml / -f dossier/ / -k dossier/Appliquer un fichier, un dossier, un kustomize
kubectl get X -o wideLes colonnes supplémentaires, selon le type : nœud et IP pour un pod, conteneurs et images pour un Deployment
kubectl get X -o yamlL'objet complet, spec et status compris, tel que le cluster le connaît
kubectl get X -o jsonpath='{.status.phase}'Extraire un champ. Au-delà de deux niveaux, -o json et jq sont plus lisibles
kubectl get X -ATous les namespaces (--all-namespaces)
kubectl get X --watchSuivre les changements en direct, -w en court
kubectl get X -l app=apiFiltrer par label
kubectl api-resourcesTous les types que ce cluster connaît, avec leur nom court et leur groupe
kubectl api-versionsTous les groupes et versions disponibles
kubectl config get-contexts / use-context nomLister les clusters connus, changer de cluster
kubectl wait --for=condition=Ready pod/X --timeout=60sBloquer jusqu'à ce qu'un objet atteigne un état, avant un exec ou un logs dans un script

Deux compléments. kubectl get X accepte les noms courts listés par api-resources : po pour pods, deploy pour deployments, svc pour services, ns pour namespaces, cm pour configmaps. Et kubectl apply --prune existe pour supprimer les objets qui ne sont plus dans vos fichiers, mais c'est un mécanisme ancien, fondé sur des labels, qui supprime ce qu'il croit vous appartenir : réservez la suppression à un outil qui sait ce qu'il a déployé, Kustomize, Helm ou un outil GitOps.

En pratiquegénérer, modifier, diff, apply

Générez un manifeste plutôt que de l'écrire :

kubectl create deployment hello --image=nginx:1.27 --replicas=2 \
  --dry-run=client -o yaml > hello.yaml

Ouvrez hello.yaml. Vous y trouverez un Deployment valide, avec un peu de bruit (creationTimestamp: null, status: {}) que vous pouvez supprimer. Modifiez replicas: 2 en replicas: 3. Puis :

kubectl apply -f hello.yaml
kubectl get deploy hello --watch

Vous verrez READY passer de 0/3 à 3/3 en quelques secondes. Modifiez maintenant l'image en nginx:1.28 dans le fichier, et regardez ce que l'apply va faire avant de le faire :

kubectl diff -f hello.yaml
kubectl apply -f hello.yaml

Le diff vous montre la ligne d'image qui change, plus une ou deux métadonnées que l'API met à jour toute seule, generation et l'annotation last-applied-configuration. C'est votre boucle de travail pour tout le reste du guide : générer, modifier, diff, apply. Supprimez avec kubectl delete -f hello.yaml.

Le piègekubectl explain, la commande que personne n'utilise

kubectl explain est la commande la plus sous-utilisée de tout l'écosystème. Elle rend inutile la moitié des recherches Google, elle n'a pas besoin d'Internet puisqu'elle lit le schéma publié par votre API server, et elle répond pour votre version et pas pour celle d'un tutoriel de 2021.

Vous ne savez plus si c'est restartPolicy ou restart_policy, ni où il se place ? kubectl explain pod.spec.restartPolicy. Vous voulez la liste de tout ce qu'accepte un conteneur ? kubectl explain pod.spec.containers --recursive.

Ça fonctionne aussi sur les ressources ajoutées par des tiers, on y revient au chapitre extension.

Chapitre 6Organiser : namespaces, labels, sélecteurs, annotations

Un namespace, c'est un dossier, pas un coffre-fort : il range, il ne protège pas. Un label, c'est l'étiquette qui sert à trier. Une annotation, c'est le post-it qui sert à se souvenir.

Un chapitre court, et un prérequis absolu pour la suite : un Service trouve ses pods par sélecteur de labels, jamais par nom. Si cette phrase ne vous dit rien, le chapitre réseau sera incompréhensible.

Le namespace

Un namespace est un cloisonnement logique à l'intérieur du cluster. Deux Deployments peuvent s'appeler api s'ils sont dans des namespaces différents ; les droits RBAC, les quotas de ressources et les NetworkPolicies se posent par namespace ; et kubectl get pods ne vous montre que le namespace courant, default tant que vous n'avez rien changé. Les composants du cluster vivent dans kube-system, et vous n'y touchez pas.

Ce que le namespace n'est pas : une frontière de sécurité.

Par défaut, un pod du namespace dev peut joindre un pod du namespace production par le réseau. Un utilisateur qui a des droits sur un namespace n'en a aucun sur les autres uniquement parce que RBAC le dit, pas parce que le namespace l'impose. L'isolation réseau demande des NetworkPolicies, l'isolation des droits demande RBAC. Et certaines ressources, les nœuds, les PersistentVolumes, les StorageClasses, ne sont dans aucun namespace du tout.

Le namespace organise ; il ne protège pas.

Label contre annotation

Un label est une paire clé-valeur posée dans metadata.labels, faite pour sélectionner. Tout ce qui, dans Kubernetes, désigne un ensemble d'objets le fait par labels : un Service choisit ses pods par labels, un Deployment reconnaît les siens par labels, une NetworkPolicy cible par labels, kubectl get -l filtre par labels. Les valeurs sont courtes, sans espace, et servent à être comparées.

Une annotation est aussi une paire clé-valeur, dans metadata.annotations, mais elle ne sert jamais à sélectionner. Elle porte de la donnée : la cause d'un déploiement, une configuration lue par un contrôleur tiers, un checksum, une URL de documentation. Les valeurs peuvent être longues et structurées.

La règle : si vous voulez pouvoir filtrer dessus, label. Si vous voulez juste que l'information voyage avec l'objet, annotation.

Kubernetes recommande un jeu de labels standardisés, que les outils de l'écosystème savent lire :

metadata:
  labels:
    app.kubernetes.io/name: api            # le nom de l'application
    app.kubernetes.io/instance: api-prod   # cette instance précise
    app.kubernetes.io/version: "1.4.2"
    app.kubernetes.io/component: backend
    app.kubernetes.io/part-of: rudeops

Dans ce guide, pour la lisibilité, les exemples utilisent des labels courts comme app: api. En production, adoptez les labels recommandés dès le premier jour ; les renommer après coup est pénible, parce que les sélecteurs d'un Deployment sont immuables une fois créés.

Les commandes des namespaces et des labels

CommandePour quoi
kubectl get nsLister les namespaces
kubectl create ns stagingEn créer un
kubectl get pods -n stagingCibler un namespace
kubectl label pod api-x env=prodPoser un label (env- pour le retirer, --overwrite pour le changer)
kubectl annotate deploy api rudeops.com/owner=cyrilPoser une annotation
kubectl get pods -l app=api,env=prodFiltrer par plusieurs labels ; -l 'env in (prod,staging)' pour un ensemble, -l '!canary' pour l'absence
kubectl get pods --show-labelsAfficher les labels en colonne
kubectl get pods --field-selector status.phase=RunningFiltrer sur un champ de l'objet plutôt qu'un label

En pratiqueun namespace, deux Deployments, des labels

kubectl create ns staging
kubectl create deployment api --image=nginx:1.27 -n staging
kubectl create deployment worker --image=nginx:1.27 -n staging

kubectl get pods -n staging --show-labels

Les deux Deployments ont posé un label app=api et app=worker sur leurs pods, tout seuls. Ajoutez un label à la main et filtrez :

kubectl label deploy api tier=frontend -n staging
kubectl get deploy -n staging -l tier=frontend
kubectl get deploy -n staging -l '!tier'

Le premier filtre renvoie api, le second renvoie worker. Remarquez que le label posé sur le Deployment n'est pas apparu sur ses pods : le label d'un objet est le sien, et ceux des pods viennent du template du Deployment, pas de ses propres métadonnées. On y revient au chapitre suivant.

Le piègetaper -n cinquante fois par jour

Taper -n staging cinquante fois par jour finit par en faire oublier un, et le cinquante-et-unième s'applique à default ou, pire, à production. Changez le namespace courant de votre contexte :

kubectl config set-context --current --namespace=staging

Toutes les commandes suivantes visent staging sans -n. Pour basculer d'un namespace à l'autre en une commande, kubens fait exactement ça, avec la complétion ; il est au chapitre outillage.

Chapitre 7Le pod

Un pod, c'est un appartement en colocation : une seule adresse, des colocataires qui se parlent dans le couloir, un bail commun. Kubernetes ne loue jamais une chambre seule.

Un pod est la plus petite unité que Kubernetes déploie. Ce n'est pas le conteneur, et la différence n'est pas une subtilité de vocabulaire.

Un pod est un groupe d'un ou plusieurs conteneurs qui partagent trois choses :

  • un espace réseau : une seule adresse IP, un seul ensemble de ports, et localhost qui désigne le pod entier ;
  • des volumes, déclarés au niveau du pod et montés dans les conteneurs qui en ont besoin ;
  • un cycle de vie : placés sur le même nœud, démarrés ensemble, arrêtés ensemble.

Dans 90 % des cas, un pod contient un seul conteneur, et vous pouvez mentalement les confondre. Les 10 % restants sont la raison d'être du concept.

L'image qui marche le mieux est celle de la colocation. Un pod est un appartement ; les conteneurs sont les colocataires.

  • Ils ont une seule adresse postale, l'IP du pod, et une seule porte d'entrée avec ses sonnettes, les ports : deux colocataires ne peuvent pas répondre tous les deux à la sonnette 8080.
  • Ils se parlent en criant dans le couloir, localhost, sans passer par la rue.
  • Ils partagent les pièces communes que le bail prévoit, les volumes, chacun y accédant depuis sa chambre, le point de montage.
  • Ils ont emménagé ensemble, et quand le bail s'arrête, tout le monde part le même jour : on ne déménage pas un colocataire seul dans un autre immeuble.

Et le propriétaire, Kubernetes, ne loue jamais une chambre : il loue des appartements entiers, même à une personne seule. C'est pour ça que l'unité de déploiement est le pod, jamais le conteneur. Dans l'entreprise de logistique du chapitre 4, ce qu'on place dans un entrepôt est une palette, pas un colis.

Anatomie d'un podUn pod contient ici deux conteneurs applicatifs, api et worker, un init container qui s'exécute et se termine avant les autres, et un sidecar, c'est-à-dire un init container avec restartPolicy: Always qui démarre avant et tourne pendant. Tous partagent un même network namespace avec une seule IP, 10.244.1.17, et se parlent sur localhost. Les volumes partagés sont déclarés au niveau du pod et montés dans les conteneurs applicatifs.PODnetwork namespace partagéune IP : 10.244.1.17 · localhostvolumes partagésdéclarés au pod, montés par conteneurinit containers'exécute et se termineavant les autressidecarinit container avec restartPolicy: Alwaysdémarre avant, tourne pendantapiconteneurworkerconteneur
Une IP par pod, pas par conteneur · les conteneurs se parlent sur localhost · le sidecar est un init container avec restartPolicy: Always

Plusieurs conteneurs, init containers et sidecars

Deux conteneurs dans un pod, c'est deux processus qui ont besoin l'un de l'autre au point de devoir vivre et mourir ensemble sur la même machine : une application et l'agent qui expédie ses logs, un serveur et le proxy qui chiffre son trafic. Le mot pour ce compagnon est sidecar.

Un init container est un conteneur qui s'exécute avant les autres et doit se terminer avec succès pour que le pod démarre : attendre qu'une base de données réponde, appliquer une migration, télécharger une configuration. S'il échoue, le pod relance l'init, encore et encore, et STATUS affiche Init:Error puis Init:CrashLoopBackOff ; tant qu'il tourne, c'est Init:0/1.

Depuis Kubernetes 1.33, les sidecars ont une définition officielle : un init container avec restartPolicy: Always. Il démarre avant les conteneurs applicatifs, comme un init, mais il ne se termine pas, tourne pendant toute la vie du pod, et s'arrête après les autres. C'est ce que le schéma montre.

Avant ce mécanisme, arrivé en alpha en 1.28 et stable en 1.33, un sidecar était un conteneur ordinaire de la liste containers, sans garantie d'ordre. Ça produisait des bugs de démarrage célèbres, et un Job qui ne finissait jamais parce que son sidecar de logs, lui, ne se terminait pas.

Le manifeste minimal d'un pod

apiVersion: v1
kind: Pod
metadata:
  name: web                    # le nom du pod, unique dans le namespace
  labels:
    app: web                   # le label que les Services et vous utiliserez pour le trouver
spec:
  containers:
    - name: nginx              # le nom du conteneur, pour kubectl logs -c et exec -c
      image: nginx:1.27        # l'image, avec un tag explicite
      ports:
        - containerPort: 80    # documentaire : le port n'est pas ouvert par cette ligne

containerPort est purement informatif ; le conteneur écoute sur 80 parce que nginx écoute sur 80, pas parce que vous l'avez écrit ici. La ligne sert aux humains, aux outils, et à kubectl expose qui s'en sert pour deviner le port.

image: nginx:1.27 et pas nginx, c'est la promesse du chapitre 2. Sans tag, c'est latest, un tag mobile qui désigne une image différente d'une semaine à l'autre. Trois conséquences :

  • deux pods d'un même Deployment peuvent tourner avec deux versions, selon le jour où leur nœud a téléchargé l'image ;
  • un apply ne déclenche aucun rollout, puisque le manifeste n'a pas changé ;
  • un rollout undo vous ramène vers un tag qui a changé lui aussi.

Un tag de version, ou mieux un digest @sha256:..., est la seule façon de savoir ce qui tourne et d'y revenir.

Les phases, et pourquoi STATUS n'en est pas une

Un pod traverse des phases : Pending (accepté mais pas encore tous ses conteneurs lancés, le plus souvent en attente d'un nœud ou d'une image), Running (placé, au moins un conteneur tourne), Succeeded (tous les conteneurs terminés avec succès, pour un Job), Failed (au moins un terminé en erreur, sans redémarrage prévu) et Unknown (le nœud ne répond plus).

La colonne STATUS de kubectl get pods n'est pas la phase. C'est un résumé plus riche, calculé par kubectl à partir de la phase, de l'état des conteneurs et des raisons d'attente. CrashLoopBackOff, ImagePullBackOff, ContainerCreating, Init:0/1, Terminating, OOMKilled sont des valeurs de STATUS, jamais des phases : un pod en CrashLoopBackOff est en phase Running. Ça compte quand vous filtrez avec --field-selector status.phase=, qui ne connaît que les cinq phases.

Pourquoi on ne crée jamais un pod à la main en production

Un pod est mortel et non réparable. S'il est supprimé, si son nœud tombe, si on le déplace, il ne revient pas. Rien ne le surveille. En production, les pods sont créés par des contrôleurs, Deployment, StatefulSet, DaemonSet, Job, qui les recréent, les remplacent et les mettent à jour. Un pod nu est un outil de test ou de débogage. Le chapitre suivant explique le reste.

Les commandes du pod

CommandePour quoi
kubectl run web --image=nginx:1.27Créer un pod sans manifeste. --rm -it --restart=Never -- sh pour un pod jetable interactif
kubectl get pods -o wideLe nœud et l'IP de chaque pod
kubectl get pods --field-selector status.phase=PendingLes pods qui n'ont pas démarré
kubectl describe pod webTout : conteneurs, état, volumes, conditions, et les events en bas
kubectl logs webLes logs. -f pour suivre, -c nom pour choisir le conteneur, --since=10m, --tail=100
kubectl logs web --previousLes logs de l'instance précédente, après un crash
kubectl exec -it web -- shUn shell dans le conteneur. -c nom s'il y en a plusieurs
kubectl debug -it web --image=busybox:1.36 --target=nginxAttacher un conteneur de débogage éphémère à un pod qui n'a pas de shell
kubectl port-forward pod/web 8080:80Joindre le port 80 du pod sur localhost:8080
kubectl cp web:/etc/nginx/nginx.conf ./nginx.confCopier un fichier depuis ou vers le conteneur
kubectl delete pod webSupprimer. --grace-period=0 --force pour un pod qui ne veut pas mourir, en dernier recours

En pratiqueun pod nu, et ce qui se passe quand on le supprime

kubectl run web --image=nginx:1.27 --port=80
kubectl get pods -o wide

Le pod est Running sur l'un de vos workers, avec une IP en 10.244.x.x. Cette IP n'est joignable que depuis l'intérieur du cluster. Ouvrez un tunnel :

kubectl port-forward pod/web 8080:80

Dans un autre terminal, curl localhost:8080 renvoie la page d'accueil de nginx, et le premier terminal affiche la connexion. Coupez le port-forward, lisez les logs, entrez dans le conteneur :

kubectl logs web
kubectl exec -it web -- sh
# dans le conteneur
cat /etc/nginx/conf.d/default.conf
exit

Puis supprimez le pod et regardez ce qui se passe :

kubectl delete pod web
kubectl get pods

Rien. No resources found. Le pod est parti et rien ne l'a remplacé. C'est exactement le problème que le chapitre suivant résout.

Le piègeles logs sont dans l'instance précédente

kubectl logs sur un pod en CrashLoopBackOff affiche les logs du conteneur courant, celui qui vient d'être relancé et qui n'a souvent rien eu le temps d'écrire. L'erreur qui a tué le processus est dans l'instance précédente :

kubectl logs web --previous

C'est le même réflexe que la touche p dans k9s, et c'est celui qui fait gagner le plus de temps par rapport à son coût d'apprentissage.

Chapitre 8Les contrôleurs de charge de travail

Un Deployment, c'est le contremaître de vos pods : il en maintient N et sait revenir en arrière. Un StatefulSet, c'est un Deployment où chaque pod a un nom et un disque. Un DaemonSet, c'est un détecteur de fumée par pièce : un pod par machine. Un Job, c'est une tâche qui doit finir ; un CronJob, la même à l'heure dite.

Un contrôleur de charge de travail (workload controller) est une ressource qui possède des pods, les crée d'après un modèle, et les maintient en vie. Vous ne créez plus de pods ; vous décrivez à un contrôleur combien vous en voulez et à quoi ils ressemblent.

ContrôleurPour quoiIdentité des podsOrdreStockageExemple
ReplicaSetMaintenir N copies identiques d'un podAnonymes, interchangeables, nom aléatoireAucunPartagé ou aucunVous ne l'utilisez jamais directement
DeploymentGérer un ReplicaSet et ses mises à jour sans coupureAnonymesAucunPartagé ou aucunUne API, un site, tout ce qui est sans état
StatefulSetDes pods avec une identité stable et un volume chacunNommés x-0, x-1, x-2, DNS propreDémarrage et arrêt ordonnésUn volume persistant par podUne base de données, un broker, tout ce qui a un état
DaemonSetExactement un pod par nœudUn par nœudAucunSouvent hostPathUn agent de logs, de métriques, un CNI
JobExécuter une tâche jusqu'à ce qu'elle réussisseAnonymesParallélisme configurableAucun en généralUne migration, un traitement par lots
CronJobCréer un Job selon un calendrierAnonymesAucunAucun en généralUne sauvegarde nocturne, un rapport

Le Deployment

Le Deployment est celui que vous utiliserez le plus, et il fonctionne en deux étages. Il ne possède pas de pods directement : il possède des ReplicaSets, et chaque ReplicaSet possède des pods. Quand vous changez l'image, le Deployment crée un nouveau ReplicaSet avec la nouvelle image, le monte progressivement, et descend l'ancien. L'ancien ReplicaSet est conservé, vide, pour pouvoir revenir en arrière. Ça s'appelle un rollout, et son historique est celui des ReplicaSets.

La stratégie par défaut, RollingUpdate, remplace les pods par vagues, avec deux paramètres :

  • maxSurge : le nombre de pods en trop que la vague peut créer temporairement ;
  • maxUnavailable : le nombre de pods qui peuvent manquer.

Par défaut, 25 % chacun. Sur trois répliques, ça signifie un pod de plus et zéro de moins à tout instant : votre service ne descend jamais sous sa capacité nominale.

L'autre stratégie, Recreate, tue tout puis relance tout. Elle sert quand deux versions ne peuvent pas coexister, une migration de schéma incompatible par exemple, et elle implique une coupure.

Le manifeste minimal d'un Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3                  # combien de pods
  selector:
    matchLabels:
      app: api                 # comment le Deployment reconnaît ses pods : par ce label
  template:                    # le modèle de pod, copié à chaque création
    metadata:
      labels:
        app: api               # doit correspondre au sélecteur, sinon l'API refuse
    spec:
      containers:
        - name: api
          image: ghcr.io/rudeops/api:1.4.2
          ports:
            - containerPort: 8080

selector et template.metadata.labels doivent correspondre : le premier dit "mes pods sont ceux qui portent app: api", le second pose ce label sur chaque pod créé. Le sélecteur est immuable après création. Tout ce qui est sous template est un spec de pod ordinaire, et tout ce que vous apprendrez sur les pods dans les chapitres suivants se place là.

Ce qui distingue vraiment un StatefulSet

Un StatefulSet donne à chaque pod une identité stable : un nom prévisible (postgres-0, postgres-1), un enregistrement DNS propre (postgres-0.postgres.default.svc.cluster.local, via un Service headless qu'on verra au chapitre réseau), et un PersistentVolumeClaim à lui, qui le suit s'il est recréé sur un autre nœud. Les pods démarrent dans l'ordre, 0 puis 1 puis 2, et s'arrêtent dans l'ordre inverse. Un pod postgres-1 supprimé revient sous le nom postgres-1, avec son disque.

C'est tout ce qui est nécessaire pour faire tourner une base de données répliquée : le primaire sait qu'il est -0, les réplicas savent le joindre par un nom qui ne change pas. Ce n'est pas une raison suffisante pour le faire : opérer une base de données dans Kubernetes demande un operator qui gère le failover, les sauvegardes et les mises à jour. Le StatefulSet fournit l'identité ; il ne fournit pas l'intelligence.

Les commandes des Deployments et des Jobs

CommandePour quoi
kubectl create deployment api --image=X --replicas=3Créer un Deployment sans manifeste
kubectl scale deployment/api --replicas=5Changer le nombre de répliques
kubectl set image deployment/api api=ghcr.io/rudeops/api:1.5.0Changer l'image d'un conteneur, conteneur=image
kubectl rollout status deployment/apiSuivre un déploiement jusqu'à sa fin, ou son blocage
kubectl rollout history deployment/apiLes révisions. --revision=2 pour le détail d'une
kubectl rollout undo deployment/apiRevenir à la révision précédente. --to-revision=N pour une révision précise
kubectl rollout restart deployment/apiRecréer tous les pods, proprement, par vagues
kubectl get rsLes ReplicaSets, dont les anciens conservés pour l'undo
kubectl get jobs / kubectl get cronjobsLes tâches et leur planification
kubectl create job manuel --from=cronjob/backupLancer un CronJob tout de suite, sans attendre l'horaire

En pratiquecasser un rollout et revenir en arrière

kubectl create deployment api --image=nginx:1.27 --replicas=3
kubectl rollout status deployment/api
kubectl get rs

Un ReplicaSet, trois pods. Changez l'image et suivez :

kubectl set image deployment/api nginx=nginx:1.28
kubectl rollout status deployment/api
Waiting for deployment "api" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 2 of 3 updated replicas are available...
deployment "api" successfully rolled out

kubectl get rs montre maintenant deux ReplicaSets : le nouveau à 3, l'ancien à 0. Cassez maintenant volontairement le déploiement avec une image qui n'existe pas :

kubectl set image deployment/api nginx=nginx:99.99-inexistante
kubectl rollout status deployment/api

Le rollout se bloque à 1 out of 3 new replicas have been updated. kubectl get pods montre un pod en ImagePullBackOff et les trois anciens toujours Running : maxUnavailable a fait son travail, votre service n'a jamais cessé de répondre. Revenez en arrière :

kubectl rollout undo deployment/api
kubectl rollout status deployment/api
kubectl rollout history deployment/api

Trois révisions dans l'historique, et le Deployment de retour sur nginx:1.28. Pour rendre cet historique lisible, annotez chaque changement avec kubectl annotate deployment/api kubernetes.io/change-cause="passage en 1.28" ; la colonne CHANGE-CAUSE l'affiche. L'option --record, qui faisait ça toute seule, est dépréciée depuis la 1.22 et masquée de l'aide. Elle fonctionne encore, mais elle disparaîtra : posez l'annotation vous-même.

Le piègedelete pod n'est pas un redémarrage

kubectl rollout restart est la seule façon propre de redémarrer les pods d'un Deployment. Elle pose une annotation avec un horodatage sur le template, ce qui déclenche un rollout ordinaire : par vagues, en respectant maxSurge et maxUnavailable, sans coupure.

kubectl delete pod fonctionne aussi, et c'est ce que tout le monde fait par réflexe. Mais ça contourne la stratégie de déploiement : les pods disparaissent d'un coup, le ReplicaSet les recrée après coup, et sur un Deployment à une seule réplique, vous venez de couper le service. Sur trois répliques supprimées dans une boucle for, pareil.

Chapitre 9La configuration : ConfigMap et Secret

Une ConfigMap, c'est la config posée à côté de l'image, pas dedans. Un Secret, c'est une ConfigMap avec un rideau, pas un coffre : base64 n'est pas un chiffrement.

Une image ne doit contenir aucune configuration propre à un environnement : ni l'URL de la base de données, ni le niveau de log, ni un mot de passe. La même image tourne en staging et en production, et c'est l'environnement qui lui injecte ce qui change. Kubernetes a deux ressources pour ça, presque identiques, dont l'une est mal nommée.

Une ConfigMap est un dictionnaire de paires clé-valeur, ou de fichiers entiers, pour la configuration non sensible. Un Secret est la même chose pour ce qui est sensible : mots de passe, tokens, clés TLS. La différence entre les deux est dans la manière dont le cluster les traite, RBAC séparé, non affichés dans describe, copiés en tmpfs et non sur disque quand ils sont montés, et surtout dans la manière dont vous devez les traiter. On y revient dans un instant.

Deux façons d'injecter, deux comportements

Une ConfigMap ou un Secret s'injecte dans un pod par variables d'environnement ou par montage en volume, et les deux ne se comportent pas pareil quand la valeur change.

Une variable d'environnement est lue une fois, au démarrage du conteneur, et figée. Si vous modifiez la ConfigMap, le pod continue avec l'ancienne valeur jusqu'à ce qu'il soit recréé.

Un fichier monté en volume est mis à jour par le kubelet à sa synchronisation périodique. Le délai total est la période de synchronisation, une minute par défaut, plus le délai de propagation du cache, qui dépend de la stratégie de détection choisie, Watch par défaut. En pratique, comptez jusqu'à un peu plus d'une minute. Le nouveau contenu apparaît alors dans le conteneur sans redémarrage. Encore faut-il que votre application relise le fichier, ce que la plupart ne font pas.

Une exception qui surprend : un fichier monté avec subPath, pour placer un seul fichier dans un dossier existant, n'est jamais mis à jour. C'est documenté, et c'est régulièrement redécouvert dans la douleur.

Le manifeste minimal d'une ConfigMap et d'un Secret

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
data:
  LOG_LEVEL: info                  # une clé, une valeur courte
  app.properties: |                # une clé dont la valeur est un fichier entier
    timeout=30
    retries=3
---
apiVersion: v1
kind: Secret
metadata:
  name: api-secret
type: Opaque                       # le type générique
stringData:                        # en clair dans le manifeste, encodé par l'API à l'écriture
  DB_PASSWORD: "ne-committez-pas-ceci"
---
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  containers:
    - name: api
      image: ghcr.io/rudeops/api:1.4.2
      env:
        - name: LOG_LEVEL          # une variable, depuis une clé de la ConfigMap
          valueFrom:
            configMapKeyRef:
              name: api-config
              key: LOG_LEVEL
      envFrom:                     # toutes les clés du Secret, en variables
        - secretRef:
            name: api-secret
      volumeMounts:
        - name: config
          mountPath: /etc/api      # chaque clé devient un fichier dans ce dossier
  volumes:
    - name: config
      configMap:
        name: api-config

Dans /etc/api, le conteneur trouvera deux fichiers, LOG_LEVEL et app.properties, avec leur contenu. Dans son environnement, LOG_LEVEL=info et DB_PASSWORD=....

Encart, celui qui manque dans tous les tutoriels. Un Secret est encodé en base64, pas chiffré.

Le champ data d'un Secret contient bmUtY29tbWl0dGV6LXBhcy1jZWNp, et n'importe qui peut le décoder en une commande. Quiconque a le droit de lire le Secret lit la valeur. Le champ stringData du manifeste ci-dessus ne fait qu'éviter de vous faire encoder à la main ; à l'arrivée, c'est du base64.

L'intérêt du Secret est ailleurs :

  • des droits RBAC distincts de ceux des ConfigMaps ;
  • une absence d'affichage dans kubectl describe ;
  • pour un Secret monté en volume, une copie écrite par le kubelet dans un tmpfs, donc pas sur du stockage durable, et supprimée dès que le pod disparaît.

Dans etcd, en revanche, un Secret est écrit tel quel, sauf si le chiffrement au repos a été configuré sur l'API server. Ce n'est pas le cas par défaut, ni sur kind, ni sur un cluster monté à la main. La plupart des clouds gérés l'activent ; vérifiez. Et un Secret dans un dépôt Git est un secret publié, base64 ou pas.

Ce point est la porte d'entrée du document sur la sécurité Kubernetes qui suivra ce guide. Ici, retenez une chose : un Secret est un mécanisme de distribution, pas de protection.

Les commandes des ConfigMaps et des Secrets

CommandePour quoi
kubectl create configmap api-config --from-literal=LOG_LEVEL=infoUne clé, une valeur
kubectl create configmap api-config --from-file=app.propertiesUne clé par fichier, nommée d'après le fichier
kubectl create configmap api-config --from-env-file=.envUne clé par ligne CLE=valeur du fichier
kubectl create secret generic api-secret --from-literal=DB_PASSWORD=xUn Secret générique. --from-file fonctionne pareil
kubectl create secret tls api-tls --cert=tls.crt --key=tls.keyUn Secret TLS, pour Ingress et Gateway
kubectl describe configmap api-configLe contenu, en clair
kubectl get secret api-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -dLire une valeur de Secret : describe la masque, -o yaml la montre encodée

En pratiqueles deux comportements dans un seul terminal

kubectl create configmap demo --from-literal=GREETING=bonjour
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: demo
spec:
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "while true; do echo env=$GREETING file=$(cat /cfg/GREETING); sleep 5; done"]
      env:
        - name: GREETING
          valueFrom:
            configMapKeyRef:
              name: demo
              key: GREETING
      volumeMounts:
        - name: cfg
          mountPath: /cfg
  volumes:
    - name: cfg
      configMap:
        name: demo
EOF
kubectl wait --for=condition=Ready pod/demo --timeout=60s
kubectl logs demo -f

Le pod affiche env=bonjour file=bonjour toutes les cinq secondes. Dans un autre terminal, modifiez la ConfigMap :

kubectl create configmap demo --from-literal=GREETING=salut --dry-run=client -o yaml | kubectl apply -f -

Continuez de regarder les logs. Pendant une minute environ, rien ne change. Puis file=salut apparaît, et env=bonjour reste bonjour, pour toujours. Vous venez de voir les deux comportements dans un seul terminal. Nettoyez avec kubectl delete pod demo et kubectl delete configmap demo.

Le piègemodifier une ConfigMap ne redémarre rien

Modifier une ConfigMap ne redémarre pas les pods qui l'utilisent. Les variables d'environnement sont figées au démarrage, les fichiers montés sont mis à jour mais l'application ne les relit pas. Dans les deux cas, après un changement de configuration, il faut :

kubectl rollout restart deployment/api

Ou, mieux, rendre le redémarrage automatique en posant sur le template du pod une annotation avec le checksum de la ConfigMap : quand le checksum change, le template change, et le Deployment déclenche un rollout tout seul. Helm le fait avec une ligne dans le template ; Kustomize le fait avec ses générateurs, qui suffixent le nom de la ConfigMap avec un hash de son contenu. Sans outil, un sha256sum dans votre script de déploiement fait l'affaire.

Chapitre 10Le réseau : Services, DNS, Ingress et Gateway API

Un Service, c'est le numéro du standard, pas celui d'un employé : une adresse stable devant des pods qui changent. Un EndpointSlice, c'est la liste de qui répond vraiment. Ingress ou Gateway, c'est la réception de l'immeuble : elle lit le nom sur le courrier et le porte au bon étage. Une NetworkPolicy, c'est le pare-feu entre pods.

Le modèle réseau de Kubernetes tient en trois règles, et tout ce que fait un plugin réseau consiste à les rendre vraies sur votre infrastructure :

  1. Chaque pod a sa propre adresse IP, unique dans le cluster.
  2. Tous les pods peuvent se joindre entre eux par cette IP, sans NAT, quel que soit le nœud.
  3. Les agents d'un nœud (le kubelet, un DaemonSet) peuvent joindre tous les pods de ce nœud.

C'est un modèle plat. Un pod n'a pas à savoir sur quelle machine tourne l'autre, et il n'y a pas de traduction d'adresse entre eux.

Le composant qui rend ça possible est le CNI (Container Network Interface), un plugin que vous choisissez à l'installation du cluster : Calico, Cilium, Flannel, ou le CNI intégré de votre cloud. kind installe le sien, kindnet, volontairement minimal. Vous n'en changerez pas tous les jours, mais sachez qu'il existe : c'est lui qui distribue les IP, route entre les nœuds, et applique les NetworkPolicies quand il sait le faire.

Le Service

Les IP des pods sont éphémères : un pod recréé en obtient une nouvelle. Personne ne peut donc s'y connecter directement, et c'est le problème que le Service résout. Un Service est un nom stable et une IP virtuelle stable, derrière lesquels se trouve un ensemble de pods choisis par sélecteur de labels. Le trafic envoyé au Service est réparti entre les pods qui correspondent au sélecteur et qui sont prêts.

Le trajet d'une requêteDepuis l'extérieur, une requête passe par un Ingress ou une Gateway, puis par un Service ClusterIP, dont le sélecteur de labels (app=api) désigne un EndpointSlice, la liste réelle des pods qui répondent, puis un pod. Le Service ne route jamais vers un pod par son nom, toujours par ses labels. Entre deux pods du cluster, le pod A résout le nom DNS api.prod.svc.cluster.local via CoreDNS et emprunte le même Service pour joindre le pod B.client externeIngressou Gateway APIServiceClusterIProute par labels, jamais par nomsélecteurapp=apiEndpointSlicela liste réelle des pods qui répondentPodapp=apiPod Aapi.prod.svc.cluster.localrésolu par CoreDNSServicele mêmePod BDEPUIS L'EXTÉRIEURENTRE DEUX PODS DU CLUSTERle trafic interne emprunte le mêmeService, donc le même sélecteur
Un Service sans endpoint est un sélecteur qui ne matche rien · kubectl get endpointslices -l kubernetes.io/service-name=api le dit en une commande

Le Service ne route jamais vers un pod par son nom. Il maintient, via un objet EndpointSlice, la liste des IP des pods qui portent les bons labels à cet instant. Cette liste est mise à jour à chaque création, suppression ou changement de disponibilité d'un pod. Un Service dont le sélecteur ne correspond à aucun pod a un EndpointSlice vide, et ne répond à rien. C'est la panne réseau numéro un, et on y revient dans le piège.

Quatre types de Service, dont deux que vous utiliserez :

  • ClusterIP, le défaut : une IP interne au cluster, joignable uniquement depuis les pods. C'est le type de 90 % des Services, tout ce qui parle à tout le reste à l'intérieur.
  • NodePort : ouvre un port, entre 30000 et 32767, sur chaque nœud du cluster, et l'envoie vers le Service. Ça expose le service à l'extérieur, sur un port bizarre, sur l'IP de n'importe quel nœud, sans équilibrage de charge digne de ce nom. C'est un mécanisme de bas niveau sur lequel les load balancers s'appuient ; ce n'est presque jamais la réponse à "comment j'expose mon application".
  • LoadBalancer : demande au cloud-controller-manager de créer un équilibreur de charge externe avec une IP publique, qui envoie vers le Service. C'est la bonne réponse sur un cloud, et un <pending> éternel sur kind.
  • Headless (clusterIP: None) : pas d'IP virtuelle. Le nom DNS du Service renvoie directement les IP des pods, et pour un StatefulSet, chaque pod obtient son propre nom. C'est ce qui permet à postgres-1 de joindre postgres-0 par son nom.

CoreDNS et les noms

Un serveur DNS, CoreDNS, tourne dans kube-system et donne un nom à chaque Service : nom.namespace.svc.cluster.local. Un pod du namespace production qui veut joindre le Service api de ce même namespace peut écrire api, api.production, ou le nom complet ; les trois fonctionnent, parce que le /etc/resolv.conf de chaque pod contient les suffixes de recherche de son namespace. Depuis un autre namespace, api seul ne fonctionne pas ; il faut au moins api.production.

C'est tout ce qu'il faut pour que vos applications se trouvent : DATABASE_HOST=postgres, et pas une IP.

Ingress et Gateway API : les deux faits à ne pas confondre

Le Service expose un port. Pour exposer des applications HTTP, avec des noms d'hôte, des chemins, du TLS, il faut une couche au-dessus, et Kubernetes en a deux.

Ingress est l'API historique : une ressource networking.k8s.io/v1 qui dit "le trafic pour api.rudeops.com/v1 va au Service api sur le port 80". Elle ne fait rien seule ; il faut un contrôleur Ingress, un proxy inverse déployé dans le cluster qui lit ces ressources et se configure en conséquence.

Gateway API est son successeur, développé par le même groupe, et arrivé à maturité : la version 1.6.0 est sortie le 30 juin 2026, avec, en canal standard, GatewayClass, Gateway, HTTPRoute, GRPCRoute, TLSRoute, TCPRoute et UDPRoute.

Elle sépare les rôles : l'équipe infra définit la Gateway, l'équipe applicative écrit ses HTTPRoute. Et elle couvre nativement ce qu'Ingress ne faisait qu'à coups d'annotations propriétaires : la réécriture de chemins, la répartition pondérée entre versions, les en-têtes, le TCP et l'UDP.

Maintenant, les deux faits, distincts, qu'il ne faut surtout pas confondre.

Ingress n'est pas déprécié. La documentation officielle présente Gateway API comme le successeur d'Ingress, et c'est une formulation positionnelle, pas une dépréciation. Aucun avis de dépréciation, aucune date de retrait. Vos ressources Ingress fonctionnent et continueront de fonctionner. Écrire "Ingress est déprécié" est une erreur factuelle, et vous la lirez souvent.

Ingress-NGINX est retiré. C'est autre chose. Ingress-NGINX était le contrôleur Ingress le plus déployé au monde, celui que la quasi-totalité des tutoriels francophones vous font installer en trois lignes.

Sa maintenance au mieux s'est arrêtée en mars 2026, sur décision de SIG Network et du Security Response Committee. Depuis : aucune release, aucun correctif de bug, aucun correctif de sécurité. Les déploiements existants continuent de tourner et les images restent téléchargeables, ce qui rend le problème invisible.

Si vous l'installez aujourd'hui, vous installez un proxy exposé sur Internet qui ne recevra plus jamais de patch.

Le chemin recommandé est Gateway API avec une implémentation maintenue, ou, si vous tenez à l'API Ingress, un autre contrôleur : Traefik, HAProxy, Envoy Gateway, Cilium et d'autres en proposent. L'outil ingress2gateway, en version 1.0, convertit vos ressources Ingress existantes en ressources Gateway API. Si vous ne devez retenir qu'un fait de ce guide sur le plan de la sécurité, c'est celui-là.

NetworkPolicy

Par défaut, tout pod peut parler à tout pod. Une NetworkPolicy restreint ça : elle sélectionne des pods par labels, et définit ce qui a le droit d'entrer (ingress) et de sortir (egress). Dès qu'une politique sélectionne un pod, tout ce qu'elle n'autorise pas explicitement est refusé pour ce pod. Une politique qui sélectionne tous les pods d'un namespace et n'autorise rien est un "deny all", la base d'une posture saine.

Une NetworkPolicy n'est appliquée que si le CNI sait le faire. Calico et Cilium le font ; Flannel seul ne le fait pas ; les CNI intégrés des clouds varient. L'API accepte la politique dans tous les cas, sans avertir : un CNI qui ne les gère pas les ignore en silence.

Le test qui tranche tient en deux commandes : une politique deny-all sur un namespace, puis un wget depuis un autre namespace, qui doit échouer. S'il passe, votre CNI ignore les NetworkPolicies.

Sur kind, ça fonctionne : kindnet applique les NetworkPolicy standard depuis la version 0.24. L'application est best effort et ne couvre que networking.k8s.io/v1, ce qui suffit largement pour les exercices de ce guide. Si vous voulez tester un CNI complet, networking.disableDefaultCNI: true dans kind.yaml vous laisse installer Calico ou Cilium à la place.

Le manifeste minimal d'un Service

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api                     # les pods qui portent ce label, et eux seuls
  ports:
    - port: 80                   # le port du Service, celui que les clients appellent
      targetPort: 8080           # le port du conteneur, derrière
  # type: ClusterIP est le défaut

Les commandes du réseau

CommandePour quoi
kubectl expose deployment api --port=80 --target-port=8080Créer un Service ClusterIP qui reprend le sélecteur du Deployment
kubectl get svcLes Services, leur type, leur IP, leurs ports
kubectl get endpointslices -l kubernetes.io/service-name=apiLes IP des pods derrière un Service. Vide = le sélecteur ne matche rien
kubectl get ingress / kubectl get httproutesLes règles d'exposition HTTP, selon l'API utilisée
kubectl port-forward svc/api 8080:80Joindre un Service depuis votre machine, pour tester
kubectl run -i --rm test --image=busybox:1.36 --restart=Never -- wget -qO- http://apiTester le DNS et le Service depuis un pod jetable

Vous croiserez kubectl get endpoints. L'API Endpoints est dépréciée depuis 1.33 au profit d'EndpointSlice, qui la remplace depuis 1.21. Apprenez directement la bonne.

En pratiquecasser un sélecteur et le diagnostiquer

kubectl create deployment api --image=nginx:1.27 --replicas=2
kubectl expose deployment api --port=80
kubectl get svc api
kubectl get endpointslices -l kubernetes.io/service-name=api

L'EndpointSlice liste deux IP, celles de vos deux pods. Joignez le Service par son nom depuis un pod temporaire :

kubectl run -i --rm test --image=busybox:1.36 --restart=Never -- wget -qO- http://api

La page nginx s'affiche. Cassez maintenant le sélecteur, sans toucher aux pods :

kubectl patch svc api -p '{"spec":{"selector":{"app":"apii"}}}'
kubectl run -i --rm test --image=busybox:1.36 --restart=Never -- wget -qO- -T 3 http://api

Le DNS résout toujours, l'IP du Service existe toujours, et la connexion échoue. Diagnostiquez :

kubectl get endpointslices -l kubernetes.io/service-name=api

Aucune adresse. Le sélecteur app=apii ne correspond à aucun pod. Réparez avec kubectl patch svc api -p '{"spec":{"selector":{"app":"api"}}}', et l'EndpointSlice se remplit à nouveau.

Le piègeun Service sans endpoint

C'est l'astuce de diagnostic numéro un du réseau Kubernetes, et elle tient en une commande. Un Service qui ne répond pas alors que les pods sont Running a, dans l'immense majorité des cas, un sélecteur qui ne correspond à aucun pod : une faute de frappe, un label renommé, un Deployment recréé avec d'autres labels.

kubectl get endpointslices -l kubernetes.io/service-name=api

Si la colonne ENDPOINTS affiche <unset>, ce n'est ni le DNS, ni le CNI, ni le firewall. C'est le sélecteur. Un cas voisin : les pods sont bien sélectionnés mais pas prêts, parce qu'une readiness probe échoue ; ils apparaissent alors dans l'EndpointSlice mais avec ready: false, et le Service les ignore. On voit les probes au chapitre débogage.

Chapitre 11Le stockage

Un PVC, c'est le bon de commande ; un PV, c'est le disque livré ; une StorageClass, c'est le catalogue. Vous remplissez le bon, le catalogue dit qui livre, le disque suit votre pod.

Le système de fichiers d'un conteneur est éphémère : tout ce qu'il écrit disparaît avec lui. Kubernetes propose une gradation, de l'éphémère assumé au persistant qui survit à tout.

Un emptyDir est un dossier vide créé à la naissance du pod, partagé entre ses conteneurs, et supprimé avec le pod. C'est le stockage de travail : un cache, un fichier échangé entre un sidecar et l'application, un espace temporaire. Il survit à un redémarrage de conteneur, pas à la suppression du pod.

Un hostPath monte un dossier du nœud dans le pod. Il est utile pour un DaemonSet qui lit les logs de la machine, et dangereux pour tout le reste : le pod dépend du nœud sur lequel il tourne, ce qui casse dès qu'il est déplacé, et il donne accès au système de fichiers de l'hôte. Les profils de sécurité restrictifs l'interdisent.

Pour le vrai persistant, Kubernetes sépare la demande de l'offre :

  • Un PersistentVolume (PV) est un morceau de stockage réel, un disque cloud, un export NFS, un volume Ceph, décrit comme une ressource du cluster, hors namespace.
  • Un PersistentVolumeClaim (PVC) est la demande d'un utilisateur : "je veux 10 Gi, accessibles en lecture-écriture par un seul nœud". C'est le PVC qu'un pod monte, jamais le PV directement.
  • Une StorageClass décrit un type de stockage que le cluster sait fournir, avec le provisionneur qui le crée et ses paramètres. Quand un PVC référence une StorageClass, le provisionneur crée un PV à la demande. C'est le provisionnement dynamique, et c'est ainsi que fonctionne tout stockage moderne dans Kubernetes.

Ce qui se passe réellement quand vous créez un PVC avec une StorageClass : le contrôleur de la StorageClass voit le PVC, appelle le provisionneur, qui crée un disque chez le fournisseur, crée un PV qui le représente, et lie le PVC au PV. Le PVC passe de Pending à Bound. Quand un pod le monte, le disque est attaché au nœud du pod et monté dans le conteneur. Si le pod est recréé sur un autre nœud, le disque est détaché puis rattaché. Vos données suivent le PVC, pas le pod.

Mode d'accèsAbréviationCe que ça veut dire
ReadWriteOnceRWOLecture-écriture par un seul nœud à la fois. Un disque bloc classique
ReadOnlyManyROXLecture seule, par plusieurs nœuds
ReadWriteManyRWXLecture-écriture par plusieurs nœuds. Il faut un stockage réseau qui le supporte, NFS, CephFS, un service de fichiers cloud
ReadWriteOncePodRWOPLecture-écriture par un seul pod, plus strict que RWO qui autorise plusieurs pods sur le même nœud
Politique de récupérationQuand le PVC est supprimé
DeleteLe PV et le stockage sous-jacent sont supprimés. Le défaut du provisionnement dynamique
RetainLe PV reste, marqué Released, avec ses données. À nettoyer à la main. Ce que vous voulez pour une base de données

Deux paragraphes sur CSI, pas plus. Le Container Storage Interface est le standard par lequel un fournisseur de stockage se branche sur Kubernetes : un pilote CSI, déployé dans le cluster, sait créer, attacher, monter et supprimer des volumes de ce fournisseur. Tous les provisionneurs modernes sont des pilotes CSI, et les anciens pilotes intégrés au code de Kubernetes ont été progressivement retirés.

Ce que ça change pour vous : rien au quotidien. Vous écrivez des PVC avec une StorageClass ; le pilote CSI fait le reste. Ce qu'il faut savoir : les snapshots de volumes, le clonage et le redimensionnement sont des capacités CSI, offertes ou non selon le pilote. Un kubectl get sc puis kubectl describe sc vous dit lequel est en place.

Le manifeste minimal d'un PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes:
    - ReadWriteOnce              # un nœud à la fois
  resources:
    requests:
      storage: 1Gi               # la taille demandée
  # storageClassName: standard   # absent = la StorageClass par défaut du cluster
---
apiVersion: v1
kind: Pod
metadata:
  name: writer
spec:
  containers:
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data       # le volume apparaît ici dans le conteneur
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data          # le PVC, pas le PV

Les commandes du stockage

CommandePour quoi
kubectl get scLes StorageClasses, et laquelle est (default)
kubectl get pvcLes demandes, leur état (Pending, Bound), leur taille, leur classe
kubectl get pvLes volumes réels, leur politique de récupération, le PVC lié
kubectl describe pvc dataLes events : la raison d'un Pending est là
kubectl get pvc -wSuivre un provisionnement en direct

En pratiqueun fichier qui survit à son pod

kubectl get sc

Sur kind, une classe standard (default) fournie par un provisionneur local. Enregistrez le manifeste ci-dessus dans pvc.yaml, appliquez-le, attendez que le pod soit prêt, puis écrivez :

kubectl apply -f pvc.yaml
kubectl get pvc data
kubectl wait --for=condition=Ready pod/writer --timeout=60s
kubectl exec writer -- sh -c 'echo "écrit le $(date)" > /data/preuve.txt'
kubectl delete pod writer

Le pod est parti. Le PVC est toujours Bound. Recréez un pod, n'importe lequel, qui monte le même PVC, en réappliquant le manifeste, et lisez :

kubectl apply -f pvc.yaml
kubectl wait --for=condition=Ready pod/writer --timeout=60s
kubectl exec writer -- cat /data/preuve.txt

Le fichier est là. Sur un cluster multi-nœuds, si le nouveau pod est placé ailleurs, le volume a suivi ; sur kind avec le provisionneur local, le pod est contraint de revenir sur le nœud du volume, ce qui illustre exactement pourquoi hostPath et le stockage local ne sont pas du stockage persistant au sens plein. kubectl delete pvc data supprime le PVC et, avec la politique Delete, le volume.

Le piègeun PVC qui reste en Pending

Un PVC qui reste en Pending. Trois causes couvrent 95 % des cas, et kubectl describe pvc donne la réponse dans ses events :

  1. Pas de StorageClass par défaut. Le PVC n'en nomme aucune, le cluster n'en a pas de marquée par défaut, rien ne se passe. L'event dit no persistent volumes available for this claim and no storage class is set. kubectl get sc confirme l'absence de (default) ; nommez la classe dans le PVC, ou marquez-en une par défaut.
  2. Aucun PV correspondant, en provisionnement statique. Vous avez créé des PV à la main et aucun n'a la taille, le mode d'accès ou la classe demandés. Le même event, avec des PV qui existent mais ne matchent pas. Comparez kubectl get pv au PVC.
  3. Mode d'accès incompatible avec le provisionneur. Vous demandez ReadWriteMany à une classe qui fournit des disques bloc. L'event vient du provisionneur et dit qu'il ne supporte pas ce mode. Il faut un autre stockage, pas un autre paramètre.

Et un faux positif à connaître : une StorageClass en volumeBindingMode: WaitForFirstConsumer laisse le PVC en Pending tant qu'aucun pod ne le monte, pour pouvoir créer le volume dans la bonne zone. L'event dit waiting for first consumer to be created before binding. Ce n'est pas une erreur ; créez le pod.

Chapitre 12L'ordonnancement et les ressources

requests, c'est la réservation ; limits, c'est le plafond. La réservation choisit la machine, le plafond la protège. Une taint, c'est un panneau "réservé" ; une toleration, c'est le badge qui l'ignore.

Quand un pod est créé, il n'a pas de nœud. Le scheduler lui en choisit un, en deux temps.

  • Le filtrage élimine les nœuds qui ne conviennent pas : pas assez de CPU ou de mémoire disponibles au regard de ce que le pod demande, un port hôte déjà pris, une taint non tolérée, un nodeSelector non satisfait, un volume attaché ailleurs.
  • Le scoring classe les nœuds restants : répartition de la charge, présence de l'image déjà téléchargée, affinités souhaitées.

Le meilleur gagne, le scheduler écrit son nom dans le pod, et le kubelet de ce nœud prend le relais.

Si le filtrage ne laisse aucun nœud, le pod reste Pending, et le scheduler réessaie régulièrement. C'est de loin la première cause de Pending.

requests et limits

Chaque conteneur peut déclarer deux choses par ressource, CPU et mémoire, et la différence entre les deux est celle que presque personne n'explique correctement.

requests est ce que le conteneur demande, et c'est ce que le scheduler utilise. Un nœud avec 4 CPU et 8 Gi ne recevra pas plus de pods que la somme de leurs requests ne le permet. Ce n'est pas une mesure de consommation, c'est une réservation. Un conteneur qui demande 500m de CPU et n'en utilise que 50m bloque quand même 500m aux yeux du scheduler.

limits est ce que le conteneur ne peut pas dépasser, et c'est le noyau du nœud qui l'applique, à l'exécution. Le scheduler n'en tient pas compte.

resources:
  requests:
    cpu: 250m          # un quart de cœur, réservé
    memory: 128Mi
  limits:
    cpu: "1"           # au plus un cœur
    memory: 256Mi      # au-delà, le conteneur est tué

Encart : ce n'est pas symétrique.

Dépasser la limite CPU provoque du throttling : le conteneur est ralenti, ses threads attendent, ses latences montent, et il continue de tourner.

Dépasser la limite mémoire provoque un OOMKill : le noyau tue le processus, le conteneur redémarre, le compteur RESTARTS s'incrémente, et kubectl describe affiche Reason: OOMKilled avec Exit Code: 137. Le 137, c'est 128 plus 9, le signal SIGKILL.

C'est exactement ce que raconte l'aperçu terminal en tête de ce guide, et c'est le code que personne ne sait lire la première fois. Le guide systemd a la même table de codes pour les services.

Les classes QoS

Selon ce que vous déclarez, Kubernetes range chaque pod dans une classe de qualité de service, qui décide qui meurt en premier quand un nœud manque de mémoire :

  • Guaranteed : requests et limits définis et égaux pour chaque conteneur, CPU et mémoire. Les derniers évincés.
  • Burstable : au moins une request définie, mais pas Guaranteed. Évincés après les BestEffort, en commençant par ceux qui dépassent le plus leurs requests.
  • BestEffort : rien de déclaré. Les premiers évincés, et le scheduler les place n'importe où puisqu'ils ne demandent rien.

Un pod sans resources est un pod BestEffort, et ce sera le premier à disparaître le jour où un voisin a une fuite mémoire. Déclarez au moins des requests. Toujours.

Contraindre le placement

  • nodeSelector : le pod ne va que sur un nœud qui porte ces labels. disktype: ssd, topology.kubernetes.io/zone: eu-west-1a. Simple, et suffisant dans la plupart des cas.
  • Affinité et anti-affinité : la version expressive, avec des opérateurs (In, NotIn, Exists) et deux forces, required qui filtre et preferred qui score. L'anti-affinité de pods est celle qui compte : "ne place pas deux répliques de api sur le même nœud", pour survivre à la perte d'une machine.
  • Taints et tolerations : l'inverse. Une taint posée sur un nœud repousse tous les pods, sauf ceux qui portent la toleration correspondante. C'est ainsi qu'on réserve des nœuds GPU, qu'on isole le control plane, et que Kubernetes lui-même marque un nœud en défaut (node.kubernetes.io/not-ready) pour en évacuer les pods.
  • topologySpreadConstraints : répartir les répliques uniformément entre zones ou entre nœuds, avec un écart maximal toléré. C'est la façon moderne d'obtenir ce que l'anti-affinité faisait de manière binaire.

Redimensionner un pod à chaud

Depuis Kubernetes 1.35, changer les requests et limits d'un conteneur ne recrée plus le pod. La fonctionnalité est stable, elle passe par la sous-ressource resize, et la plupart des contenus en ligne l'ignorent encore :

kubectl patch pod api-7d4f9c8b6-2xk4p --subresource resize --patch \
  '{"spec":{"containers":[{"name":"api","resources":{"limits":{"memory":"512Mi"}}}]}}'

Le conteneur voit sa nouvelle limite sans redémarrer, à condition que son resizePolicy le permette (c'est le défaut pour le CPU comme pour la mémoire) et que le nœud ait la place. Sur un Deployment, modifiez le template comme d'habitude ; c'est sur un pod qui dérive qu'on gagne à ne pas le tuer.

Les commandes des ressources et des nœuds

CommandePour quoi
kubectl top nodes / kubectl top podsLa consommation réelle, CPU et mémoire. Demande metrics-server, absent par défaut sur kind (minikube addons enable metrics-server sur minikube)
kubectl describe node rudeops-workerLa section Allocated resources : ce qui est réservé, contre la capacité
kubectl get pods --field-selector status.phase=PendingLes pods sans nœud
kubectl taint nodes rudeops-worker gpu=true:NoSchedulePoser une taint (gpu- à la fin pour la retirer)
kubectl cordon rudeops-workerInterdire tout nouveau placement sur ce nœud
kubectl drain rudeops-worker --ignore-daemonsetsÉvacuer proprement les pods d'un nœud, avant maintenance
kubectl uncordon rudeops-workerRouvrir le nœud

Pour que kubectl top fonctionne sur kind, il faut poser metrics-server, et lui dire d'accepter les certificats auto-signés des kubelets, sans quoi il refuse de démarrer :

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch deployment metrics-server -n kube-system --type=json \
  -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl rollout status deployment/metrics-server -n kube-system

Une minute plus tard, kubectl top nodes répond.

En pratiqueun pod que le scheduler refuse

Demandez plus de mémoire qu'un nœud kind n'en a :

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: glouton
spec:
  containers:
    - name: glouton
      image: busybox:1.36
      command: ["sleep", "3600"]
      resources:
        requests:
          memory: 512Gi
EOF
kubectl get pod glouton

Pending, et il le restera. Remontez à la raison :

kubectl describe pod glouton

Tout en bas, dans les events :

Warning  FailedScheduling  12s  default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient memory. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod.

Trois nœuds, trois refus, chacun avec sa cause : les deux workers manquent de mémoire, et le control plane porte une taint qui repousse les pods ordinaires, celle-là même dont on parlait deux sections plus haut. Corrigez la request à 64Mi dans le manifeste, réappliquez, et le pod démarre en quelques secondes : le scheduler a réessayé tout seul.

Le piègePending n'est pas un problème d'image

Un pod en Pending n'est presque jamais un problème d'image. Un problème d'image donne ImagePullBackOff, et le pod a déjà un nœud.

Pending, c'est le scheduler qui ne trouve pas de nœud, et la raison exacte est dans les events du describe, tout en bas : Insufficient cpu, Insufficient memory, node(s) had untolerated taint, node(s) didn't match Pod's node affinity, volume node affinity conflict.

C'est le même ressort que systemctl status dans le guide systemd : la réponse est écrite, il faut juste lire jusqu'en bas.

Chapitre 13La sécurité de base

Un ServiceAccount, c'est la carte d'identité d'un pod. RBAC, c'est la liste de qui peut faire quoi, et où : tout ce qui n'est pas écrit est interdit. securityContext, c'est ce que vous retirez au conteneur : un conteneur n'est pas isolé par magie.

Ce chapitre est volontairement le socle, et il annonce sa borne : qui a le droit de faire quoi sur l'API, avec quels privilèges un conteneur tourne, et comment le cluster refuse les pods dangereux. Le threat model, les quatre C, la chaîne d'approvisionnement des images, le chiffrement d'etcd et le durcissement des nœuds appartiennent au document sur la sécurité Kubernetes qui suivra.

ServiceAccount

Tout ce qui parle à l'API server est authentifié. Les humains le sont par leur kubeconfig, avec un certificat, un token ou un fournisseur d'identité. Les pods le sont par un ServiceAccount : un compte propre au namespace, dont un token est monté dans chaque conteneur, à /var/run/secrets/kubernetes.io/serviceaccount/token. Chaque namespace a un ServiceAccount default, attribué à tout pod qui n'en précise pas, et sans aucun droit par défaut.

Si votre application n'a pas besoin de parler à l'API Kubernetes, et la plupart n'en ont pas besoin, désactivez le montage du token avec automountServiceAccountToken: false sur le pod. Un token qui n'existe pas ne peut pas être volé.

RBAC

Le contrôle d'accès repose sur quatre types, et une règle des quatre combinaisons.

  • Un Role liste des permissions, des verbes (get, list, watch, create, update, patch, delete) sur des ressources (pods, deployments, secrets), dans un namespace.
  • Un ClusterRole fait la même chose, sans namespace : pour les ressources hors namespace (nœuds, PV), ou pour des permissions réutilisables partout.
  • Un RoleBinding attache un Role ou un ClusterRole à des sujets (utilisateurs, groupes, ServiceAccounts), dans un namespace.
  • Un ClusterRoleBinding attache un ClusterRole à des sujets, sur tout le cluster.

Les quatre combinaisons :

  • Role + RoleBinding : des droits dans un namespace.
  • ClusterRole + RoleBinding : des droits définis une fois et accordés dans un namespace précis. C'est ainsi qu'on réutilise les ClusterRoles fournis (view, edit, admin) sans les redéfinir.
  • ClusterRole + ClusterRoleBinding : des droits partout. C'est cluster-admin, à distribuer avec parcimonie.
  • Role + ClusterRoleBinding : impossible, l'API refuse.

RBAC ne connaît que des autorisations : il n'y a pas de règle de refus, et ce qui n'est pas explicitement accordé est refusé.

securityContext

Le securityContext d'un pod ou d'un conteneur décide avec quels privilèges le processus tourne. Le jeu qu'il faut connaître, et que le profil restricted plus bas exige :

securityContext:
  runAsNonRoot: true                  # refuse de démarrer si l'image tourne en root
  runAsUser: 10001
  allowPrivilegeEscalation: false     # pas de setuid, pas de gain de privilèges
  readOnlyRootFilesystem: true        # le système de fichiers de l'image est en lecture seule
  capabilities:
    drop: ["ALL"]                     # aucune capability Linux, sauf celles rajoutées
  seccompProfile:
    type: RuntimeDefault              # le filtre d'appels système du runtime

Si vous avez lu la section sandboxing du guide systemd, tout ceci vous est familier : runAsNonRoot est User=, readOnlyRootFilesystem est ProtectSystem=strict, capabilities.drop est CapabilityBoundingSet=, allowPrivilegeEscalation: false est NoNewPrivileges=yes. Ce sont les mêmes mécanismes du noyau, exposés par deux outils différents. Un conteneur n'est pas isolé par magie ; il l'est par ce que vous lui retirez.

Pod Security Admission

Un cluster ne peut pas compter sur chaque manifeste pour être bien écrit. Pod Security Admission, stable depuis 1.25, est le contrôleur d'admission intégré qui refuse, ou avertit, quand un pod ne respecte pas un profil. Il a remplacé PodSecurityPolicy, supprimé dans la même version 1.25 après des années de complexité ; si un tutoriel vous montre un PodSecurityPolicy, il date.

Trois profils, définis par les Pod Security Standards :

  • privileged : tout est permis. Pour kube-system et les composants d'infrastructure.
  • baseline : interdit ce qui est connu pour être dangereux, mode privilégié, hostPath, réseau de l'hôte, capabilities exotiques, sans casser les applications ordinaires.
  • restricted : le durcissement, qui exige le securityContext ci-dessus. Le bon défaut pour tout ce que vous déployez vous-même.

Le profil se pose par namespace, avec des labels, et en trois modes : enforce refuse, warn prévient dans la sortie de kubectl, audit journalise.

kubectl label ns production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted

Le manifeste minimal d'un Role et de son binding

apiVersion: v1
kind: ServiceAccount
metadata:
  name: lecteur
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: lecture-pods
rules:
  - apiGroups: [""]                  # "" est le groupe historique, celui de v1
    resources: ["pods", "pods/log"]  # les pods, et leurs logs, qui sont une sous-ressource
    verbs: ["get", "list", "watch"]  # lire, lister, suivre. Rien d'autre
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: lecteur-lit-les-pods
subjects:
  - kind: ServiceAccount
    name: lecteur
    namespace: default               # le namespace du sujet, obligatoire pour un ServiceAccount
roleRef:
  kind: Role
  name: lecture-pods
  apiGroup: rbac.authorization.k8s.io

Les commandes de RBAC

CommandePour quoi
kubectl auth can-i create deploymentsAi-je le droit ? Répond yes ou no
kubectl auth can-i --listTout ce que j'ai le droit de faire dans ce namespace
kubectl auth can-i delete pods --as system:serviceaccount:default:lecteurTester les droits de quelqu'un d'autre, sans être lui
kubectl auth can-i get nodes --as jeanne --as-group devIdem pour un utilisateur et un groupe
kubectl create role lecture --verb=get,list --resource=podsCréer un Role sans manifeste
kubectl create rolebinding x --role=lecture --serviceaccount=default:lecteurEt le binding
kubectl get sa / kubectl get roles,rolebindingsCe qui existe dans le namespace

En pratiquetester des droits sans rien déployer

Appliquez le manifeste ci-dessus, puis interrogez les droits du ServiceAccount, sans rien déployer et sans vous connecter avec lui :

kubectl auth can-i list pods --as system:serviceaccount:default:lecteur
kubectl auth can-i get pods/log --as system:serviceaccount:default:lecteur
kubectl auth can-i delete pods --as system:serviceaccount:default:lecteur
kubectl auth can-i list secrets --as system:serviceaccount:default:lecteur
kubectl auth can-i list pods --as system:serviceaccount:default:lecteur -n kube-system

yes, yes, no, no, no. Lecture seule, sur les pods, dans un seul namespace, exactement ce que le Role dit. La dernière ligne montre la portée du Role : rien en dehors de default. Pour donner la même lecture dans tous les namespaces, remplacez le Role par un ClusterRole et le RoleBinding par un ClusterRoleBinding, et relancez la dernière commande.

Le piègeauth can-i --as, avant la prod

kubectl auth can-i --as permet de tester une politique RBAC avant de la livrer. Le sujet n'a pas besoin d'exister, aucun pod n'a besoin de tourner, et la réponse vient de l'autorisateur réel du cluster, pas d'une simulation.

Personne ne le fait. Tout le monde écrit le Role, déploie l'application, et découvre en production un pods is forbidden: User "system:serviceaccount:prod:api" cannot list resource "pods" dans les logs.

Ajoutez la commande à votre revue de manifestes ; c'est trente secondes qui en économisent trente minutes.

Chapitre 14Observer et débugger

describe avant logs, toujours. La cause est écrite en bas des events dans neuf cas sur dix. Liveness : "es-tu vivant ?" Readiness : "peux-tu servir ?" Startup : "as-tu fini de démarrer ?"

Le chapitre que vous mettrez en favori. Tout ce qui précède vous a donné le vocabulaire ; celui-ci vous donne l'ordre dans lequel regarder.

La méthode, dans l'ordre

  1. kubectl get : quel objet est dans quel état. get pods pour le STATUS et les RESTARTS, get deploy pour le READY, get events pour ce qui vient de se passer. Ne cherchez pas encore la cause ; cherchez l'objet qui ne va pas.
  2. kubectl describe sur cet objet, et lisez jusqu'en bas. Les events y sont, et dans neuf cas sur dix la cause aussi : FailedScheduling, Failed to pull image, Back-off restarting failed container, Liveness probe failed, MountVolume.SetUp failed.
  3. kubectl logs, avec --previous si le conteneur a redémarré. C'est là que votre application a écrit pourquoi elle est morte.
  4. Les events du namespace, s'il n'y a pas d'objet évident : kubectl events montre ce qui s'est passé partout dans l'ordre chronologique, y compris sur des objets déjà disparus.
  5. kubectl top, pour ce qui touche aux ressources : un nœud saturé, un pod qui frôle sa limite.

Dans cet ordre, parce que chaque étape est plus coûteuse et plus précise que la précédente, et que sauter à logs sans describe fait perdre un temps considérable sur tout ce qui n'est pas un crash applicatif.

Les probes

Kubernetes ne sait pas si votre application va bien. Il sait si le processus tourne, ce qui n'est pas la même chose : un serveur bloqué sur un deadlock tourne. Les probes sont les vérifications que vous lui donnez, et il y en a trois, avec trois effets différents.

  • livenessProbe : "le processus est-il vivant ?" Si elle échoue, le conteneur est tué et redémarré. C'est un remède violent, à réserver aux états dont seul un redémarrage sort.
  • readinessProbe : "peut-il recevoir du trafic ?" Si elle échoue, le pod est marqué non prêt dans l'EndpointSlice du Service, qui cesse de lui envoyer du trafic jusqu'à ce qu'elle réussisse à nouveau. Le conteneur n'est pas touché. C'est celle qui protège vos utilisateurs pendant un démarrage lent, une saturation ou une dépendance en panne.
  • startupProbe : "a-t-il fini de démarrer ?" Tant qu'elle n'a pas réussi, les deux autres sont suspendues. Elle sert aux applications qui mettent une minute à démarrer et qu'une liveness impatiente tuerait avant la fin.
containers:
  - name: api
    image: ghcr.io/rudeops/api:1.4.2
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      periodSeconds: 5
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      periodSeconds: 10
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /healthz
        port: 8080
      failureThreshold: 30       # 30 x 10 s : jusqu'à cinq minutes pour démarrer
      periodSeconds: 10

Les trois effets de bord classiques :

  • Une liveness trop agressive, sans startupProbe, qui tue un service lent à démarrer avant qu'il ait fini, indéfiniment. C'est le CrashLoopBackOff d'une application qui n'a rien fait de mal.
  • Une readiness manquante, qui envoie du trafic à un pod dont le processus tourne mais qui n'a pas encore chargé sa configuration ni ouvert ses connexions. Ce sont les erreurs 502 à chaque déploiement, pendant dix secondes, que personne n'explique.
  • Une readiness qui appelle une dépendance externe. Si la base de données tombe, tous vos pods deviennent non prêts en même temps, et une panne partielle devient totale.

Le tableau symptôme, cause, correction

SymptômeCause probableCorrection
CrashLoopBackOffLe processus meurt au démarrage, encore et encorelogs --previous, puis corriger l'application ou sa configuration
ImagePullBackOff / ErrImagePullNom ou tag d'image faux, registre privé sans identifiantsdescribe, vérifier le nom exact et l'imagePullSecret
PendingAucun nœud ne convientdescribe, lire les events du scheduler
OOMKilled, code 137Limite mémoire dépasséeRelever la limite, ou corriger la fuite
EvictedPression sur le nœud, le pod a été sacrifiéPoser des requests, viser une classe QoS meilleure que BestEffort
CreateContainerConfigErrorConfigMap ou Secret référencé absentVérifier les noms et les clés référencés
Terminating qui n'en finit pasUn finalizer bloque, ou l'arrêt gracieux traînedescribe, regarder metadata.finalizers ; en dernier recours --force
Init:0/1, Init:ErrorUn init container échouelogs -c <nom-de-l-init>
Running mais Service muetLe sélecteur ne matche rien, ou readiness en échecget endpointslices, puis describe les probes
ContainerCreating qui dureUn volume ne se monte pas, un Secret manque, une image est lourdedescribe, event MountVolume ou Pulling

Les commandes du débogage

CommandePour quoi
kubectl events --for pod/api-x --watchLes events d'un objet, en direct, lisibles
kubectl events --types=WarningSeulement les avertissements du namespace
kubectl get events --sort-by=.lastTimestampL'ancienne forme, triée, quand vous la trouvez dans un runbook
kubectl describe pod api-xL'état complet et les events de l'objet
kubectl logs deploy/api --all-containers --prefix -fLes logs de tous les pods et conteneurs d'un Deployment, préfixés
kubectl logs -l app=api --tail=50Les logs par sélecteur
kubectl debug -it api-x --image=busybox:1.36 --target=apiUn conteneur de débogage dans un pod distroless, avec les outils qui manquent
kubectl debug node/rudeops-worker -it --image=ubuntuUn shell sur le nœud, monté sous /host
kubectl top pods --containersLa consommation par conteneur

Ce que k9s change

Tout ce chapitre se fait en trois touches dans k9s, et c'est son meilleur cas d'usage :

  • ctrl-z ne montre que les ressources en défaut, ce qui remplace la première étape de la méthode sur un namespace à deux cents pods ;
  • :xray deploy affiche l'arbre Deployment, ReplicaSet, pods, conteneurs avec l'état de santé propagé, ce qui répond à "lequel est en train de mal se passer" d'un coup d'œil ;
  • p dans la vue logs bascule sur l'instance précédente, le --previous du réflexe CrashLoopBackOff.

Le guide k9s détaille tout ça ; il a été écrit pour être lu après celui-ci.

En pratiqueun CrashLoopBackOff en trois commandes

Fabriquez un CrashLoopBackOff, et remontez à la cause avec la méthode :

kubectl create deployment crash --image=busybox:1.36 -- sh -c 'echo "config manquante: DATABASE_URL"; exit 1'
kubectl get pods -l app=crash --watch

Error, puis CrashLoopBackOff, avec RESTARTS qui monte et un délai entre chaque essai qui double jusqu'à cinq minutes. Étape deux :

kubectl describe pod -l app=crash

Dans la section du conteneur, Last State: Terminated, Reason: Error, Exit Code: 1 ; tout en bas, l'event Back-off restarting failed container. Ce n'est pas un OOM, pas un problème d'image : le processus sort volontairement avec le code 1. Étape trois :

kubectl logs -l app=crash --previous

config manquante: DATABASE_URL. Vous avez la cause, écrite par l'application elle-même. Trois commandes, dans l'ordre, sans hypothèse. kubectl delete deployment crash pour nettoyer.

Le piègekubectl events existe

kubectl events existe comme commande à part entière, stable depuis 1.28, et presque personne ne l'utilise. Elle trie par défaut dans l'ordre chronologique, ce que kubectl get events ne fait pas sans --sort-by, elle filtre par objet avec --for, par type avec --types, et elle sait suivre en direct avec --watch. Sur un déploiement qui se passe mal, kubectl events --for deploy/api --watch dans un terminal pendant que vous appliquez dans l'autre est le meilleur poste d'observation que kubectl offre.

Chapitre 15Étendre Kubernetes

Une CRD, c'est un mot ajouté au vocabulaire de l'API. Un operator, c'est l'expert embauché pour parler ce mot : il applique la boucle de réconciliation à vos propres objets.

Un chapitre court, orienté "vous allez croiser ces mots, et il faut savoir ce qu'ils désignent".

CRD : ajouter un type à l'API

Une CustomResourceDefinition ajoute un type de ressource à l'API server. Une fois la CRD installée, kubectl get certificates ou kubectl apply -f mon-cluster-postgres.yaml fonctionnent comme pour n'importe quel type natif : l'objet est validé, stocké dans etcd, soumis à RBAC, listé par api-resources. L'API server ne sait rien faire de plus avec ; il le stocke.

Controller et operator

Ce qui donne vie à une ressource custom est un controller : un programme, déployé dans le cluster comme un Deployment ordinaire, qui surveille ces objets et applique la boucle de réconciliation du chapitre 5. Il lit spec, regarde la réalité, agit, écrit status. Exactement le même modèle que le contrôleur de Deployment, appliqué à vos propres objets.

Un operator est un controller qui encode le savoir-faire opérationnel d'un logiciel précis. L'operator PostgreSQL de votre choix sait créer un cluster à partir d'une ressource PostgresCluster, gérer le failover, planifier les sauvegardes, et faire une mise à jour majeure dans le bon ordre.

C'est la réponse à "comment faire tourner une base de données dans Kubernetes" : pas un StatefulSet nu, un operator. cert-manager, qui obtient et renouvelle vos certificats TLS, est probablement le premier que vous installerez, et Prometheus Operator le second.

Helm

Helm est le gestionnaire de paquets de l'écosystème. Un chart est un ensemble de manifestes paramétrés par des valeurs, et helm install les rend, les applique et suit ce qui a été installé, avec un historique et un rollback. L'immense majorité des logiciels tiers se distribuent en chart.

Ce n'est pas un tutoriel Helm, mais deux commandes vous serviront dès demain : helm template pour voir les manifestes qu'un chart va appliquer avant de l'installer, et helm show values pour voir ce qu'il accepte comme paramètres.

Les commandes des CRD

CommandePour quoi
kubectl get crdTous les types ajoutés au cluster
kubectl api-resources --api-group=cert-manager.ioLes types d'un groupe précis, avec leurs noms courts
kubectl explain certificate.specLa documentation d'une ressource custom, depuis le schéma de sa CRD
kubectl get certificates -ALes instances, comme pour n'importe quel type

Le piègeexplain fonctionne sur les CRD

kubectl explain fonctionne aussi sur les CRD installées, à condition que leur auteur ait renseigné le schéma, ce que font tous les projets sérieux. C'est souvent la seule documentation à jour d'un operator tiers : celle qui correspond à la version réellement installée dans votre cluster, et pas à la dernière version du site du projet.

Chapitre 16L'outillage

Le chapitre qui ferme la boucle avec la collection.

k9s d'abord, et en développé. C'est une interface terminal qui lit votre kubeconfig et vous fait naviguer entre les ressources avec des touches, au lieu d'enchaîner les commandes. Vous l'ouvrez, vous tapez :pods, puis l pour les logs, s pour un shell, d pour describe, shift-f pour un port-forward. Il rafraîchit tout seul, propose le bon conteneur, suit un rollout en direct.

Le partage qui s'impose : k9s pour comprendre, kubectl pour agir et tracer.

Le guide k9s couvre l'installation, la navigation, la configuration, les plugins, et le mode lecture seule sans lequel il ne faut pas l'ouvrir sur une prod. Il renvoie lui-même vers les guides jq, tmux et zsh, et ce guide-ci est celui qu'il suppose que vous avez lu.

Puis, dans l'ordre où vous en aurez besoin :

  • kubectx et kubens : kubectx prod pour changer de cluster, kubens staging pour changer de namespace, avec la complétion et un - pour revenir au précédent. Deux scripts, près de dix ans d'existence, aucune raison de s'en passer.
  • stern : les logs de plusieurs pods à la fois, par sélecteur ou par expression régulière sur le nom, colorés par pod, en direct. stern api suit tous les pods dont le nom contient api. C'est kubectl logs -l avec les couleurs et le suivi des pods qui apparaissent.
  • krew : le gestionnaire de plugins kubectl. kubectl krew install neat pour nettoyer un -o yaml de ses champs de status, tree pour l'arbre des propriétaires d'un objet, who-can pour savoir qui a un droit donné. Les plugins sont des binaires kubectl-nom, invoqués comme des sous-commandes.
  • kubecolor : colore la sortie de kubectl. alias kubectl=kubecolor et c'est tout.
  • Helm : vu au chapitre précédent, installé dès que vous déployez un logiciel tiers.
  • Headlamp : pour ceux qui veulent une interface graphique. Projet CNCF, il est le successeur recommandé de Kubernetes Dashboard, dont le dépôt a été archivé en janvier 2026. Il se lance en application de bureau ou en service dans le cluster, respecte RBAC, et s'étend par plugins.

La complétion et l'alias k

Deux lignes qui changent le quotidien. La complétion de kubectl connaît les commandes, les types, les noms des objets de votre cluster et les namespaces :

# zsh, dans ~/.zshrc
source <(kubectl completion zsh)
alias k=kubectl
compdef k=kubectl

Avec Oh My Zsh, le plugin kubectl fait tout ça et ajoute une centaine d'alias ; le guide zsh montre la configuration complète, alias k compris. k get po -n <TAB> liste vos namespaces. Vous n'écrirez plus jamais un nom de pod en entier.

Les concepts en une phrase

Le mémo à imprimer, à côté de celui des commandes qui suit.

ConceptEn une phrase
ClusterUn cerveau et ses bras : le control plane décide, les nœuds exécutent
Control planeLe cerveau. Il décide de tout, il n'exécute rien
kube-apiserverLe guichet unique : ce qui ne passe pas par lui n'existe pas
etcdLa mémoire : ce qui n'y est pas écrit n'existe pas, et il n'y a pas de copie
kube-schedulerLe dispatcheur : il choisit la machine, il ne lance rien
kube-controller-managerLe contremaître : il compare le plan à la réalité, en boucle
NœudUn bras : une machine qui exécute et ne décide rien
kubeletLe chef d'entrepôt : il ne prend ses ordres que du siège
ManifesteLa destination écrite en YAML, jamais l'itinéraire
NamespaceUn dossier, pas un coffre-fort : il range, il ne protège pas
LabelL'étiquette qui sert à trier
AnnotationLe post-it qui sert à se souvenir
PodUn appartement en colocation : une adresse, un couloir, un bail commun
DeploymentLe contremaître des pods : il en maintient N et sait revenir en arrière
StatefulSetUn Deployment où chaque pod a un nom et un disque
DaemonSetUn détecteur de fumée par pièce : un pod par machine
Job, CronJobUne tâche qui doit finir ; la même à l'heure dite
ConfigMapLa config posée à côté de l'image, pas dedans
SecretUne ConfigMap avec un rideau, pas un coffre
ServiceLe numéro du standard, pas celui d'un employé
EndpointSliceLa liste de qui répond vraiment
Ingress, GatewayLa réception de l'immeuble : elle lit le nom et porte au bon étage
NetworkPolicyLe pare-feu entre pods
PVC, PV, StorageClassLe bon de commande, le disque livré, le catalogue
requests, limitsLa réservation qui choisit la machine, le plafond qui la protège
Taint, tolerationLe panneau "réservé", et le badge qui l'ignore
ServiceAccountLa carte d'identité d'un pod
RBACQui peut faire quoi, et où : tout ce qui n'est pas écrit est interdit
ProbesEs-tu vivant ? Peux-tu servir ? As-tu fini de démarrer ?
CRDUn mot ajouté au vocabulaire de l'API
OperatorL'expert embauché pour parler ce mot

À retenir

CommandePour quoi
kubectl explain pod.spec.containersLa doc de l'API, depuis votre cluster, pour votre version
kubectl create deploy X --image=Y --dry-run=client -o yamlGénérer un manifeste au lieu de l'écrire
kubectl diff -f f.yaml puis kubectl apply -f f.yamlVoir, puis appliquer
kubectl get X -o wide / -o yaml / -A / -w / -l k=vLes formats et filtres transversaux
kubectl config set-context --current --namespace=XNe plus taper -n
kubectl describe pod XTout, et les events en bas
kubectl logs X --previousLes logs de l'instance qui a crashé
kubectl exec -it X -- sh / kubectl debug -it X --image=busyboxEntrer dans un conteneur, ou s'en greffer un
kubectl port-forward svc/X 8080:80Joindre un Service depuis votre machine
kubectl rollout status / undo / restart deploy/XSuivre, annuler, redémarrer un déploiement
kubectl get endpointslices -l kubernetes.io/service-name=XLe Service a-t-il des pods derrière lui
kubectl get secret X -o jsonpath='{.data.k}' | base64 -dLire un Secret, qui n'est pas chiffré
kubectl describe pvc XPourquoi un volume reste en Pending
kubectl top nodes / podsLa consommation réelle
kubectl auth can-i V R --as system:serviceaccount:NS:SATester RBAC avant de livrer
kubectl events --for pod/X --watchLes events, lisibles, en direct
kubectl get crd / kubectl explain custom.specCe que des tiers ont ajouté à l'API, et sa doc

FAQ

Quelle est la différence entre un pod et un conteneur ?

Un conteneur est un processus isolé lancé à partir d'une image. Un pod est le groupe d'un ou plusieurs conteneurs que Kubernetes déploie ensemble sur un même nœud, avec une seule adresse IP, des volumes partagés et un cycle de vie commun. Kubernetes ne manipule jamais un conteneur seul : il place, redémarre et supprime des pods. Dans la plupart des cas, un pod contient un seul conteneur, et la distinction n'apparaît qu'avec les init containers et les sidecars.

Deployment ou StatefulSet, lequel choisir ?

Deployment, sauf si vos pods ont besoin d'une identité stable : un nom prévisible, un enregistrement DNS propre, un volume persistant à eux qui les suit s'ils sont recréés. C'est le cas d'une base de données, d'un broker de messages, d'un cluster qui élit un primaire. Pour une API, un site, un worker, tout ce qui est sans état et interchangeable, le Deployment est le bon choix, et de très loin le plus fréquent. Et pour une base de données, un operator plutôt qu'un StatefulSet nu.

ClusterIP ou NodePort pour exposer une application ?

Ni l'un ni l'autre, pour l'exposer à l'extérieur. ClusterIP est le défaut et sert au trafic interne entre vos services ; c'est ce que vous voulez dans 90 % des cas. NodePort ouvre un port entre 30000 et 32767 sur chaque nœud, sans équilibrage de charge ni TLS ; c'est une brique de bas niveau, pas une solution d'exposition. Pour l'extérieur, un Service LoadBalancer sur un cloud, et une Gateway ou un Ingress devant pour le HTTP.

Quelle différence entre requests et limits ?

requests est la réservation, utilisée par le scheduler pour choisir un nœud avec assez de place. limits est le plafond, appliqué à l'exécution par le noyau. Dépasser la limite CPU ralentit le conteneur ; dépasser la limite mémoire le tue, avec le code de sortie 137. Déclarez toujours des requests ; sans elles, le pod est BestEffort et sera le premier évincé quand un nœud manque de mémoire.

Faut-il Kubernetes pour trois conteneurs ?

Non. Trois conteneurs sur une machine, c'est Docker Compose ou des unités systemd. Kubernetes commence à valoir son coût quand vous avez plusieurs machines et que la perte de l'une d'elles doit être absorbée sans intervention humaine. En dessous, vous payez un plan de contrôle, une couche réseau et trois versions par an pour un bénéfice que vous ne touchez pas. L'apprendre reste utile, parce que c'est devenu l'API commune de l'infrastructure ; l'utiliser en prod pour trois conteneurs ne l'est pas.

Docker est-il mort ?

Non. Kubernetes a retiré en 1.24 l'adaptateur qui lui permettait d'utiliser Docker Engine comme runtime, au profit de containerd et CRI-O. Le format des images n'a pas changé : une image construite par Docker est une image OCI, et c'est ce que containerd exécute. Vos Dockerfiles, vos images et vos registres fonctionnent exactement comme avant. Docker construit, Kubernetes exécute, et c'est l'arrangement normal.

Un Secret Kubernetes est-il chiffré ?

Non, il est encodé en base64, ce qui n'est pas un chiffrement : base64 -d suffit à lire la valeur. Un Secret se distingue d'une ConfigMap par des droits RBAC séparés, un masquage dans kubectl describe et un stockage en mémoire sur les nœuds. Dans etcd, il est en clair sauf si le chiffrement au repos a été configuré sur l'API server, ce qui n'est pas le cas par défaut. Un Secret commité dans Git est un secret publié.

Pourquoi mon pod reste-t-il en Pending ?

Parce que le scheduler ne trouve aucun nœud qui convienne, presque toujours. kubectl describe pod le dit dans ses events, tout en bas : Insufficient memory, Insufficient cpu, une taint non tolérée, une affinité impossible, un volume attaché à un autre nœud. Un PVC lui-même en Pending bloque aussi le pod. Un problème d'image ne donne pas Pending mais ImagePullBackOff, avec un nœud déjà attribué.

Ingress ou Gateway API en 2026 ?

Gateway API pour tout nouveau projet : l'API est stable, en version 1.6, couvre HTTP, gRPC, TLS, TCP et UDP, et sépare proprement les rôles. Ingress n'est pas déprécié et vos ressources existantes continuent de fonctionner. En revanche, Ingress-NGINX, le contrôleur Ingress que la plupart des tutoriels installent, est retiré depuis mars 2026 et ne reçoit plus aucun correctif de sécurité. Si vous l'utilisez, migrez, vers Gateway API avec ingress2gateway ou vers un autre contrôleur maintenu.

Par où commencer pour apprendre Kubernetes ?

Par un cluster local, kind ou minikube, et par les exercices de ce guide dans l'ordre : ils construisent chacun sur le précédent, du pod nu qui ne revient pas jusqu'au CrashLoopBackOff diagnostiqué en trois commandes. Ne commencez pas par un cluster cloud, ni par Helm, ni par un tutoriel qui déploie une application à douze microservices. Une fois les seize chapitres faits, la documentation officielle de kubernetes.io devient lisible, et la certification KCNA couvre à peu près ce périmètre.

Pour aller plus loin

  • Le guide k9s - la suite naturelle : piloter ce que vous venez d'apprendre au terminal, sans réécrire les commandes

  • Le guide Docker - construire les images que ce guide déploie, et les construire bien

  • Le guide systemd - l'alternative pour une machine, et le sandboxing dont le securityContext est le cousin

  • Le guide jq - traiter les sorties -o json de kubectl au-delà de ce que jsonpath sait faire

  • Documentation Kubernetes - Concepts - la référence, dense mais exacte, lisible une fois ce guide terminé

  • Kubernetes API Reference - la version en ligne de kubectl explain, avec les liens entre types

  • Gateway API - la documentation officielle, avec le guide de migration depuis Ingress

  • ingress2gateway - convertir vos ressources Ingress existantes

  • kind - le cluster local utilisé dans ce guide, et sa configuration multi-nœuds

  • Pod Security Standards - le détail des trois profils

  • Les certifications KCNA et KCSA - le format des deux épreuves, les domaines avec leurs pondérations officielles, et un quiz d'entraînement. Ce guide couvre les domaines Kubernetes Fundamentals et Container Orchestration, soit 72 % du programme de la KCNA

  • kubectl explain, kubectl api-resources, kubectl --help - vos trois sources qui sont toujours à jour avec votre cluster