Bonus Chapitre 43-44 / 24

Remplacer le broker : l'API server comme bus de messages & CRDs comme messages durables

Comprendre comment l'API server de Kubernetes et les Custom Resources rejouent le rôle d'un bus de messages et de messages durables — état désiré vs état réel, avec du YAML, kubectl et du C#/KubeOps.

Remplacer le broker : l’API server comme bus de messages

L’idée en une phrase

L’API server de Kubernetes, adossé à etcd et à son mécanisme de watch, remplit les fonctions structurantes d’un bus de messages — réception, stockage durable, diffusion à des abonnés — mais selon un modèle level-based : les consommateurs ne réagissent pas à des messages éphémères, ils réconcilient l’état désiré publié vers l’état réel observé, la boucle observe → diff → agit tenant lieu de traitement de message.

Analogie : Considérons un tableau d’affichage municipal plutôt qu’une distribution de courrier. Un bus de messages classique agit comme un facteur : il remet chaque lettre une fois, puis la lettre disparaît. L’API server agit comme un tableau public : on y épingle l’état voulu, l’affiche reste visible tant qu’on ne la retire pas, et quiconque passe peut lire l’état courant complet à tout moment. Un lecteur absent hier lit aujourd’hui l’affiche encore en place ; il n’a rien manqué, car l’information est un état persistant et non un événement fugace.

Points clés

  • Un bus de messages classique (RabbitMQ, Kafka) assure trois fonctions : accepter des messages d’un producteur, les conserver, et les diffuser à des consommateurs abonnés. L’API server assure les mêmes fonctions : kubectl apply publie, etcd conserve, le watch diffuse aux contrôleurs. Le vocabulaire diffère, les responsabilités se recouvrent.
  • La différence tient au modèle de livraison. Un broker est edge-based : il transporte des transitions (« commande créée ») que le consommateur traite une fois. L’API server est level-based : il expose l’état courant complet (« il doit exister 3 replicas »), relu à chaque cycle. Cette opposition, posée au chapitre day 13-14, est la clé de la robustesse.
  • Le watch n’est pas une file de messages à consommer mais un signal de réveil. Une notification perdue ne fait pas perdre d’information : le resync périodique (chapitre day 15-16) relit l’état complet et rattrape tout écart. L’événement déclenche la boucle plus tôt ; il ne porte jamais la donnée à lui seul.
  • La durabilité est déléguée à etcd (chapitre day 3-4). Là où un broker peut acquitter puis oublier un message, l’API server conserve l’objet tant qu’il n’est pas explicitement supprimé. L’état désiré survit au redémarrage de tous les consommateurs.
  • Ce modèle dispense de l’acquittement et du dead-letter. Un consommateur qui échoue ne « perd » pas le message : au cycle suivant, il relit le même état désiré et retente. La reprise sur erreur est structurelle, non ajoutée par-dessus.

Exemple concret

Une équipe veut déclencher une sauvegarde nocturne. Avec un broker, un producteur émet un message RunBackup à 2 h ; si le consommateur est en panne à cet instant et que le message expire, la sauvegarde est perdue. Avec l’API server, on publie un objet déclarant schedule: "0 2 * * *" : l’état désiré reste inscrit dans etcd. Si le contrôleur redémarre à 2 h 03, il relit l’objet au démarrage, observe qu’aucune sauvegarde n’a eu lieu cette nuit (état réel), calcule l’écart et la lance. Aucun message n’a été « perdu », car il n’y avait pas de message éphémère à perdre : seulement un état persistant à réconcilier.

Tableau — broker classique vs API server

FonctionBroker de messages (Kafka/RabbitMQ)API server Kubernetes
Unité transportéeMessage / événement (edge)État désiré complet (level)
PersistanceRétention configurable, puis purgeetcd, jusqu’à suppression explicite
DiffusionPush vers abonnés (consume)Watch = signal + relecture d’état
Message manquéPerdu ou en dead-letterRattrapé par le resync périodique
Reprise sur erreurAck / nack / retry à outillerStructurelle : relire l’état et retenter

