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 apporte | Kubernetes vous coûte |
|---|---|
| Le placement automatique des conteneurs sur les machines | Un plan de contrôle à héberger, sauvegarder et mettre à jour |
| Le redémarrage et le déplacement automatiques en cas de panne | Une couche réseau virtuelle à comprendre avant de pouvoir déboguer |
| Le passage de 3 à 30 copies en une commande | Un modèle de stockage qui ne ressemble à rien de ce que vous connaissez |
| Une API unique, déclarative, versionnée, la même partout | Une courbe d'apprentissage qui se compte en semaines, pas en heures |
| Un écosystème immense : un outil existe pour chaque besoin | Un écosystème immense : il faut choisir, et les choix vieillissent vite |
| Le même outillage du laptop au cloud | Trois 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 :
| Outil | Ce qu'il simule fidèlement | Ce qu'il fausse | Pour qui |
|---|---|---|---|
| kind | Un 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 minimal | Suivre ce guide, tester des manifestes, la CI |
| minikube | Un 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-machines | Découvrir sans se poser de questions |
| k3s | Une distribution Kubernetes complète et allégée, la même en local et sur un vrai serveur | Quelques composants remplacés (SQLite à la place d'etcd par défaut, Traefik intégré) | Un homelab, de l'edge, une petite prod |
| Talos | Un OS immuable conçu pour Kubernetes, sans shell ni SSH, piloté entièrement par API | Rien : c'est un cluster de prod. C'est aussi plus lourd à prendre en main | Ceux 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.
kube-apiserver · seul lui parle à etcd, l'unique état du clusterLe 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
LoadBalancery 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
| Commande | Pour quoi |
|---|---|
kubectl cluster-info | L'adresse de l'API server et du DNS du cluster |
kubectl get nodes -o wide | Les nœuds, leur version, leur OS, leur runtime et leur IP |
kubectl describe node rudeops-worker | Tout sur un nœud : capacité, ressources allouées, conditions, pods hébergés |
kubectl get --raw /readyz | L'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.
spec, le contrôleur poursuit status· un pod supprimé revient parce que la boucle ne s'arrête jamaisCette 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.yamlcrée l'objet et échoue s'il existe déjà.kubectl replace -f fichier.yamlremplace l'objet entier et échoue s'il n'existe pas.kubectl edit deployment/apiouvre l'objet dans votre éditeur ; la modification est immédiate et ne laisse aucune trace dans vos fichiers.kubectl apply -f fichier.yamlcré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.
| Commande | Pour quoi |
|---|---|
kubectl explain pod.spec.containers.resources | La 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 yaml | Gé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.yaml | Ce 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 wide | Les colonnes supplémentaires, selon le type : nœud et IP pour un pod, conteneurs et images pour un Deployment |
kubectl get X -o yaml | L'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 -A | Tous les namespaces (--all-namespaces) |
kubectl get X --watch | Suivre les changements en direct, -w en court |
kubectl get X -l app=api | Filtrer par label |
kubectl api-resources | Tous les types que ce cluster connaît, avec leur nom court et leur groupe |
kubectl api-versions | Tous les groupes et versions disponibles |
kubectl config get-contexts / use-context nom | Lister les clusters connus, changer de cluster |
kubectl wait --for=condition=Ready pod/X --timeout=60s | Bloquer 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
| Commande | Pour quoi |
|---|---|
kubectl get ns | Lister les namespaces |
kubectl create ns staging | En créer un |
kubectl get pods -n staging | Cibler un namespace |
kubectl label pod api-x env=prod | Poser un label (env- pour le retirer, --overwrite pour le changer) |
kubectl annotate deploy api rudeops.com/owner=cyril | Poser une annotation |
kubectl get pods -l app=api,env=prod | Filtrer par plusieurs labels ; -l 'env in (prod,staging)' pour un ensemble, -l '!canary' pour l'absence |
kubectl get pods --show-labels | Afficher les labels en colonne |
kubectl get pods --field-selector status.phase=Running | Filtrer 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
localhostqui 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.
localhost · le sidecar est un init container avec restartPolicy: AlwaysPlusieurs 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
applyne déclenche aucun rollout, puisque le manifeste n'a pas changé ; - un
rollout undovous 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
| Commande | Pour quoi |
|---|---|
kubectl run web --image=nginx:1.27 | Créer un pod sans manifeste. --rm -it --restart=Never -- sh pour un pod jetable interactif |
kubectl get pods -o wide | Le nœud et l'IP de chaque pod |
kubectl get pods --field-selector status.phase=Pending | Les pods qui n'ont pas démarré |
kubectl describe pod web | Tout : conteneurs, état, volumes, conditions, et les events en bas |
kubectl logs web | Les logs. -f pour suivre, -c nom pour choisir le conteneur, --since=10m, --tail=100 |
kubectl logs web --previous | Les logs de l'instance précédente, après un crash |
kubectl exec -it web -- sh | Un shell dans le conteneur. -c nom s'il y en a plusieurs |
kubectl debug -it web --image=busybox:1.36 --target=nginx | Attacher un conteneur de débogage éphémère à un pod qui n'a pas de shell |
kubectl port-forward pod/web 8080:80 | Joindre le port 80 du pod sur localhost:8080 |
kubectl cp web:/etc/nginx/nginx.conf ./nginx.conf | Copier un fichier depuis ou vers le conteneur |
kubectl delete pod web | Supprimer. --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ôleur | Pour quoi | Identité des pods | Ordre | Stockage | Exemple |
|---|---|---|---|---|---|
| ReplicaSet | Maintenir N copies identiques d'un pod | Anonymes, interchangeables, nom aléatoire | Aucun | Partagé ou aucun | Vous ne l'utilisez jamais directement |
| Deployment | Gérer un ReplicaSet et ses mises à jour sans coupure | Anonymes | Aucun | Partagé ou aucun | Une API, un site, tout ce qui est sans état |
| StatefulSet | Des pods avec une identité stable et un volume chacun | Nommés x-0, x-1, x-2, DNS propre | Démarrage et arrêt ordonnés | Un volume persistant par pod | Une base de données, un broker, tout ce qui a un état |
| DaemonSet | Exactement un pod par nœud | Un par nœud | Aucun | Souvent hostPath | Un agent de logs, de métriques, un CNI |
| Job | Exécuter une tâche jusqu'à ce qu'elle réussisse | Anonymes | Parallélisme configurable | Aucun en général | Une migration, un traitement par lots |
| CronJob | Créer un Job selon un calendrier | Anonymes | Aucun | Aucun en général | Une 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
| Commande | Pour quoi |
|---|---|
kubectl create deployment api --image=X --replicas=3 | Créer un Deployment sans manifeste |
kubectl scale deployment/api --replicas=5 | Changer le nombre de répliques |
kubectl set image deployment/api api=ghcr.io/rudeops/api:1.5.0 | Changer l'image d'un conteneur, conteneur=image |
kubectl rollout status deployment/api | Suivre un déploiement jusqu'à sa fin, ou son blocage |
kubectl rollout history deployment/api | Les révisions. --revision=2 pour le détail d'une |
kubectl rollout undo deployment/api | Revenir à la révision précédente. --to-revision=N pour une révision précise |
kubectl rollout restart deployment/api | Recréer tous les pods, proprement, par vagues |
kubectl get rs | Les ReplicaSets, dont les anciens conservés pour l'undo |
kubectl get jobs / kubectl get cronjobs | Les tâches et leur planification |
kubectl create job manuel --from=cronjob/backup | Lancer 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
| Commande | Pour quoi |
|---|---|
kubectl create configmap api-config --from-literal=LOG_LEVEL=info | Une clé, une valeur |
kubectl create configmap api-config --from-file=app.properties | Une clé par fichier, nommée d'après le fichier |
kubectl create configmap api-config --from-env-file=.env | Une clé par ligne CLE=valeur du fichier |
kubectl create secret generic api-secret --from-literal=DB_PASSWORD=x | Un Secret générique. --from-file fonctionne pareil |
kubectl create secret tls api-tls --cert=tls.crt --key=tls.key | Un Secret TLS, pour Ingress et Gateway |
kubectl describe configmap api-config | Le contenu, en clair |
kubectl get secret api-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d | Lire 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 :
- Chaque pod a sa propre adresse IP, unique dans le cluster.
- Tous les pods peuvent se joindre entre eux par cette IP, sans NAT, quel que soit le nœud.
- 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.
kubectl get endpointslices -l kubernetes.io/service-name=api le dit en une commandeLe 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-1de joindrepostgres-0par 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
| Commande | Pour quoi |
|---|---|
kubectl expose deployment api --port=80 --target-port=8080 | Créer un Service ClusterIP qui reprend le sélecteur du Deployment |
kubectl get svc | Les Services, leur type, leur IP, leurs ports |
kubectl get endpointslices -l kubernetes.io/service-name=api | Les IP des pods derrière un Service. Vide = le sélecteur ne matche rien |
kubectl get ingress / kubectl get httproutes | Les règles d'exposition HTTP, selon l'API utilisée |
kubectl port-forward svc/api 8080:80 | Joindre un Service depuis votre machine, pour tester |
kubectl run -i --rm test --image=busybox:1.36 --restart=Never -- wget -qO- http://api | Tester 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ès | Abréviation | Ce que ça veut dire |
|---|---|---|
ReadWriteOnce | RWO | Lecture-écriture par un seul nœud à la fois. Un disque bloc classique |
ReadOnlyMany | ROX | Lecture seule, par plusieurs nœuds |
ReadWriteMany | RWX | Lecture-écriture par plusieurs nœuds. Il faut un stockage réseau qui le supporte, NFS, CephFS, un service de fichiers cloud |
ReadWriteOncePod | RWOP | Lecture-écriture par un seul pod, plus strict que RWO qui autorise plusieurs pods sur le même nœud |
| Politique de récupération | Quand le PVC est supprimé |
|---|---|
Delete | Le PV et le stockage sous-jacent sont supprimés. Le défaut du provisionnement dynamique |
Retain | Le 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
| Commande | Pour quoi |
|---|---|
kubectl get sc | Les StorageClasses, et laquelle est (default) |
kubectl get pvc | Les demandes, leur état (Pending, Bound), leur taille, leur classe |
kubectl get pv | Les volumes réels, leur politique de récupération, le PVC lié |
kubectl describe pvc data | Les events : la raison d'un Pending est là |
kubectl get pvc -w | Suivre 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 :
- 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 scconfirme l'absence de(default); nommez la classe dans le PVC, ou marquez-en une par défaut. - 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 pvau PVC. - 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,requiredqui filtre etpreferredqui score. L'anti-affinité de pods est celle qui compte : "ne place pas deux répliques deapisur 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
| Commande | Pour quoi |
|---|---|
kubectl top nodes / kubectl top pods | La 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-worker | La section Allocated resources : ce qui est réservé, contre la capacité |
kubectl get pods --field-selector status.phase=Pending | Les pods sans nœud |
kubectl taint nodes rudeops-worker gpu=true:NoSchedule | Poser une taint (gpu- à la fin pour la retirer) |
kubectl cordon rudeops-worker | Interdire 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-worker | Rouvrir 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-systemet 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
securityContextci-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
| Commande | Pour quoi |
|---|---|
kubectl auth can-i create deployments | Ai-je le droit ? Répond yes ou no |
kubectl auth can-i --list | Tout ce que j'ai le droit de faire dans ce namespace |
kubectl auth can-i delete pods --as system:serviceaccount:default:lecteur | Tester les droits de quelqu'un d'autre, sans être lui |
kubectl auth can-i get nodes --as jeanne --as-group dev | Idem pour un utilisateur et un groupe |
kubectl create role lecture --verb=get,list --resource=pods | Créer un Role sans manifeste |
kubectl create rolebinding x --role=lecture --serviceaccount=default:lecteur | Et le binding |
kubectl get sa / kubectl get roles,rolebindings | Ce 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
kubectl get: quel objet est dans quel état.get podspour leSTATUSet lesRESTARTS,get deploypour leREADY,get eventspour ce qui vient de se passer. Ne cherchez pas encore la cause ; cherchez l'objet qui ne va pas.kubectl describesur 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.kubectl logs, avec--previoussi le conteneur a redémarré. C'est là que votre application a écrit pourquoi elle est morte.- Les events du namespace, s'il n'y a pas d'objet évident :
kubectl eventsmontre ce qui s'est passé partout dans l'ordre chronologique, y compris sur des objets déjà disparus. 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
CrashLoopBackOffd'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ôme | Cause probable | Correction |
|---|---|---|
CrashLoopBackOff | Le processus meurt au démarrage, encore et encore | logs --previous, puis corriger l'application ou sa configuration |
ImagePullBackOff / ErrImagePull | Nom ou tag d'image faux, registre privé sans identifiants | describe, vérifier le nom exact et l'imagePullSecret |
Pending | Aucun nœud ne convient | describe, lire les events du scheduler |
OOMKilled, code 137 | Limite mémoire dépassée | Relever la limite, ou corriger la fuite |
Evicted | Pression sur le nœud, le pod a été sacrifié | Poser des requests, viser une classe QoS meilleure que BestEffort |
CreateContainerConfigError | ConfigMap ou Secret référencé absent | Vérifier les noms et les clés référencés |
Terminating qui n'en finit pas | Un finalizer bloque, ou l'arrêt gracieux traîne | describe, regarder metadata.finalizers ; en dernier recours --force |
Init:0/1, Init:Error | Un init container échoue | logs -c <nom-de-l-init> |
Running mais Service muet | Le sélecteur ne matche rien, ou readiness en échec | get endpointslices, puis describe les probes |
ContainerCreating qui dure | Un volume ne se monte pas, un Secret manque, une image est lourde | describe, event MountVolume ou Pulling |
Les commandes du débogage
| Commande | Pour quoi |
|---|---|
kubectl events --for pod/api-x --watch | Les events d'un objet, en direct, lisibles |
kubectl events --types=Warning | Seulement les avertissements du namespace |
kubectl get events --sort-by=.lastTimestamp | L'ancienne forme, triée, quand vous la trouvez dans un runbook |
kubectl describe pod api-x | L'état complet et les events de l'objet |
kubectl logs deploy/api --all-containers --prefix -f | Les logs de tous les pods et conteneurs d'un Deployment, préfixés |
kubectl logs -l app=api --tail=50 | Les logs par sélecteur |
kubectl debug -it api-x --image=busybox:1.36 --target=api | Un conteneur de débogage dans un pod distroless, avec les outils qui manquent |
kubectl debug node/rudeops-worker -it --image=ubuntu | Un shell sur le nœud, monté sous /host |
kubectl top pods --containers | La 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-zne 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 deployaffiche 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 ;pdans la vue logs bascule sur l'instance précédente, le--previousdu 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
| Commande | Pour quoi |
|---|---|
kubectl get crd | Tous les types ajoutés au cluster |
kubectl api-resources --api-group=cert-manager.io | Les types d'un groupe précis, avec leurs noms courts |
kubectl explain certificate.spec | La documentation d'une ressource custom, depuis le schéma de sa CRD |
kubectl get certificates -A | Les 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 prodpour changer de cluster,kubens stagingpour 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 apisuit tous les pods dont le nom contientapi. C'estkubectl logs -lavec les couleurs et le suivi des pods qui apparaissent. - krew : le gestionnaire de plugins kubectl.
kubectl krew install neatpour nettoyer un-o yamlde ses champs de status,treepour l'arbre des propriétaires d'un objet,who-canpour savoir qui a un droit donné. Les plugins sont des binaireskubectl-nom, invoqués comme des sous-commandes. - kubecolor : colore la sortie de kubectl.
alias kubectl=kubecoloret 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.
| Concept | En une phrase |
|---|---|
| Cluster | Un cerveau et ses bras : le control plane décide, les nœuds exécutent |
| Control plane | Le cerveau. Il décide de tout, il n'exécute rien |
| kube-apiserver | Le guichet unique : ce qui ne passe pas par lui n'existe pas |
| etcd | La mémoire : ce qui n'y est pas écrit n'existe pas, et il n'y a pas de copie |
| kube-scheduler | Le dispatcheur : il choisit la machine, il ne lance rien |
| kube-controller-manager | Le contremaître : il compare le plan à la réalité, en boucle |
| Nœud | Un bras : une machine qui exécute et ne décide rien |
| kubelet | Le chef d'entrepôt : il ne prend ses ordres que du siège |
| Manifeste | La destination écrite en YAML, jamais l'itinéraire |
| Namespace | Un dossier, pas un coffre-fort : il range, il ne protège pas |
| Label | L'étiquette qui sert à trier |
| Annotation | Le post-it qui sert à se souvenir |
| Pod | Un appartement en colocation : une adresse, un couloir, un bail commun |
| Deployment | Le contremaître des pods : il en maintient N et sait revenir en arrière |
| StatefulSet | Un Deployment où chaque pod a un nom et un disque |
| DaemonSet | Un détecteur de fumée par pièce : un pod par machine |
| Job, CronJob | Une tâche qui doit finir ; la même à l'heure dite |
| ConfigMap | La config posée à côté de l'image, pas dedans |
| Secret | Une ConfigMap avec un rideau, pas un coffre |
| Service | Le numéro du standard, pas celui d'un employé |
| EndpointSlice | La liste de qui répond vraiment |
| Ingress, Gateway | La réception de l'immeuble : elle lit le nom et porte au bon étage |
| NetworkPolicy | Le pare-feu entre pods |
| PVC, PV, StorageClass | Le bon de commande, le disque livré, le catalogue |
| requests, limits | La réservation qui choisit la machine, le plafond qui la protège |
| Taint, toleration | Le panneau "réservé", et le badge qui l'ignore |
| ServiceAccount | La carte d'identité d'un pod |
| RBAC | Qui peut faire quoi, et où : tout ce qui n'est pas écrit est interdit |
| Probes | Es-tu vivant ? Peux-tu servir ? As-tu fini de démarrer ? |
| CRD | Un mot ajouté au vocabulaire de l'API |
| Operator | L'expert embauché pour parler ce mot |
À retenir
| Commande | Pour quoi |
|---|---|
kubectl explain pod.spec.containers | La doc de l'API, depuis votre cluster, pour votre version |
kubectl create deploy X --image=Y --dry-run=client -o yaml | Générer un manifeste au lieu de l'écrire |
kubectl diff -f f.yaml puis kubectl apply -f f.yaml | Voir, puis appliquer |
kubectl get X -o wide / -o yaml / -A / -w / -l k=v | Les formats et filtres transversaux |
kubectl config set-context --current --namespace=X | Ne plus taper -n |
kubectl describe pod X | Tout, et les events en bas |
kubectl logs X --previous | Les logs de l'instance qui a crashé |
kubectl exec -it X -- sh / kubectl debug -it X --image=busybox | Entrer dans un conteneur, ou s'en greffer un |
kubectl port-forward svc/X 8080:80 | Joindre un Service depuis votre machine |
kubectl rollout status / undo / restart deploy/X | Suivre, annuler, redémarrer un déploiement |
kubectl get endpointslices -l kubernetes.io/service-name=X | Le Service a-t-il des pods derrière lui |
kubectl get secret X -o jsonpath='{.data.k}' | base64 -d | Lire un Secret, qui n'est pas chiffré |
kubectl describe pvc X | Pourquoi un volume reste en Pending |
kubectl top nodes / pods | La consommation réelle |
kubectl auth can-i V R --as system:serviceaccount:NS:SA | Tester RBAC avant de livrer |
kubectl events --for pod/X --watch | Les events, lisibles, en direct |
kubectl get crd / kubectl explain custom.spec | Ce 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
securityContextest le cousin -
Le guide jq - traiter les sorties
-o jsonde kubectl au-delà de ce quejsonpathsait 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