DevOps, CI/CD et conteneurs
Les bases de Kubernetes : architecture, Pods, Deployments et Services
Intermédiaire10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Kubernetes remplace une pile de scripts de déploiement par une boucle de contrôle : vous déclarez l'état souhaité et le cluster travaille en permanence pour l'atteindre. Ce guide explique les composants qui font tourner cette boucle, les objets de base que vous utiliserez tous les jours — Pods, ReplicaSets, Deployments, Services, ConfigMaps et Secrets — et se termine par un manifeste complet pour une vraie application, ainsi que les commandes kubectl pour l'exploiter.
Ce que vous allez apprendre
- Décrire les composants du control plane et des nœuds, et le rôle de chacun
- Expliquer la boucle de réconciliation et pourquoi la configuration Kubernetes est déclarative
- Créer des Deployments et comprendre les couches Pod et ReplicaSet qui se trouvent en dessous
- Exposer des workloads avec des Services et les résoudre via le DNS du cluster
- Fournir configuration et secrets aux conteneurs sans reconstruire les images
- Utiliser labels, selectors et namespaces pour organiser et sélectionner les objets
Une fois qu'une application est empaquetée sous forme d'image de conteneur, une nouvelle question se pose : qu'est-ce qui l'exécute ? Sur une seule machine, une politique de redémarrage et un script suffisent. Sur tout un parc, il faut quelque chose qui décide sur quelle machine tourne chaque conteneur, le redémarre quand il meurt, le déplace quand un nœud tombe, remplace l'ancienne version par la nouvelle sans perdre de trafic, et donne aux autres services un moyen stable de le trouver. Ce travail s'appelle l'orchestration, et Kubernetes est devenu la réponse standard.
Ce qui rend Kubernetes singulier, ce n'est pas la liste des fonctionnalités, c'est le modèle. Vous ne lui demandez pas de démarrer un conteneur ; vous enregistrez l'état souhaité dans la base de données du cluster, et un ensemble de contrôleurs travaille en continu pour que la réalité s'y conforme. Une fois cette idée assimilée, le reste du système — messages d'erreur compris — devient bien plus facile à lire. Ce guide part de l'architecture pour arriver à un manifeste complet et fonctionnel, en partant du principe que vous savez déjà ce qu'est une image grâce au guide Les bases de Docker.
Comment un cluster est constitué
Un cluster, c'est un control plane plus un ensemble de nœuds worker.
Le control plane exécute quatre composants. L'API server est la porte d'entrée : tout — kubectl, les contrôleurs, les kubelets de chaque nœud — lui parle via une API REST authentifiée, et rien ne parle à rien d'autre. etcd est le magasin clé-valeur qui contient tout l'état du cluster ; si vous ne sauvegardez qu'une chose, sauvegardez etcd. Le scheduler surveille les Pods sans nœud assigné et leur en choisit un, en fonction des demandes de ressources, des labels de nœud, des taints et des règles d'affinité. Le controller manager fait tourner les boucles de réconciliation : le contrôleur de Deployments, le contrôleur de nœuds, le contrôleur de jobs et des dizaines d'autres, chacun surveillant les objets d'un type donné et agissant sur les écarts. Sur un cluster managé, vous ne voyez jamais ces processus ; sur un cluster autogéré, ils tournent généralement sous forme de Pods statiques sur des machines dédiées, en nombre impair pour le quorum d'etcd.
Chaque nœud worker exécute trois choses. Le kubelet est l'agent qui dialogue avec l'API server, reçoit les Pods assignés à son nœud, demande au container runtime de les démarrer et remonte leur statut. Le container runtime — containerd ou CRI-O dans les clusters actuels, depuis la suppression du shim Docker en 1.24 — télécharge réellement les images et exécute les conteneurs. kube-proxy programme les règles réseau du nœud pour que le trafic destiné à l'IP virtuelle d'un Service atteigne l'un des Pods situés derrière.
Ajoutez un plugin CNI (Calico, Cilium, Flannel et d'autres) qui attribue à chaque Pod une IP routable, et CoreDNS, qui tourne comme un workload pour résoudre les noms de Services, et vous avez un cluster.
La boucle de réconciliation
Chaque contrôleur exécute la même boucle : observer l'état courant via l'API server, le comparer à l'état souhaité enregistré dans la spec de l'objet, et faire un pas pour réduire l'écart. Rien n'est une commande ponctuelle.
C'est pour cela que les objets Kubernetes ont une spec (ce que vous voulez) et un status (ce qui est vrai à l'instant présent), et que la réponse à « pourquoi mon Pod ne tourne-t-il pas ? » se trouve presque toujours en lisant le statut et les événements qui lui sont rattachés. Supprimez un Pod qui appartient à un Deployment et un remplaçant apparaît en quelques secondes — non pas parce que la suppression a déclenché une création, mais parce qu'un contrôleur a constaté qu'il manquait un Pod par rapport à ce que demande la spec.
C'est aussi pour cela que la configuration est déclarative. Vous écrivez du YAML qui décrit l'état final et vous l'appliquez :
kubectl apply -f k8s/ # create or update everything in the directory
kubectl diff -f k8s/ # what would change, before it changes
kubectl delete -f k8s/
kubectl apply enregistre la configuration que vous avez fournie : appliquer un fichier modifié supprime donc les champs que vous avez retirés et laisse intacts ceux gérés par d'autres acteurs. Les commandes impératives (kubectl create deployment, kubectl scale, kubectl edit) existent et sont utiles pour explorer, mais la configuration de production a sa place dans des fichiers sous contrôle de version — le même argument que pour l'infrastructure as code en général.
Les Pods
Un Pod est la plus petite unité que Kubernetes planifie. Il contient un ou plusieurs conteneurs qui partagent un namespace réseau — une seule adresse IP, un seul espace de ports, si bien qu'ils se joignent sur localhost — et peuvent partager des volumes. Les conteneurs d'un Pod sont toujours placés sur le même nœud, et ils vivent et meurent ensemble.
La plupart des Pods n'ont qu'un conteneur applicatif. Le second conteneur, quand il existe, est un auxiliaire : un forwarder de logs qui lit un volume partagé, un proxy de service mesh qui intercepte le trafic, un agent qui rafraîchit un fichier d'identifiants.
apiVersion: v1
kind: Pod
metadata:
name: shop-api
labels:
app: shop-api
spec:
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
Vous ne devriez quasiment jamais créer un Pod de cette façon. Un Pod isolé n'est pas recréé si son nœud meurt : personne ne le possède. Les Pods sont du bétail, créés et détruits par des contrôleurs, et leurs noms, adresses IP et durées de vie sont tous éphémères. Tout ce qui doit persister va dans un volume ; tout ce qui doit être joignable se place derrière un Service.
Deployments et ReplicaSets
Un ReplicaSet maintient en vie un nombre donné de Pods correspondant à un template. Un Deployment possède des ReplicaSets et gère les transitions entre eux, ce qui vous donne des rollouts versionnés et le rollback.
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-api
labels: { app: shop-api }
spec:
replicas: 3
selector:
matchLabels:
app: shop-api
template:
metadata:
labels:
app: shop-api
version: "1.4.2"
spec:
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
ports:
- containerPort: 8080
Le selector indique au Deployment quels Pods lui appartiennent ; le template est le Pod qu'il produit à la chaîne. Les deux doivent concorder — les labels du template doivent satisfaire le selector — et le selector est immuable après la création : choisissez donc un label stable comme app plutôt qu'une valeur qui change à chaque release.
Changez le tag de l'image et réappliquez : le Deployment crée un second ReplicaSet, le fait monter en charge à mesure que les nouveaux Pods deviennent prêts, et réduit l'ancien. kubectl rollout status deployment/shop-api suit la progression ; kubectl rollout undo revient au ReplicaSet précédent. Le degré de progressivité — surge, indisponibilité, attente de la readiness — est détaillé dans le guide Patterns Kubernetes.
Deux autres contrôleurs de workloads existent pour d'autres besoins : le DaemonSet exécute exactement un Pod par nœud (agents de logs, node exporters), et le StatefulSet donne aux Pods des identités stables et leur propre stockage (bases de données, brokers).
Labels, selectors et namespaces
Kubernetes n'a pas de hiérarchie d'objets. À la place, tout est étiqueté avec des labels — des paires clé/valeur arbitraires — et les autres objets trouvent ce dont ils ont besoin grâce à des selectors. Un Service trouve ses Pods par selector, un Deployment possède ses Pods par selector, et vous interrogez aussi par selector :
kubectl get pods -l app=shop-api
kubectl get pods -l 'environment in (staging,prod),tier!=batch'
kubectl label pod shop-api-7c9f-2k4x debug=true # add a label on the fly
Cette dernière astuce mérite d'être retenue : modifiez les labels d'un Pod pour que le selector du Service ne corresponde plus, et le Pod sort de l'équilibrage de charge tout en continuant de tourner. Vous pouvez alors attacher un débogueur à une instance défaillante sans que les utilisateurs l'atteignent, et le Deployment démarrera un remplaçant pour conserver le nombre de réplicas.
Utilisez les annotations pour les métadonnées non identifiantes — une URL de build, un checksum, la configuration d'un ingress controller. Les selectors les ignorent.
Les namespaces partitionnent un cluster : les noms doivent être uniques au sein d'un namespace, et les quotas, règles RBAC et network policies s'appliquent par namespace. Les découpages typiques se font par équipe, ou par environnement quand plusieurs environnements partagent un cluster. Les objets de portée cluster — nœuds, PersistentVolumes, StorageClasses — existent en dehors des namespaces.
kubectl create namespace staging
kubectl config set-context --current --namespace=staging # stop typing -n
kubectl get all -n staging
Les Services et comment le trafic trouve un Pod
Les IP des Pods changent en permanence. Un Service fournit une IP virtuelle et un nom DNS stables devant un ensemble de Pods sélectionnés par label, et répartit la charge entre ceux qui sont prêts.
apiVersion: v1
kind: Service
metadata:
name: shop-api
spec:
type: ClusterIP
selector:
app: shop-api
ports:
- name: http
port: 80 # the Service port
targetPort: 8080 # the container port
Quatre types comptent :
| Type | Ce qu'il vous apporte | Usage typique |
|---|---|---|
ClusterIP | Une IP virtuelle joignable uniquement depuis l'intérieur du cluster | Appels de service à service. Le type par défaut. |
NodePort | La même chose, plus un port élevé ouvert sur chaque nœud | Clusters bare metal, démos rapides |
LoadBalancer | Un NodePort plus un load balancer cloud avec une IP externe | Points d'entrée publics sur les clusters managés |
Headless (clusterIP: None) | Pas d'IP virtuelle ; le DNS renvoie directement les IP des Pods | StatefulSets, équilibrage de charge côté client |
À l'intérieur du cluster, CoreDNS résout shop-api dans le même namespace, et shop-api.staging.svc.cluster.local depuis n'importe où. La chaîne de connexion dans la configuration d'un autre service est donc simplement http://shop-api/, et elle continue de fonctionner à travers tous les redémarrages, replanifications et rollouts.
Exposer de nombreux services HTTP derrière une seule adresse relève d'un Ingress ou de la plus récente Gateway API plutôt que d'un LoadBalancer par service, sujet traité dans le guide des patterns.
Configuration et secrets
Les images doivent être identiques d'un environnement à l'autre ; seule la configuration change. Les ConfigMaps contiennent la configuration non sensible, les Secrets les identifiants, et les deux peuvent être exposés sous forme de variables d'environnement ou de fichiers.
apiVersion: v1
kind: ConfigMap
metadata:
name: shop-api-config
data:
SPRING_PROFILES_ACTIVE: "prod"
application.yaml: |
shop:
currency: EUR
catalogue:
page-size: 50
---
apiVersion: v1
kind: Secret
metadata:
name: shop-api-db
type: Opaque
stringData:
username: "shop"
password: "correct-horse-battery-staple"
stringData vous permet d'écrire des valeurs en clair que l'API server encode en base64 à l'entrée — bien plus facile à relire que des blobs pré-encodés. Consommez-les dans la spec du Pod :
envFrom:
- configMapRef:
name: shop-api-config
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: shop-api-db
key: password
volumeMounts:
- name: config
mountPath: /config
readOnly: true
Le montage sous forme de fichier a une propriété utile : les mises à jour du ConfigMap se propagent dans le conteneur en cours d'exécution en une minute ou deux, si bien qu'une application qui surveille son fichier de configuration peut prendre en compte les changements sans redémarrer. Les variables d'environnement, en revanche, sont figées au démarrage du conteneur — modifier un ConfigMap ne redémarre rien, et c'est pourquoi les équipes ajoutent une annotation de checksum au template de Pod pour qu'une mise à jour déclenche un rollout du Deployment.
Ne commitez pas dans Git de manifestes de Secrets contenant de vraies valeurs. Utilisez Sealed Secrets, l'External Secrets Operator ou le gestionnaire de secrets de votre cloud, et activez le chiffrement au repos pour etcd.
Un manifeste complet
Tout ce qui précède, pour un seul service :
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-api
namespace: staging
labels: { app: shop-api }
spec:
replicas: 3
selector:
matchLabels: { app: shop-api }
template:
metadata:
labels: { app: shop-api }
annotations:
config-checksum: "8f4c1a9e"
spec:
serviceAccountName: shop-api
securityContext:
runAsNonRoot: true
runAsUser: 10001
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef: { name: shop-api-config }
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: shop-api-db, key: password }
- name: POD_NAME
valueFrom:
fieldRef: { fieldPath: metadata.name }
volumeMounts:
- { name: config, mountPath: /config, readOnly: true }
- { name: tmp, mountPath: /tmp }
volumes:
- name: config
configMap: { name: shop-api-config }
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: shop-api
namespace: staging
spec:
selector: { app: shop-api }
ports:
- { name: http, port: 80, targetPort: http }
Remarquez targetPort: http, qui fait référence au port nommé du conteneur : si vous renommez le port plus tard, le Service suit. fieldRef injecte des métadonnées du cluster dans le conteneur, et c'est ainsi que les applications apprennent le nom de leur propre Pod pour la journalisation. Enfin, l'emptyDir sur /tmp est ce qui permettra plus tard de monter le système de fichiers du conteneur en lecture seule sans casser les bibliothèques qui ont besoin d'un espace de travail temporaire.
Travailler avec kubectl
Une douzaine de commandes couvrent l'exploitation courante.
kubectl get pods -o wide # with node and IP
kubectl get deploy,svc,cm -n staging
kubectl describe pod shop-api-7c9f-2k4x # spec, status and events
kubectl logs -f deploy/shop-api # follow one Pod of the Deployment
kubectl logs shop-api-7c9f-2k4x --previous # the crashed container's logs
kubectl exec -it shop-api-7c9f-2k4x -- sh
kubectl port-forward svc/shop-api 8080:80 # reach a ClusterIP from your laptop
kubectl rollout status deploy/shop-api
kubectl rollout undo deploy/shop-api
kubectl scale deploy/shop-api --replicas=5
kubectl get events --sort-by=.lastTimestamp # what the cluster has been doing
kubectl top pods # live CPU and memory, needs metrics-server
Quand quelque chose ne va pas, l'ordre qui résout le plus vite est le suivant : get pods pour voir la phase, describe pod pour lire les événements en bas de la sortie, logs --previous si le Pod est en crash loop. La phase nomme généralement le problème directement — ImagePullBackOff (mauvais tag ou identifiants de registre manquants), CrashLoopBackOff (le processus se termine ; lisez les logs), Pending (rien ne peut le planifier ; les événements disent s'il s'agit de ressources, d'une taint ou d'un volume non lié), CreateContainerConfigError (une clé de ConfigMap ou de Secret référencée n'existe pas).
Points clés à retenir
- Le control plane — API server, etcd, scheduler, controller manager — enregistre l'état souhaité et fait tourner les boucles qui font converger la réalité vers lui ; le kubelet, le container runtime et kube-proxy font le travail sur chaque nœud.
- Tout est déclaratif : écrivez la spec, appliquez-la, et lisez le
statuset les événements quand le résultat diffère de ce que vous attendiez. - Un Pod est l'unité de planification et il est jetable ; les Deployments possèdent des ReplicaSets qui possèdent des Pods, ce qui vous donne rollouts, rollback et auto-réparation.
- Les labels et les selectors permettent aux objets de se trouver les uns les autres, et les namespaces partitionnent les noms, les quotas et les accès.
- Les Services fournissent une IP virtuelle et un nom DNS stables devant les Pods prêts :
ClusterIPpour le trafic interne,LoadBalancerpour les points d'entrée cloud, headless pour un adressage stable par Pod. - Les ConfigMaps et les Secrets gardent la configuration hors des images ; les fichiers montés peuvent se mettre à jour à chaud, pas les variables d'environnement, et les Secrets exigent le chiffrement au repos et un RBAC strict.
get,describe,logs --previouseteventsdiagnostiquent presque tout, et la phase du Pod nomme généralement la panne d'emblée.
Sources et lectures complémentaires
Questions fréquentes
- Ai-je besoin de Kubernetes pour mon application ?
- Pas forcément. Kubernetes est rentable quand vous faites tourner de nombreux services, que vous avez besoin d'auto-réparation face aux pannes de machines, que vous déployez souvent sans interruption de service, ou que vous voulez une plateforme uniforme partagée par plusieurs équipes. Pour un ou deux services sur un seul serveur, il ajoute énormément de mécanique et une courbe d'apprentissage réellement raide. Un cluster managé réduit le coût d'exploitation, mais pas le coût conceptuel.
- Pourquoi un Pod plutôt qu'un simple conteneur ?
- Un Pod est un groupe de conteneurs qui partagent un namespace réseau, une adresse IP et éventuellement des volumes, et qui sont toujours placés sur le même nœud. Ce regroupement permet à un conteneur auxiliaire — un collecteur de logs, un proxy, un agent qui renouvelle des identifiants — de tourner à côté de l'application et de la joindre sur localhost. La plupart des Pods contiennent exactement un conteneur applicatif et, parfois, un auxiliaire.
- Quelle différence entre un Deployment et un ReplicaSet ?
- Un ReplicaSet maintient en marche un nombre fixe de Pods identiques. Un Deployment gère les ReplicaSets dans le temps : quand vous modifiez le template de Pod, il crée un nouveau ReplicaSet, le fait monter en charge tout en réduisant l'ancien, et conserve les précédents pour le rollback. Vous créez presque toujours des Deployments en les laissant posséder les ReplicaSets, et c'est pourquoi on écrit rarement un ReplicaSet à la main.
- Les Secrets Kubernetes sont-ils vraiment secrets ?
- Par défaut, ils sont encodés en base64, pas chiffrés, et quiconque peut lire les Secrets d'un namespace peut lire leurs valeurs. Ils valent mieux qu'une variable d'environnement dans un manifeste, car l'accès est contrôlé par RBAC et ils ne sont pas intégrés aux images, mais une installation sérieuse ajoute le chiffrement au repos dans etcd, un RBAC strict et, souvent, un gestionnaire de secrets externe synchronisé dans le cluster.