Services et EndpointSlices : réconcilier le réseau & DaemonSet, StatefulSet, Job : des réconciliateurs spécialisés
Comprendre comment les Services et EndpointSlices réconclient le routage réseau, et comment DaemonSet, StatefulSet et Job adaptent la boucle de contrôle à des besoins spécialisés.
Services et EndpointSlices : réconcilier le réseau
L’idée en une phrase
Un Service déclare un point d’accès réseau stable (une ClusterIP virtuelle) vers un ensemble de pods sélectionnés par labels ; le contrôleur d’EndpointSlices réconcilie en permanence la liste des adresses IP réelles des pods sains avec cette déclaration — comblant l’écart entre l’état désiré (« le trafic doit atteindre les pods correspondant au sélecteur ») et l’état réel (les pods effectivement prêts à recevoir du trafic).
Analogie : Considérons le standard téléphonique d’une entreprise. Le numéro public (le Service) ne change jamais, mais la liste interne des postes disponibles (les EndpointSlices) évolue constamment : un employé arrive, son poste est ajouté ; un autre part en pause, son poste est temporairement retiré. Le standardiste (kube-proxy) consulte cette liste à chaque appel entrant et transfère vers un poste actif. Si tous les employés changent de bureau simultanément, le numéro public reste identique — seule la liste interne est réconciliée.
Points clés
- Un Service de type ClusterIP reçoit une adresse IP virtuelle stable, attribuée à sa création et immuable. Le champ
spec.selectordéfinit quels pods sont ciblés. Le Service lui-même ne route rien : il est une déclaration d’intention réseau, l’état désiré du routage. - Le contrôleur EndpointSlice (dans le kube-controller-manager) surveille les Services et les Pods via des informers (chapitre 13-14). Pour chaque Service, il maintient un ou plusieurs objets EndpointSlice qui listent les adresses
IP:portdes pods correspondant au sélecteur et dont la readiness probe est positive. Chaque EndpointSlice contient au maximum 100 endpoints par défaut, ce qui résout les problèmes de scalabilité de l’ancien objet Endpoints (limité à un seul objet par Service). - kube-proxy, présent sur chaque nœud (déployé en DaemonSet — sujet de la seconde partie de ce chapitre), surveille les EndpointSlices et programme les règles de routage correspondantes — iptables, IPVS ou nftables selon la configuration. Lorsqu’un EndpointSlice est mis à jour, kube-proxy réconcilie les règles locales du nœud avec la nouvelle liste d’endpoints.
- Lorsqu’un pod est supprimé ou échoue sa readiness probe, le contrôleur EndpointSlice retire son adresse de l’EndpointSlice correspondant. kube-proxy supprime la règle associée. Le trafic cesse d’être dirigé vers ce pod — sans reconfiguration manuelle. C’est la réconciliation réseau en action.
Exemple concret
Un Deployment api déclare 3 replicas. Un Service api-svc sélectionne les pods portant le label app: api. À t₀, les trois pods sont Running et passent leur readiness probe — le contrôleur EndpointSlice crée un objet listant les trois adresses (10.244.1.5:8080, 10.244.2.3:8080, 10.244.3.7:8080). kube-proxy programme trois règles iptables sur chaque nœud. À t₁, le pod 10.244.2.3 échoue sa readiness probe. Le contrôleur EndpointSlice détecte le changement via l’informer, retire l’adresse de l’EndpointSlice. kube-proxy observe la mise à jour et supprime la règle correspondante. Le trafic est désormais réparti entre les deux pods restants. À t₂, le pod redevient ready — le contrôleur réajoute son adresse, kube-proxy reprogramme la règle. L’écart entre l’état désiré (3 endpoints) et l’état réel (2 endpoints) a été comblé sans intervention humaine.
Service, EndpointSlice et kube-proxy
| Composant | Rôle dans la réconciliation | Ce qu’il surveille |
|---|---|---|
| Service | Déclare l’état désiré du routage (sélecteur + port) | — (objet déclaratif, lu par les contrôleurs) |
| Contrôleur EndpointSlice | Réconcilie la liste des endpoints avec les pods réels | Services et Pods (via informers) |
| kube-proxy | Réconcilie les règles réseau du nœud avec les EndpointSlices | EndpointSlices (via informer) |
Code YAML — Service et Deployment associé
# api-deployment.yaml — 3 replicas avec le label app: api
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api # le Service cible ce label
spec:
containers:
- name: api
image: myregistry/api:2.1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5 # vérifié toutes les 5 s
---
# api-svc.yaml — l'ÉTAT DÉSIRÉ du routage réseau
apiVersion: v1
kind: Service
metadata:
name: api-svc
spec:
type: ClusterIP
selector:
app: api # cible les pods avec ce label
ports:
- port: 80 # port exposé par le Service
targetPort: 8080 # port du conteneur
# Le contrôleur EndpointSlice crée automatiquement un
# EndpointSlice listant les IP:8080 des pods app=api ready.
Code kubectl — observer la réconciliation réseau
# Vérifier le Service et son ClusterIP stable
kubectl get svc api-svc
# NAME TYPE CLUSTER-IP PORT(S) AGE
# api-svc ClusterIP 10.96.42.17 80/TCP 5m
# Observer les EndpointSlices réconciliés par le contrôleur
kubectl get endpointslices -l kubernetes.io/service-name=api-svc
# NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# api-svc-abc12 IPv4 8080 10.244.1.5,10.244.2.3,10.244.3.7 5m
# Simuler la perte d'un pod — le contrôleur retire l'endpoint
kubectl delete pod api-xyz42 --grace-period=0
kubectl get endpointslices -l kubernetes.io/service-name=api-svc
# ENDPOINTS: 10.244.1.5,10.244.3.7 ← 2 endpoints, le troisième retiré
# Le Deployment recrée le pod, le contrôleur réajoute l'endpoint
kubectl get endpointslices -l kubernetes.io/service-name=api-svc
# ENDPOINTS: 10.244.1.5,10.244.3.7,10.244.4.2 ← nouveau pod, 3 endpoints
Piège courant : « Un Service route le trafic directement vers les pods » est un raccourci trompeur. Le Service est un objet déclaratif — il ne route rien par lui-même. C’est la chaîne de réconciliation Service → contrôleur EndpointSlice → EndpointSlice → kube-proxy → règles iptables/IPVS qui assure le routage effectif. Comprendre cette chaîne est essentiel pour diagnostiquer un problème de connectivité : si un pod est accessible directement par IP mais pas via le Service, l’écart se situe dans l’un de ces maillons, pas dans le pod lui-même.
DaemonSet, StatefulSet, Job : des réconciliateurs spécialisés
L’idée en une phrase
Le Deployment réconcilie un nombre de replicas (chapitre 9-10), mais certains besoins exigent des réconciliateurs spécialisés : le DaemonSet garantit exactement un pod par nœud qualifié, le StatefulSet maintient des pods à identité stable et stockage persistant, et le Job converge vers un nombre de terminaisons réussies — chacun implémente sa propre variante de la boucle observe → diff → agit.
Analogie : Considérons trois contrats de gestion d’un immeuble. Le premier (DaemonSet) installe un détecteur de fumée à chaque étage — si un étage est ajouté, un détecteur est posé ; si un étage est condamné, le détecteur est retiré. Le deuxième (StatefulSet) attribue des bureaux numérotés à des locataires, chacun avec un coffre-fort personnel — le bureau 0 existe avant le bureau 1, et si le locataire du bureau 2 quitte l’immeuble, son coffre-fort reste en place pour le suivant. Le troisième (Job) engage une équipe pour repeindre 5 pièces — une fois les 5 pièces terminées, le contrat est clos et l’équipe quitte l’immeuble.
Points clés
- Le DaemonSet controller surveille les nœuds et les pods. Son état désiré : un pod sur chaque nœud qui satisfait le
nodeSelectoret lestolerations. Lorsqu’un nœud rejoint le cluster, le contrôleur crée un pod ; lorsqu’un nœud est retiré, le pod est nettoyé par le garbage collector (chapitre 25-26). kube-proxy est l’exemple canonique de DaemonSet : il doit tourner sur chaque nœud pour réconcilier les règles réseau locales. - Le StatefulSet controller maintient N pods avec des identités ordinales stables (
pod-0,pod-1, …pod-N-1). Chaque pod est lié à un PersistentVolumeClaim dédié qui survit à la suppression du pod. La réconciliation est ordonnée : les pods sont créés de 0 à N-1 et supprimés de N-1 à 0 (politique OrderedReady par défaut). Cette garantie d’ordre est essentielle pour les systèmes distribués à état (bases de données, brokers de messages) où l’initialisation et l’arrêt doivent suivre un protocole séquentiel. - Le Job controller réconcilie un nombre de completions réussies. Son état désiré :
spec.completionspods terminés avec succès (exit code 0). Il crée des pods, suit leur terminaison, et s’arrête lorsque le compte est atteint. Le champspec.parallelismcontrôle combien de pods tournent simultanément. Un CronJob ajoute une dimension temporelle : son état désiré est « ce Job doit exister à chaque occurrence du schedule cron ». - Ces trois contrôleurs utilisent les mêmes mécanismes internes que le Deployment controller : informers, work queue (chapitre 15-16), rate limiter. Ce qui diffère, c’est la sémantique de l’écart : couverture des nœuds (DaemonSet), identité ordinale et stockage (StatefulSet), nombre de terminaisons réussies (Job).
Exemple concret
Un cluster de 4 nœuds exécute un DaemonSet log-collector. À t₀, 4 pods tournent (un par nœud). Un cinquième nœud est ajouté au cluster. Le DaemonSet controller observe l’écart : 5 nœuds qualifiés, 4 pods. Il crée un pod log-collector-xxxxx sur le nouveau nœud. Parallèlement, un StatefulSet postgres déclare 3 replicas. Les pods postgres-0, postgres-1, postgres-2 existent, chacun lié à son PVC (data-postgres-0, etc.). Le pod postgres-1 crashe — le contrôleur le recrée avec le même nom postgres-1, rattaché au même PVC data-postgres-1. Les données persistent. Enfin, un Job migration déclare 1 completion. Le contrôleur crée un pod ; celui-ci termine avec exit code 0. Le Job passe en status Complete et ne crée plus de pod. Si le pod avait échoué, le contrôleur en aurait créé un nouveau (dans la limite de backoffLimit).
Comparaison des réconciliateurs spécialisés
| Contrôleur | État désiré | Écart réconcilié | Identité des pods |
|---|---|---|---|
| Deployment | N replicas identiques | Nombre de pods Running | Interchangeables (hash aléatoire) |
| DaemonSet | 1 pod par nœud qualifié | Couverture des nœuds | Un pod lié à chaque nœud |
| StatefulSet | N pods ordonnés avec stockage | Identité ordinale et PVC | Stable (pod-0, pod-1, …) |
| Job | N completions réussies | Nombre de terminaisons exit 0 | Éphémères (supprimés après succès) |
Code YAML — DaemonSet, StatefulSet et Job
# log-collector-ds.yaml — un pod sur CHAQUE nœud
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
containers:
- name: collector
image: fluent/fluent-bit:3.0
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
hostPath:
path: /var/log # accès aux logs du nœud
# État désiré : autant de pods que de nœuds qualifiés.
# Ajout d'un nœud → le contrôleur crée un pod automatiquement.
---
# postgres-sts.yaml — pods ordonnés avec stockage persistant
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres # headless Service pour DNS stable
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data # chaque pod reçoit data-postgres-{i}
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
# Création ordonnée : postgres-0 Ready avant postgres-1.
# PVC data-postgres-0 survit à la suppression de postgres-0.
---
# migration-job.yaml — une tâche à accomplir une fois
apiVersion: batch/v1
kind: Job
metadata:
name: migration
spec:
completions: 1 # état désiré : 1 succès
backoffLimit: 3 # max 3 tentatives sur échec
template:
spec:
restartPolicy: Never # obligatoire pour un Job
containers:
- name: migrate
image: myregistry/db-migrate:1.4
command: ["./migrate", "--target", "v42"]
# Le contrôleur crée un pod. Exit 0 → Job Complete.
# Exit non-0 → nouveau pod, jusqu'à backoffLimit.
Code kubectl — observer les réconciliateurs spécialisés
# DaemonSet : vérifier la couverture des nœuds
kubectl get ds log-collector
# NAME DESIRED CURRENT READY NODE-SELECTOR AGE
# log-collector 4 4 4 <none> 10m
# DESIRED = nombre de nœuds qualifiés (l'état désiré)
# CURRENT/READY = pods effectifs (l'état réel)
# Ajouter un nœud → observer la réconciliation
kubectl get ds log-collector
# DESIRED: 5 CURRENT: 5 READY: 5 ← pod créé automatiquement
# StatefulSet : observer l'ordre de création
kubectl get pods -l app=postgres -w
# postgres-0 0/1 Pending 0 0s
# postgres-0 1/1 Running 0 5s ← 0 ready avant 1
# postgres-1 0/1 Pending 0 0s
# postgres-1 1/1 Running 0 4s
# postgres-2 0/1 Pending 0 0s
# Vérifier les PVC persistants
kubectl get pvc -l app=postgres
# NAME STATUS VOLUME CAPACITY AGE
# data-postgres-0 Bound pv-abc 10Gi 10m
# data-postgres-1 Bound pv-def 10Gi 10m
# data-postgres-2 Bound pv-ghi 10Gi 10m
# Job : suivre la progression
kubectl get job migration
# NAME COMPLETIONS DURATION AGE
# migration 0/1 5s 5s
kubectl get job migration
# NAME COMPLETIONS DURATION AGE
# migration 1/1 12s 20s ← terminé avec succès
Piège courant : « Supprimer un pod de StatefulSet perd ses données, comme pour un Deployment » est une confusion fréquente. Contrairement aux pods de Deployment, un pod de StatefulSet est lié à un PersistentVolumeClaim nominatif (
data-postgres-0,data-postgres-1, etc.) qui n’est PAS supprimé avec le pod. Le contrôleur recrée le pod avec le même nom ordinal et le rattache au même PVC — les données persistent. C’est précisément la différence de sémantique de réconciliation : le Deployment réconcilie un compteur anonyme, le StatefulSet réconcilie des identités stables avec leur stockage.