Pods et Services
Les trois ressources que vous écrirez tous les jours.
Pods, Services et Deployments
Un Pod a une adresse IP. Cette adresse change à chaque recréation du Pod, et un Pod est recréé souvent : à chaque déploiement, à chaque plantage, à chaque fois que l’ordonnanceur décide de le déplacer. Aucune application ne peut donc appeler une autre par son adresse. C’est le problème que résout cette leçon, et il est plus profond qu’il n’y paraît.
Les trois ressources qui suivent forment une chaîne dont chaque anneau a une raison d’être précise. Le Pod exécute. Le Deployment garantit qu’il existe le bon nombre de Pods de la bonne version, et sait passer d’une version à l’autre sans coupure. Le Service fournit un nom stable et une adresse stable devant un ensemble de Pods dont l’identité varie en permanence.
Ce qui relie ces trois objets n’est pas une référence directe, et c’est le point qui déroute au début : ils ne se connaissent pas par leur nom, mais par des labels. Un Service ne sait pas quels Pods il expose ; il déclare un critère de sélection, et le système lui associe en continu tous les Pods qui y répondent. Ce couplage indirect est ce qui permet à un Pod de disparaître et à son remplaçant d’être servi immédiatement, sans aucune reconfiguration.
Comprendre ce mécanisme de labels, c’est comprendre 80 % de Kubernetes. Un sélecteur mal écrit produit exactement le symptôme le plus opaque du débutant : un Service qui répond, mais avec une erreur de connexion, parce qu’il ne pointe sur rien.
Le Deployment plutôt que le Pod
On ne crée pas un Pod à la main en dehors d’un test jetable. Un Pod seul n’a aucune résilience : s’il meurt, il reste mort. On déclare un Deployment, qui prend cette responsabilité.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web # doit correspondre aux labels du template ci-dessous
template: # le moule à partir duquel les Pods sont fabriqués
metadata:
labels:
app: web # les labels que porteront les Pods créés
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
Deux pièges dans ce fichier, tous les deux liés au même endroit.
Le premier : spec.selector.matchLabels et spec.template.metadata.labels doivent correspondre. Le selector dit « les Pods dont je suis responsable ont ce label », le template dit « les Pods que je fabrique portent ce label ». S’ils divergent, l’API server rejette le manifeste, ce qui est une bonne nouvelle : l’erreur est détectée à l’écriture.
Le second : spec.selector est immuable après création. Pour le modifier, il faut supprimer et recréer le Deployment. Choisissez donc vos labels avec un peu d’attention.
kubectl apply -f deployment.yaml
kubectl get pods -l app=web -w # observer la montée en charge en direct
kubectl get deployment web # répliques prêtes sur répliques voulues
L’intermédiaire ReplicaSet
Un Deployment ne pilote pas les Pods directement. Il crée un ReplicaSet, et c’est ce ReplicaSet qui maintient le compte de Pods. Cette couche paraît gratuite jusqu’à ce qu’on comprenne à quoi elle sert :
kubectl get replicasets
# NAME DESIRED CURRENT READY AGE
# web-7d4b8c9f5 3 3 3 2m
Chaque version du template produit un ReplicaSet distinct. Lors d’une mise à jour, le Deployment fait décroître l’ancien ReplicaSet et croître le nouveau. L’ancien est conservé à zéro réplique — c’est exactement ce qui rend le retour arrière possible et instantané : la définition précédente est encore là, il suffit de la remonter.
Vérifiez la hiérarchie : supprimez un Pod à la main, il est recréé en quelques secondes par le ReplicaSet, qui a constaté que le compte ne correspondait plus.
kubectl delete pod web-7d4b8c9f5-xk2mp
kubectl get pods -l app=web # un nouveau Pod, avec un nouveau suffixe
Les labels et les sélecteurs
Un label est une paire clé-valeur libre, apposée sur n’importe quel objet. Ce n’est pas de la documentation : c’est le mécanisme de liaison du système.
metadata:
labels:
app: web
tier: frontend
version: v1
environnement: production
kubectl sait interroger sur ces labels, ce qui remplace avantageusement une lecture manuelle de listes :
kubectl get pods -l app=web # égalité
kubectl get pods -l app=web,tier=frontend # conjonction
kubectl get pods -l 'app!=web' # négation
kubectl get pods -l 'environnement in (staging,production)' # ensemble
kubectl get pods -l version # présence de la clé
kubectl get pods --show-labels # afficher les labels
Le conseil qui évite le plus de désordre : réservez les labels du sélecteur à l’identité de l’application (app, tier) et mettez tout ce qui varie souvent — un numéro de version, un identifiant de commit — dans des labels hors sélecteur ou dans des annotations. Un sélecteur qui contient version: v1 casse la continuité du Service à chaque livraison.
Le Service : une adresse qui ne bouge pas
Un Service crée une adresse IP virtuelle et un nom DNS stables, et répartit le trafic vers tous les Pods qui correspondent à son sélecteur.
ClusterIP, le cas par défaut
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # valeur par défaut : joignable depuis l'intérieur du cluster
selector:
app: web # tous les Pods portant app=web deviennent des cibles
ports:
- name: http
port: 80 # port du Service
targetPort: 80 # port écouté dans le conteneur
protocol: TCP
La distinction port / targetPort est constante et vaut d’être fixée : port est ce que les clients appellent, targetPort est ce que le conteneur écoute. Rien n’oblige à ce qu’ils soient égaux, et ils le sont rarement en pratique.
Le Service est joignable par son nom depuis n’importe quel Pod du cluster, grâce au DNS interne :
web # même namespace
web.atelier # namespace explicite
web.atelier.svc.cluster.local # nom complet
Vérifiez-le depuis un Pod éphémère, c’est le test qui lève le doute le plus vite :
kubectl run debug --image=busybox:1.36 --rm -it --restart=Never -- \
wget -qO- http://web:80
Le diagnostic essentiel : les endpoints
Un Service qui ne trouve aucun Pod ne renvoie pas d’erreur explicite. Il existe, il a une adresse, et il refuse simplement les connexions. La cause est presque toujours un sélecteur qui ne correspond à rien.
kubectl get endpoints web # les Pods réellement derrière le Service
# Liste vide ? Le sélecteur ne correspond à aucun Pod. Comparez les deux :
kubectl get service web -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels
Retenez ce réflexe : connexion refusée sur un Service, on regarde les endpoints avant tout le reste. Cela transforme une demi-heure de tâtonnement en trente secondes.
NodePort
NodePort ouvre le même port sur tous les nœuds du cluster, dans la plage 30000-32767.
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080 # facultatif ; sinon Kubernetes en attribue un
# minikube donne l'URL directement joignable
minikube service web-nodeport --url
C’est pratique en développement et à éviter en production : le port est non standard, il n’y a ni TLS ni nom de domaine, et l’exposition porte sur tous les nœuds à la fois. Pour du trafic HTTP réel, la bonne réponse est un Ingress, vu en leçon 6.
LoadBalancer, et sa dépendance au fournisseur
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
Soyons précis, parce que ce point trompe beaucoup de monde : Kubernetes ne sait pas créer d’équilibreur de charge. Il émet une demande, et attend qu’un contrôleur externe la satisfasse. Sur AWS, Google Cloud ou Azure, ce contrôleur existe et provisionne une ressource facturée. Sur un cluster local ou une installation sans intégration cloud, personne ne répond, et le Service reste indéfiniment avec une adresse externe <pending>.
kubectl get service web-lb
# EXTERNAL-IP reste <pending> sans contrôleur : ce n'est pas un bug
# Avec minikube, une commande simule le comportement (à laisser tourner)
minikube tunnel
| Type | Portée | Usage réaliste |
|---|---|---|
ClusterIP | interne au cluster | communication entre services : le cas courant |
NodePort | port ouvert sur chaque nœud | développement, accès de dépannage |
LoadBalancer | adresse externe fournie par le cloud | point d’entrée public, dépend du fournisseur |
Les mises à jour progressives
C’est le service que Kubernetes rend le mieux, et il est presque gratuit. Changez l’image d’un Deployment : les Pods sont remplacés par lots, en maintenant le service disponible pendant toute la transition.
# Fragment à insérer dans le spec du Deployment, à côté de replicas.
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # au plus 1 Pod indisponible pendant la bascule
maxSurge: 1 # au plus 1 Pod en plus des 4 voulues
Avec ces valeurs, il y a toujours entre 3 et 5 Pods pendant la transition, dont au moins 3 disponibles. maxUnavailable: 0 garantit zéro perte de capacité, au prix d’un déploiement plus lent puisqu’il faut d’abord créer avant de détruire.
kubectl apply -f deployment.yaml # après modification de l'image du manifeste
# Variante impérative, utile pour un correctif urgent
kubectl set image deployment/web nginx=nginx:1.27.1-alpine
kubectl rollout status deployment/web # se termine quand la bascule est finie
Le retour arrière
Puisque les anciens ReplicaSets sont conservés, revenir en arrière ne demande aucune reconstruction :
kubectl rollout history deployment/web # historique des révisions
kubectl rollout undo deployment/web # revenir à la précédente
kubectl rollout undo deployment/web --to-revision=2 # à une révision précise
# Suspendre puis reprendre une bascule en cours, si les erreurs augmentent
kubectl rollout pause deployment/web
kubectl rollout resume deployment/web
Une remarque honnête sur l’historique : par défaut il ne montre pas ce qui a changé entre deux révisions, seulement leur numéro. Pour rendre l’historique lisible, ajoutez une annotation à chaque livraison :
metadata:
name: web
annotations:
kubernetes.io/change-cause: "Passage à nginx 1.27.1 — correctif CVE"
La mise à l’échelle
kubectl scale deployment/web --replicas=6 # impératif
kubectl apply -f deployment.yaml # déclaratif, après édition du fichier
Préférez le déclaratif. Un kubectl scale est écrasé au prochain apply du manifeste, ce qui produit une régression silencieuse et difficile à expliquer.
Les sondes : liveness et readiness
Sans sonde, Kubernetes considère un conteneur comme sain dès que son processus principal tourne. C’est une approximation grossière : une application peut avoir démarré son serveur HTTP sans être capable de répondre — connexion à la base non établie, cache non chargé. Le Service enverra alors du trafic vers un Pod qui répondra par des erreurs.
Deux sondes répondent à deux questions distinctes, et les confondre a des conséquences sérieuses.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: mon-api:1.0.0
ports:
- containerPort: 8080
# « Ce Pod peut-il recevoir du trafic maintenant ? »
# En échec : retiré des endpoints du Service, mais NON redémarré.
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
# « Ce processus est-il définitivement bloqué ? »
# En échec : le conteneur est TUÉ et redémarré.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
# « Le démarrage est-il terminé ? »
# Tant qu'elle échoue, la livenessProbe est suspendue.
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30
Les erreurs classiques, à connaître avant de les commettre :
- Faire pointer la liveness sur un endpoint qui vérifie la base de données. Si la base ralentit, tous les Pods échouent la sonde, sont tués en même temps, et une lenteur passagère devient une panne totale. La liveness ne doit tester que le processus lui-même ; les dépendances externes relèvent de la readiness.
- Un
initialDelaySecondstrop court sur la liveness. L’application est tuée avant d’avoir fini de démarrer, en boucle : c’est leCrashLoopBackOffinexplicable. LastartupProbeest la réponse propre pour les démarrages lents. - Ne déclarer aucune readiness. Le trafic arrive sur un Pod qui n’est pas prêt, et chaque déploiement provoque une vague d’erreurs 502.
Trois formes de sondes existent, selon ce que l’application expose :
# HTTP : tout code de 200 à 399 est un succès
readinessProbe:
httpGet: { path: /ready, port: 8080 }
---
# TCP : la connexion s'ouvre-t-elle ?
readinessProbe:
tcpSocket: { port: 5432 }
---
# Commande : code de sortie 0 = succès
livenessProbe:
exec: { command: ["sh", "-c", "test -f /tmp/sain"] }
Diagnostiquer un Pod
Les commandes à connaître par cœur, dans l’ordre où on les utilise :
# 1. Vue d'ensemble : STATUS et RESTARTS racontent l'essentiel
kubectl get pods -o wide
# 2. La section Events en fin de sortie donne la cause réelle
kubectl describe pod web-7d4b8c9f5-xk2mp
# 3. Les logs applicatifs
kubectl logs web-7d4b8c9f5-xk2mp
kubectl logs -f deployment/web # suivi en direct
kubectl logs web-7d4b8c9f5-xk2mp --previous # logs du conteneur AVANT son crash
# 4. Entrer dans le conteneur
kubectl exec -it web-7d4b8c9f5-xk2mp -- sh
# 5. Atteindre le Pod depuis le poste, sans l'exposer
kubectl port-forward pod/web-7d4b8c9f5-xk2mp 8080:80
L’option --previous est celle qu’on découvre trop tard. Sur un Pod en CrashLoopBackOff, kubectl logs montre le conteneur courant, souvent vide parce qu’il vient de redémarrer. --previous montre l’instance qui a réellement planté, et donc le message d’erreur.
Les statuts les plus fréquents et leur signification :
| Statut | Cause habituelle |
|---|---|
Pending | l’ordonnanceur ne trouve pas de nœud : ressources insuffisantes ou volume non lié |
ImagePullBackOff | image introuvable, tag erroné, ou registre privé sans identifiants |
CrashLoopBackOff | le conteneur démarre puis s’arrête en boucle : voir logs --previous |
Running mais 0/1 READY | la readinessProbe échoue |
OOMKilled | dépassement de la limite mémoire (voir leçon 6) |
À retenir
- On déclare un Deployment, jamais un Pod à la main. Il crée un ReplicaSet par version du template, et c’est la conservation des anciens ReplicaSets qui rend
kubectl rollout undoinstantané. - Les objets se lient par labels, pas par noms. Gardez les labels du sélecteur stables (
app,tier) et sortez tout ce qui varie souvent — versions, commits — du sélecteur, qui est de toute façon immuable. - Un Service injoignable se diagnostique par
kubectl get endpoints: une liste vide signifie un sélecteur qui ne correspond à aucun Pod. ClusterIPpour l’interne,NodePortpour du développement,LoadBalanceruniquement là où un contrôleur cloud existe — sinon l’adresse externe reste<pending>indéfiniment.- readiness retire du trafic sans redémarrer, liveness tue et redémarre. Ne mettez jamais de dépendance externe dans une liveness : une lenteur de base de données deviendrait une panne générale.
Conseil : lancez
kubectl get pods -wdans un terminal, puis déclenchez une mise à jour dans un autre. Voir les Pods apparaître et disparaître par lots ancre le mécanisme mieux que n’importe quelle explication.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion