Intermédiaire Chapitre 17-18 / 10

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.selector dé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:port des 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

ComposantRôle dans la réconciliationCe qu’il surveille
ServiceDéclare l’état désiré du routage (sélecteur + port)— (objet déclaratif, lu par les contrôleurs)
Contrôleur EndpointSliceRéconcilie la liste des endpoints avec les pods réelsServices et Pods (via informers)
kube-proxyRéconcilie les règles réseau du nœud avec les EndpointSlicesEndpointSlices (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 nodeSelector et les tolerations. 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.completions pods 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 champ spec.parallelism contrô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
DeploymentN replicas identiquesNombre de pods RunningInterchangeables (hash aléatoire)
DaemonSet1 pod par nœud qualifiéCouverture des nœudsUn pod lié à chaque nœud
StatefulSetN pods ordonnés avec stockageIdentité ordinale et PVCStable (pod-0, pod-1, …)
JobN completions réussiesNombre 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.


Quiz — validation des acquis
Intermédiaire 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Relire la fiche memo ci-dessus en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.