Code YAML — publier un état sur le « bus »

# Publier sur l'API server, ce n'est pas émettre un événement,
# c'est inscrire un ÉTAT DÉSIRÉ durable que des consommateurs réconcilieront.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"   # état DÉSIRÉ : la donnée reste, elle n'expire pas
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: acme/backup:1.4

Code kubectl — s’abonner au « bus » via un watch

# Publier l'état désiré (équivalent d'un "publish" sur le bus)
kubectl apply -f nightly-backup.yaml
# cronjob.batch/nightly-backup created

# S'abonner : le watch diffuse les changements d'état aux consommateurs.
# Ce n'est pas une file à vider, c'est un signal de relecture d'état.
kubectl get cronjob nightly-backup --watch
# NAME             SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
# nightly-backup   0 2 * * *     False     0        <none>          3s

Piège courant : « Le watch est une file de messages qu’il faut consommer sans en perdre un seul » est inexact. Le watch ne garantit pas la livraison de chaque transition ; il peut sauter des événements, et c’est sans conséquence. La garantie ne porte pas sur les messages mais sur la convergence : le consommateur relit l’état complet à chaque cycle et au resync, si bien qu’une notification manquée ne fait perdre aucune information. Concevoir un contrôleur en supposant qu’il reçoit tous les événements, dans l’ordre et une seule fois, reproduit les fragilités d’un broker — précisément ce que le modèle level-based évite.


CRDs comme messages durables

L’idée en une phrase

Une Custom Resource (instance de CRD, chapitre day 19-20) peut jouer le rôle d’un message durable : au lieu d’émettre un événement volatil, on crée un objet persistant dont la spec porte l’intention (l’état désiré) et le status trace l’avancement du traitement (l’état réel), un reconciler faisant office de consommateur idempotent qui referme l’écart entre les deux.

Analogie : Considérons un ticket accroché à un tableau de service plutôt qu’un ordre crié à la cantonade. Un ordre lancé oralement est perdu si personne n’écoute au bon moment. Un ticket épinglé reste lisible : il indique ce qui est demandé, on y note l’état d’avancement (« pris en charge », « terminé »), et si l’opérateur change en cours de route, le suivant reprend le ticket là où il en était. Le ticket est le message ; le tableau est le stockage durable ; la note d’avancement est le status.

Points clés

  • Une Custom Resource réifie un message en donnée persistante. Sa spec est la charge utile (l’intention), immuable du point de vue du consommateur ; son status est l’accusé de traitement, écrit par le reconciler. Cette dualité spec/status, vue au chapitre day 5-6, est exactement celle d’un message enrichi de son état de traitement.
  • Le traitement est idempotent par construction. Un consommateur de broker doit se prémunir contre les doublons de livraison ; un reconciler relit la même Custom Resource à chaque cycle et doit converger sans effet cumulatif (règle d’or du chapitre day 23-24). L’idempotence n’est pas une précaution, c’est le mode de fonctionnement normal.
  • Le champ observedGeneration distingue « pas encore traité » de « traité ». En comparant metadata.generation (incrémenté à chaque changement de spec) à status.observedGeneration, le reconciler sait si le message courant a déjà été réconcilié — un mécanisme de suivi de progression intégré, sans offset externe à gérer comme dans Kafka.
  • La suppression d’un message durable se gère par finalizer (chapitre day 25-26). Tant qu’un finalizer est présent, l’objet n’est pas réellement effacé : le consommateur peut effectuer un nettoyage (relâcher une ressource externe) avant de retirer le finalizer, garantissant qu’aucun message n’est « acquitté » avant traitement complet.
  • Ce modèle a un coût. etcd n’est pas dimensionné pour un très fort débit de petits messages transitoires : convertir chaque événement à haute fréquence en Custom Resource sature l’API server. Le pattern convient aux intentions durables et peu nombreuses, non à un flux massif d’événements éphémères.

