Expert Chapitre 41-42 / 24

La réconciliation comme pattern universel & Concevoir son propre système réconcilié

Comprendre pourquoi la boucle observe → diff → agit dépasse Kubernetes et constitue un pattern d'ingénierie général, puis apprendre à concevoir un système réconcilié de bout en bout — avec du YAML, kubectl et du C#.

La réconciliation comme pattern universel

L’idée en une phrase

La réconciliation n’est pas une particularité de Kubernetes mais un pattern d’ingénierie général : dès qu’un système sépare la déclaration d’un état désiré de l’observation d’un état réel, et qu’une boucle observe → diff → agit referme continûment l’écart, on retrouve la même mécanique — que la ressource gérée soit un pod, une entrée DNS, une infrastructure cloud ou le solde d’un compte.

Analogie : Considérons le corps humain et l’homéostasie. La température cible de 37 °C est l’état désiré, la température mesurée est l’état réel. L’hypothalamus compare en permanence les deux et déclenche frisson ou transpiration pour refermer l’écart. Aucun organe n’exécute une séquence figée d’ordres : la correction naît de l’écart lui-même, exactement comme dans un contrôleur Kubernetes. Le même schéma régule la glycémie, l’hydratation ou la pression artérielle — une boucle par grandeur régulée.

Points clés

  • Le pattern se reconnaît à quatre invariants : une déclaration d’état désiré séparée du code qui l’applique, une observation de l’état réel, un calcul d’écart (diff), et une action correctrice répétée indéfiniment. Retirer l’un de ces éléments fait retomber le système dans l’impératif, où l’on décrit le « comment » au lieu du « quoi » (distinction posée au chapitre day 1-2).
  • Le pattern est level-based par nature : il agit sur l’état courant complet, non sur une suite d’événements (opposition vue au chapitre day 13-14). Cette propriété lui donne sa robustesse — un ordre manqué, dupliqué ou reçu dans le désordre n’a pas d’importance, car la boucle relit l’état réel à chaque cycle et converge quand même.
  • On le retrouve bien au-delà de l’orchestration de conteneurs. Terraform réconcilie une infrastructure déclarée vers des ressources cloud réelles ; un thermostat réconcilie une consigne vers une température ; un correcteur PID en automatique referme un écart de mesure ; DNS propage un état désiré de zone vers des résolveurs. Le vocabulaire diffère, le squelette est identique.
  • La convergence remplace l’exécution garantie. Un système réconcilié ne promet pas qu’une action précise a eu lieu, mais que l’état réel tend vers l’état désiré tant que la boucle tourne. Cette garantie est éventuelle (convergence), non instantanée — nuance déjà rencontrée avec le self-healing au chapitre day 39-40.
  • Le pattern échoue hors de son domaine de validité. Il suppose un état désiré descriptible et un état réel observable et réconciliable. Une action intrinsèquement impérative et non idempotente — « envoyer exactement un e-mail », « débiter une fois » — ne se modélise pas directement comme un champ à faire converger : il faut alors réifier l’effet en un état observable (un e-mail « marqué envoyé »).

Exemple concret

Terraform illustre le pattern hors de Kubernetes. Un fichier .tf déclare l’état désiré : un bucket de stockage et deux machines. La commande terraform plan observe l’état réel (via l’API cloud et le fichier d’état), calcule le diff — « bucket absent, 1 machine à créer, 1 conforme » — et terraform apply agit pour refermer l’écart. Si une machine est supprimée hors de Terraform, le prochain plan détecte de nouveau l’écart et propose de la recréer : c’est la détection de dérive du chapitre day 39-40, appliquée à une infrastructure entière plutôt qu’à des pods. La différence majeure avec Kubernetes tient à la boucle : Terraform réconcilie sur invocation (un humain lance apply), là où un contrôleur Kubernetes réconcilie en continu. Le squelette observe → diff → agit est pourtant rigoureusement le même.

Tableau — le même pattern sous plusieurs formes

SystèmeÉtat désiréÉtat réelFréquence de la boucle
KubernetesManifeste dans etcdObjets du clusterContinue (watch + resync)
TerraformFichiers .tfRessources cloudSur invocation (apply)
ThermostatConsigneTempérature mesuréeContinue (capteur)
GitOps (ArgoCD)Dépôt GitÉtat du clusterContinue (polling | webhook)
DNSZone déclaréeEnregistrements servisPériodique (TTL, refresh)

