Anatomie d'un controller & Observe → diff → agit : la boucle en détail
Comprendre la structure interne d'un contrôleur Kubernetes — informer, work queue, fonction Reconcile — et le déroulé précis des trois phases observe → diff → agit, avec YAML et kubectl.
Anatomie d’un controller
L’idée en une phrase
Un contrôleur Kubernetes est un programme structuré en trois composants — un informer qui observe, une work queue qui tamponne, une fonction Reconcile qui agit — dont l’assemblage produit une boucle de réconciliation fiable : l’état désiré (la spec de l’objet surveillé) est comparé à l’état réel (les ressources effectivement présentes), et l’écart est comblé à chaque passage.
Analogie : Considérons un contrôleur aérien dans une tour de contrôle. Un écran radar (l’informer) affiche en continu la position des avions. Chaque écart — un avion trop bas, trop proche d’un autre — est inscrit sur une fiche déposée dans un bac de traitement (la work queue). Le contrôleur traite les fiches une par une, donne les instructions de correction, puis passe à la suivante. Il ne surveille pas chaque avion en permanence lui-même : l’écran le fait. Il ne traite pas tous les écarts d’un coup : le bac les sérialise. Cette séparation des rôles — détecter, mettre en file, traiter — est exactement celle d’un contrôleur Kubernetes.
Points clés
- L’informer maintient un cache local des objets surveillés, alimenté par un watch sur l’API server (comme vu au chapitre 7-8). Lorsqu’un objet est créé, modifié ou supprimé, l’informer invoque un callback (handler) qui enfile la clé de l’objet (
namespace/name) dans la work queue — jamais l’objet entier. - La work queue découple la détection de l’événement et son traitement. Plusieurs événements rapprochés portant sur le même objet sont dédupliqués : la clé n’apparaît qu’une fois, évitant un traitement redondant. La queue gère aussi le réessai en cas d’erreur.
- La fonction Reconcile est le coeur métier. Elle reçoit une clé, relit l’état courant depuis le cache de l’informer (jamais depuis l’API server directement en condition normale), compare avec l’état désiré et effectue les appels API nécessaires pour combler l’écart.
- Un contrôleur ne réagit pas à un événement ponctuel (« un pod a été supprimé ») mais réconcilie un état complet (« il y a 2 pods, il en faut 3 »). Cette approche dite level-triggered (par opposition à edge-triggered, chapitre 13-14) le rend naturellement résilient aux événements manqués.
Exemple concret
Le contrôleur du ReplicaSet (vu au chapitre 9-10) illustre cette anatomie. Son informer surveille les objets ReplicaSet et les pods. Lorsqu’un pod est supprimé, l’informer inscrit la clé du ReplicaSet propriétaire dans la work queue (grâce aux ownerReferences). La fonction Reconcile est invoquée : elle lit le ReplicaSet depuis le cache, compte les pods réels correspondant au selector, constate un écart de 1 et appelle l’API server pour créer un pod. L’informer n’a pas dit « un pod a disparu » — il a simplement signalé que l’état a changé. C’est la fonction Reconcile qui a déterminé la nature et l’ampleur de l’écart.
Les trois composants d’un contrôleur
| Composant | Rôle | Entrée | Sortie |
|---|---|---|---|
| Informer (+ cache) | Observer les changements, maintenir un cache local | Watch stream depuis l’API server | Clé enfilée dans la work queue |
| Work queue | Tamponner, dédupliquer, gérer les réessais | Clés d’objets | Clé défilée vers Reconcile |
| Fonction Reconcile | Comparer désiré vs réel, agir | Clé + lecture du cache | Appels API (create, update, delete) |
Code YAML — l’objet qui déclenche la boucle
# replicaset.yaml — l'état désiré qu'un contrôleur réconcilie
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3 # état désiré : 3 pods
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
# L'informer du contrôleur ReplicaSet surveille cet objet.
# Toute modification de replicas ou du template enfile la clé "default/web"
# dans la work queue, déclenchant un passage de Reconcile.
Code kubectl — observer le contrôleur au travail
# Appliquer l'état désiré
kubectl apply -f replicaset.yaml
# replicaset.apps/web created
# Supprimer un pod : l'informer détecte le changement, Reconcile agit
kubectl delete pod web-abc12
# pod "web-abc12" deleted
# Observer la réconciliation : un nouveau pod apparaît en quelques secondes
kubectl get pods -l app=web -w
# NAME READY STATUS AGE
# web-def34 1/1 Running 2m
# web-ghi56 1/1 Running 2m
# web-jkl78 0/1 Pending 1s ← créé par la fonction Reconcile
# Les events confirment le passage de la boucle
kubectl describe rs web | grep -A3 Events
# Events:
# Type Reason Age Message
# Normal SuccessfulCreate 2s Created pod: web-jkl78
Piège courant : « Le contrôleur reçoit un événement et réagit à cet événement » est une simplification trompeuse. L’événement ne fait qu’enfiler une clé dans la work queue ; la fonction Reconcile, elle, relit l’état complet et réconcilie sur la base d’un écart calculé, non d’un événement. Si deux événements arrivent simultanément pour le même objet, la clé n’est enfilée qu’une fois et un seul passage de Reconcile traite l’état résultant. C’est cette indirection qui rend le contrôleur idempotent et résilient.
Observe → diff → agit : la boucle en détail
L’idée en une phrase
Chaque passage de la fonction Reconcile suit trois phases invariantes : observer l’état réel (lire le cache et, si nécessaire, l’API server), calculer l’écart entre cet état réel et l’état désiré (la spec), puis agir pour combler cet écart — et rien d’autre. Cette discipline garantit qu’un contrôleur produit toujours le même résultat pour le même état d’entrée, indépendamment de l’historique des événements qui l’ont déclenché.
Analogie : Considérons un inventoriste dans un entrepôt. Sa procédure est toujours la même : il compte les articles en rayon (observer), compare avec le bon de commande (diff), puis commande le réassort exact pour les articles manquants (agir). Peu importe si le stock a baissé à cause d’un vol, d’une erreur de livraison ou d’un pic de demande — la procédure est identique. Il ne se demande pas pourquoi il manque des articles : il constate un écart et le comble.
Points clés
- Observer : la fonction Reconcile lit l’objet primaire (celui dont la clé a été enfilée) depuis le cache de l’informer, puis lit les ressources secondaires associées (par exemple, les pods d’un ReplicaSet via le selector). Accéder au cache est une opération locale, sans appel réseau — la latence est négligeable.
- Diff : l’écart se calcule en comparant la spec de l’objet primaire (état désiré) avec les ressources secondaires observées (état réel). Pour un ReplicaSet :
replicasdésiré − nombre de pods réels = écart. Pour un Deployment : le template courant vs le template du ReplicaSet actif. La nature du diff est propre à chaque contrôleur, mais le principe est universel. - Agir : le contrôleur appelle l’API server pour créer, mettre à jour ou supprimer des ressources, strictement dans la limite de l’écart calculé. Si l’écart est nul, il ne fait rien — c’est le cas normal et souhaitable, non une anomalie.
- L’idempotence découle de cette structure : deux passages successifs de Reconcile sur le même état produisent le même résultat (rien au second passage, puisque l’écart a été comblé au premier). Ce n’est pas un bonus mais une propriété requise — un contrôleur non idempotent corromprait l’état du cluster (chapitre 23-24).
Exemple concret
Un contrôleur de ReplicaSet reçoit la clé default/web. Phase 1 (observer) : il lit le ReplicaSet web depuis le cache — replicas: 3, selector app=web. Il liste les pods du cache portant le label app=web : deux pods Running. Phase 2 (diff) : écart = 3 − 2 = 1 pod manquant. Phase 3 (agir) : il appelle POST /api/v1/namespaces/default/pods pour créer un pod conforme au template. Au tick suivant, si la clé est de nouveau enfilée, la phase 1 observe 3 pods, le diff est 0, et la phase 3 ne fait rien. Le contrôleur converge en un tick, puis reste au repos.
Les trois phases et leurs invariants
| Phase | Entrée | Opération | Sortie | Invariant |
|---|---|---|---|---|
| Observer | Clé de l’objet | Lecture du cache (informer) | État réel complet | Pas d’appel API si le cache suffit |
| Diff | Spec (désiré) + état réel | Comparaison | Écart (nature + amplitude) | Déterministe pour un état donné |
| Agir | Écart calculé | Appels API (create/update/delete) | État réel rapproché du désiré | Idempotent : écart 0 → aucune action |
Code YAML — un état qui illustre le diff
# Un ReplicaSet déclare 3 replicas.
# Si l'on supprime manuellement un pod, l'état réel passe à 2.
# La phase « diff » du contrôleur calcule 3 - 2 = 1.
# La phase « agir » crée exactement 1 pod — ni plus, ni moins.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
Code kubectl — tracer les trois phases en live
# Observer : lire l'état désiré et l'état réel
kubectl get rs web -o jsonpath='{.spec.replicas}'
# 3 ← état désiré
kubectl get pods -l app=web --no-headers | wc -l
# 3 ← état réel (écart = 0, rien à faire)
# Provoquer un écart : supprimer un pod
kubectl delete pod web-abc12 --wait=false
# pod "web-abc12" deleted
# Le contrôleur observe → diff → agit en quelques secondes
kubectl get pods -l app=web -w
# NAME READY STATUS AGE
# web-abc12 1/1 Terminating 5m
# web-def34 1/1 Running 5m
# web-ghi56 1/1 Running 5m
# web-xyz99 0/1 Pending 0s ← agir : 1 pod créé (diff = 1)
# web-xyz99 1/1 Running 3s ← convergence atteinte
# Vérifier l'idempotence : forcer une resynchronisation sans écart
kubectl annotate rs web reconcile-test=ok
# La clé est réenfilée, Reconcile s'exécute, observe 3/3, ne fait rien.
kubectl get events --field-selector involvedObject.name=web --sort-by=lastTimestamp
# Aucun événement SuccessfulCreate : l'écart était nul, le contrôleur n'a pas agi.
Piège courant : « Le contrôleur sait ce qui a changé et n’agit que sur le changement » laisse croire qu’il fonctionne par différence incrémentale (edge-triggered). En réalité, chaque passage de Reconcile repart de l’état complet : il ne reçoit qu’une clé, sans information sur la nature de l’événement déclencheur. C’est pourquoi un contrôleur bien écrit produit le bon résultat même si un événement a été perdu ou si la work queue a fusionné plusieurs notifications. La robustesse vient de ce que la boucle ignore l’historique et ne raisonne que sur l’écart présent.