Exemple concret

On modélise une demande de provisionnement comme une Custom Resource ProvisionRequest. Un utilisateur crée l’objet avec spec.size: large : le message est publié et persistant. Le reconciler l’observe, lit status.phase (vide), provisionne la ressource, puis écrit status.phase: Ready et status.observedGeneration: 1. Si le reconciler tombe juste après le provisionnement mais avant d’écrire le status, il redémarre, relit l’objet, retrouve phase vide, et retente : grâce à l’idempotence, il ne provisionne pas deux fois (il vérifie d’abord l’existence de la ressource). Si l’utilisateur modifie ensuite spec.size: xlarge, metadata.generation passe à 2 ; le reconciler constate observedGeneration (1) < generation (2), détecte donc un « nouveau message » et réconcilie de nouveau.

Tableau — message de broker vs Custom Resource

AspectMessage de brokerCustom Resource « message durable »
SupportFile / topicObjet dans etcd
Charge utileCorps du messagespec
Suivi de traitementOffset / ack externestatus + observedGeneration
Contre les doublonsDéduplication à outillerIdempotence du reconciler
Fin de vieAck puis purgeFinalizer puis suppression
Débit adaptéÉlevé, événements courtsFaible, intentions durables

Code YAML — un message durable sous forme de Custom Resource

# La Custom Resource EST le message : la spec porte l'intention (durable),
# le status trace le traitement. Rien n'expire tant qu'on ne supprime pas l'objet.
apiVersion: acme.io/v1
kind: ProvisionRequest
metadata:
  name: req-4821
  generation: 2               # incrémenté à chaque changement de spec
  finalizers:
    - acme.io/cleanup         # empêche l'effacement avant nettoyage
spec:
  size: xlarge                # charge utile : l'intention
  region: eu-west
status:
  phase: Ready                # accusé de traitement (état réel)
  observedGeneration: 1       # 1 < 2 -> un nouveau "message" reste à traiter

Code C# / KubeOps — un reconciler comme consommateur idempotent

// La Custom Resource est le message durable ; ce reconciler est son consommateur.
// Il est rejoué à chaque cycle : il doit converger sans traiter deux fois.
public class ProvisionRequestController : IEntityController<V1ProvisionRequest>
{
    private readonly IProvisioner _provisioner;

    public ProvisionRequestController(IProvisioner provisioner)
        => _provisioner = provisioner;

    public async Task ReconcileAsync(V1ProvisionRequest msg, CancellationToken token)
    {
        // Déjà traité pour cette version de la spec ? -> ne rien refaire (idempotence)
        if (msg.Status.ObservedGeneration == msg.Generation())
            return;

        // OBSERVER l'état réel, puis AGIR de façon idempotente :
        // on ne provisionne que si la ressource n'existe pas déjà.
        if (!await _provisioner.ExistsAsync(msg.Name(), token))
            await _provisioner.CreateAsync(msg.Spec.Size, msg.Spec.Region, token);

        // Accuser le traitement dans le STATUS : équivalent d'un ack, mais durable.
        msg.Status.Phase = "Ready";
        msg.Status.ObservedGeneration = msg.Generation();
        await _provisioner.UpdateStatusAsync(msg, token);
        // Une exception non capturée -> KubeOps requeue : le "message" sera relu.
    }
}

Piège courant : « Une Custom Resource est un message, donc on peut remplacer n’importe quel bus par des CRDs » est excessif. La Custom Resource convient aux intentions durables et peu fréquentes, pas à un flux à haut débit d’événements courts : chaque objet transite par l’API server et etcd, dont le débit et la volumétrie sont limités. Multiplier les Custom Resources éphémères sature le plan de contrôle et dégrade tout le cluster. Le modèle troque le débit contre la durabilité et l’idempotence gratuite ; l’employer là où l’on a besoin d’un très fort débit, c’est utiliser un tableau d’affichage pour acheminer un flot continu de dépêches.


Quiz — validation des acquis
Bonus 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.