Code YAML — le pattern lu dans un objet quelconque

# Tout objet réconcilié expose la même dualité : spec (désiré) / status (réel).
# Ce n'est pas propre aux Deployments — c'est la signature du pattern.
apiVersion: acme.io/v1
kind: DatabaseBackup
metadata:
  name: nightly
spec:
  schedule: "0 2 * * *"     # état DÉSIRÉ : ce que l'on veut
  retentionDays: 30
status:
  lastBackupTime: "2026-07-19T02:00:11Z"   # état RÉEL : ce qui est observé
  observedGeneration: 4                     # quelle spec a été réconciliée

Code bash — reconnaître la boucle dans n’importe quel outil déclaratif

# Terraform : observe → diff (plan), puis agit (apply). Même squelette que kubectl.
terraform plan
# Plan: 1 to add, 0 to change, 0 to destroy.   <-- le DIFF, explicite

terraform apply -auto-approve
# aws_instance.web: Creating...                <-- l'action corrige l'écart

# Rejouer plan sans rien changer : le diff est nul -> convergence atteinte
terraform plan
# No changes. Your infrastructure matches the configuration.

Piège courant : « La réconciliation est un mécanisme interne à Kubernetes » est réducteur. La réconciliation est un pattern dont Kubernetes est une implémentation particulièrement aboutie, pas l’inventeur. Le confondre avec l’outil empêche d’en reconnaître la forme ailleurs — dans un pipeline GitOps, un IaC, une régulation physique — et donc de le réutiliser à bon escient. Inversement, l’appliquer à un problème sans état désiré descriptible ni état réel observable produit une abstraction inutile : le pattern n’est universel que dans son domaine de validité.


Concevoir son propre système réconcilié

L’idée en une phrase

Concevoir un système réconcilié revient à répondre méthodiquement à cinq questions — quel est l’état désiré, où est-il stocké, comment observer l’état réel, comment calculer le diff, comment agir de façon idempotente — puis à faire tourner cette boucle en continu : la qualité de la conception se mesure à sa capacité à converger malgré les pannes, les redémarrages et les événements manqués.

Analogie : Considérons la construction d’un chef de chantier automatisé. On lui remet un plan (l’état désiré), on lui donne les moyens d’inspecter le bâtiment (observer l’état réel), on lui apprend à lister ce qui manque au plan (le diff), et on exige qu’il puisse reprendre son inspection à tout moment sans jamais démolir ce qui est déjà correct (idempotence). Un chef de chantier qui recommence tout à chaque visite, ou qui suppose que rien n’a bougé depuis la veille, ne tient pas la charge d’un chantier réel.

Points clés

  • Rendre l’état désiré déclaratif et versionné. Il doit être une donnée, non du code : un manifeste, une ressource, une ligne en base. Le séparer de l’action permet de l’auditer, de le diffuser et de le comparer. Une CRD (chapitre day 19-20) est le véhicule idiomatique dans l’écosystème Kubernetes ; le champ metadata.generation sert de repère de version.
  • Choisir level-based plutôt qu’edge-based. La boucle doit relire l’état réel complet à chaque cycle et non réagir uniquement aux événements. Les événements ne servent qu’à déclencher un cycle plus tôt ; ils ne portent jamais la logique. Ce choix, détaillé au chapitre day 13-14, est ce qui rend le système tolérant aux notifications perdues.
  • Garantir l’idempotence de la phase « agit ». Réconcilier N fois le même état désiré doit produire le même résultat qu’une seule fois (règle d’or du chapitre day 23-24). Concrètement : préférer un apply/create-or-update (chapitre day 27-28) à un create aveugle, et vérifier l’existence avant d’agir. C’est cette propriété qui autorise le resync et le redémarrage sans dégât.
  • Refléter le résultat dans un status observable. La boucle écrit dans un status ce qu’elle a observé et fait, notamment observedGeneration pour savoir quelle version de la spec a été réconciliée (chapitre day 5-6). Sans status, aucune observabilité (chapitre day 31-32) : impossible de distinguer « pas encore réconcilié » de « en échec ».
  • Prévoir la reprise sur erreur et la fin de vie. Une réconciliation qui échoue doit être requeue avec backoff (chapitre day 23-24), jamais abandonnée. La suppression doit passer par un finalizer (chapitre day 25-26) si un nettoyage externe est nécessaire. Un système réconcilié se juge autant sur son chemin nominal que sur sa capacité à converger après une panne.

Exemple concret

On souhaite un contrôleur qui, pour chaque objet DatabaseBackup déclarant retentionDays: 30, garantit qu’un CronJob de sauvegarde existe et purge les sauvegardes de plus de 30 jours. La conception suit les cinq questions. État désiré : la spec du DatabaseBackup, stockée comme CRD dans etcd. Observation : lister le CronJob enfant et les sauvegardes existantes. Diff : le CronJob est-il présent et conforme au schedule ? Des sauvegardes dépassent-elles la rétention ? Action idempotente : create-or-update du CronJob, suppression des sauvegardes expirées — rejouable sans risque. Status : écrire lastBackupTime et observedGeneration. À t₀, l’objet est créé ; la boucle crée le CronJob. À t₀+1 h, un opérateur supprime le CronJob à la main ; au cycle suivant, la boucle observe son absence, calcule l’écart et le recrée — self-healing obtenu gratuitement, sans code spécifique, parce que la boucle est level-based et idempotente.

Tableau — les cinq décisions de conception

Question de conceptionBonne réponseAnti-pattern à éviter
Où vit l’état désiréDonnée déclarative versionnée (CRD, manifeste)Logique impérative codée en dur
Comment observer l’état réelRelecture complète à chaque cycle (level-based)Se fier au seul flux d’événements
Comment agircreate-or-update idempotentcreate aveugle non rejouable
Comment tracer le résultatstatus + observedGenerationAucun retour observable
Comment gérer l’échecRequeue avec backoff, finalizerAbandonner ou lever sans retenter

Code C# / KubeOps — la charpente d’un reconciler conçu selon ces principes

// Un reconciler qui applique les cinq décisions de conception.
// L'entité EST l'état désiré ; la méthode est rejouée à chaque cycle (level-based).
public class DatabaseBackupController : IEntityController<V1DatabaseBackup>
{
    private readonly IKubernetesClient _client;

    public DatabaseBackupController(IKubernetesClient client) => _client = client;

    public async Task ReconcileAsync(V1DatabaseBackup entity, CancellationToken token)
    {
        // 1. OBSERVER l'état réel : le CronJob enfant existe-t-il déjà ?
        var existing = await _client.GetAsync<V1CronJob>(
            entity.Name(), entity.Namespace(), token);

        // 2. DIFF + AGIR de façon IDEMPOTENTE : create-or-update, jamais un create aveugle.
        //    Rejouer cette méthode N fois produit le même résultat qu'une fois.
        var desired = BuildCronJob(entity);          // dérivé de la spec (état désiré)
        if (existing is null)
            await _client.CreateAsync(desired, token);
        else
            await _client.UpdateAsync(Merge(existing, desired), token);

        // 3. Refléter le résultat dans le STATUS (observabilité + observedGeneration)
        entity.Status.ObservedGeneration = entity.Generation();
        entity.Status.LastBackupTime = DateTime.UtcNow;
        await _client.UpdateStatusAsync(entity, token);

        // En cas d'exception non capturée, KubeOps REQUEUE avec backoff : ne jamais
        // avaler l'erreur silencieusement — la boucle doit pouvoir retenter.
    }
}

Code YAML — la CRD qui porte l’état désiré du système conçu

# La CRD réifie l'état désiré en donnée déclarative et versionnée :
# c'est la première des cinq décisions de conception.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databasebackups.acme.io
spec:
  group: acme.io
  scope: Namespaced
  names:
    kind: DatabaseBackup
    plural: databasebackups
  versions:
    - name: v1
      served: true
      storage: true
      subresources:
        status: {}          # status séparé : la boucle y écrit l'état réel observé
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                schedule: { type: string }
                retentionDays: { type: integer }

Piège courant : « Concevoir un système réconcilié, c’est écrire une boucle qui vérifie l’état toutes les N secondes » est une vision incomplète. La périodicité est le détail le moins important ; ce qui fait la solidité du système, c’est l’idempotence de l’action et le caractère level-based de l’observation. Une boucle qui relit tout et agit de façon rejouable converge même si elle tourne rarement, redémarre en plein cycle ou manque des événements. À l’inverse, une boucle très fréquente mais dont l’action n’est pas idempotente accumule les effets de bord et diverge. On ne conçoit pas d’abord une horloge : on conçoit d’abord une convergence.


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