Avancé Chapitre 25-26 / 13

Owner references et garbage collection & Finalizers : réconcilier la suppression

Comment Kubernetes lie les objets entre eux par ownerReferences, supprime automatiquement les enfants orphelins, et comment les finalizers permettent de réconcilier la suppression — avec YAML, kubectl et C#/KubeOps.

Owner references et garbage collection

L’idée en une phrase

Une ownerReference est un lien de parenté inscrit dans les métadonnées d’un objet Kubernetes, indiquant qu’il appartient à un autre objet (son owner). Le garbage collector est le réconciliateur chargé de l’état « pas d’orphelins » : lorsqu’un owner disparaît, il observe les enfants restants, calcule l’écart avec l’état désiré (aucun enfant sans parent) et les supprime pour le combler.

Analogie : Considérons un bail locatif liant des équipements de bureau à une entreprise. Chaque équipement porte la mention « propriété de Société X ». Si la société est liquidée, le liquidateur identifie tous les équipements rattachés et procède à leur restitution. L’ownerReference est ce lien contractuel ; le garbage collector est le liquidateur. Sans cette mention, les équipements resteraient indéfiniment dans des bureaux vides — des orphelins silencieux consommant des ressources.

Points clés

  • Le champ metadata.ownerReferences est un tableau : chaque entrée contient apiVersion, kind, name et uid de l’owner. Le champ blockOwnerDeletion: true retient la suppression de l’owner tant que l’enfant existe (cascade foreground).
  • Kubernetes gère trois politiques de suppression en cascade : Background (par défaut — l’owner disparaît immédiatement, le GC nettoie les enfants en arrière-plan), Foreground (l’owner reste avec un deletionTimestamp tant que tous les enfants bloquants ne sont pas supprimés) et Orphan (les enfants sont dissociés et survivent).
  • Le garbage collector est lui-même une boucle de réconciliation : il surveille les objets dont l’ownerReference pointe vers un UID inexistant et les supprime. C’est un processus level-based (chapitre 13-14) : il réévalue l’état à chaque passage, sans dépendre d’un événement de suppression ponctuel.
  • Un objet peut avoir plusieurs owners. Il n’est supprimé par le GC que lorsque tous ses owners ont disparu — le dernier lien rompu déclenche le nettoyage.
  • Les ownerReferences sont automatiquement posées par les contrôleurs natifs (Deployment → ReplicaSet → Pod, comme vu au chapitre 9-10), mais un Operator custom doit les poser explicitement sur les ressources enfants qu’il crée.

Exemple concret

Un Deployment webapp déclare 3 replicas. Le contrôleur de Deployment crée un ReplicaSet webapp-7d4b9c avec une ownerReference pointant vers le Deployment. Le ReplicaSet crée ensuite 3 Pods, chacun portant une ownerReference vers le ReplicaSet. À t₀, l’administrateur supprime le Deployment avec kubectl delete deployment webapp. En cascade background (par défaut), le Deployment disparaît de l’API server. Le garbage collector observe que le ReplicaSet webapp-7d4b9c pointe vers un UID inexistant : il le supprime. Les 3 Pods, désormais orphelins à leur tour, sont eux aussi supprimés. Trois niveaux d’arbre nettoyés, sans aucune intervention manuelle — la réconciliation a convergé vers « zéro enfant orphelin ».

Politiques de suppression en cascade

PolitiqueComportement ownerComportement enfantsCas d’usage
Background (défaut)Supprimé immédiatementGC les nettoie en arrière-planSuppression standard
ForegroundReste visible, deletionTimestamp poséSupprimés en premier ; l’owner disparaît aprèsGarantir le nettoyage avant l’owner
OrphanSupprimé immédiatementConservés, ownerReference retiréeMigration, adoption par un autre owner

Code YAML — ownerReference posée par un Operator

# Le Deployment enfant créé par un Operator, lié à sa CR parente.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-deploy
  namespace: default
  ownerReferences:
    - apiVersion: myapp.example.com/v1
      kind: WebApp
      name: webapp            # nom de la CR parente
      uid: a1b2c3d4-...      # UID exact de la CR (obtenu via metadata.uid)
      controller: true        # ce contrôleur est le propriétaire principal
      blockOwnerDeletion: true # cascade foreground : l'owner attend ce child
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
        - name: web
          image: nginx:1.27

Code kubectl — observer la cascade

# Lister les ownerReferences d'un ReplicaSet
kubectl get rs webapp-7d4b9c -o jsonpath='{.metadata.ownerReferences[0].name}'
# webapp    ← le Deployment parent

# Supprimer le Deployment avec cascade background (défaut)
kubectl delete deployment webapp
# deployment.apps "webapp" deleted

# Observer la disparition progressive des enfants
kubectl get rs,pods -l app=webapp --watch
# NAME                             DESIRED   CURRENT   READY   AGE
# replicaset.apps/webapp-7d4b9c   0         0         0       5s
# Aucun pod restant : la cascade a convergé.

# Supprimer avec cascade orphan (les enfants survivent)
kubectl delete deployment webapp --cascade=orphan
# Les pods restent vivants, sans ownerReference.

Code C# — poser l’ownerReference en KubeOps

// Lors de la création d'un enfant, poser l'ownerReference
// lie le cycle de vie de l'enfant à celui de la CR parente.
public async Task ReconcileAsync(V1WebApp entity, CancellationToken token)
{
    var ns = entity.Namespace() ?? "default";
    var existing = await _client.Get<V1Deployment>(entity.Name(), ns);

    if (existing is null)
    {
        var deploy = BuildDeployment(entity);

        // Poser l'ownerReference : le GC supprimera ce Deployment
        // automatiquement si la CR WebApp disparaît.
        deploy.SetOwnerReference(entity);

        await _client.Create(deploy);
        return;
    }

    // Idempotent : si l'enfant existe et converge, rien à écrire (chapitre 23-24).
}

Piège courant : « Supprimer un Deployment supprime ses Pods directement » est inexact. La suppression se propage par ownerReferences à travers l’arbre : le Deployment est l’owner du ReplicaSet, qui est l’owner des Pods. Le garbage collector suit cette chaîne niveau par niveau. Si un Operator crée des enfants sans poser d’ownerReference, ces enfants survivent indéfiniment à la suppression de la CR parente — des ressources orphelines consommant du cluster sans être gérées par aucune boucle.


Finalizers : réconcilier la suppression

L’idée en une phrase

Un finalizer est une chaîne inscrite dans metadata.finalizers qui empêche la suppression effective d’un objet tant qu’elle n’a pas été retirée. Ce mécanisme transforme la suppression en un processus réconciliable : au lieu de disparaître instantanément, l’objet reçoit un deletionTimestamp (état désiré : « être supprimé ») et le contrôleur responsable observe cet état, exécute sa logique de nettoyage, puis retire le finalizer pour autoriser la suppression finale.

Analogie : Considérons un départ d’un logement. La résiliation du bail fixe une date de fin (le deletionTimestamp), mais le locataire ne peut pas simplement claquer la porte : il reste un état des lieux, un relevé de compteurs et la restitution des clés — autant de finalizers. Chaque étape accomplie est rayée de la liste. Ce n’est qu’une fois la liste vide que le propriétaire libère le dépôt de garantie et que le bail est effectivement clos. Sans cette liste, le locataire partirait en laissant derrière lui des dettes et des clés en circulation.

Points clés

  • metadata.finalizers est un tableau de chaînes arbitraires (par convention au format domaine inversé : myapp.example.com/cleanup). Tant qu’au moins un finalizer est présent, l’API server refuse de supprimer l’objet même si deletionTimestamp est posé.
  • Le flux de suppression avec finalizer suit trois temps : (1) l’utilisateur envoie DELETE → l’API server pose deletionTimestamp mais conserve l’objet ; (2) le contrôleur détecte deletionTimestamp, exécute le nettoyage externe (supprimer un bucket S3, révoquer un certificat, retirer une règle DNS…) ; (3) le contrôleur retire son finalizer → quand la liste est vide, l’API server supprime l’objet.
  • Le contrôleur doit ajouter son finalizer dès la première réconciliation et le retirer uniquement après un nettoyage réussi. Oublier de l’ajouter laisse la suppression s’effectuer sans nettoyage ; oublier de le retirer bloque l’objet indéfiniment en état « Terminating ».
  • La logique de finalizer est elle-même soumise à l’idempotence (chapitre 23-24) : le nettoyage peut être rejoué en cas de requeue ou de redémarrage. Supprimer un bucket qui n’existe plus ne doit pas échouer — il suffit de vérifier l’existence avant d’agir.
  • Les finalizers coopèrent avec les ownerReferences : en cascade foreground, un enfant avec un finalizer retient la suppression de l’owner tant que le nettoyage n’est pas terminé, garantissant un ordre de suppression déterministe.

Exemple concret

Un Operator gère une CR Database qui provisionne une base PostgreSQL externe. Le contrôleur pose le finalizer db.example.com/cleanup au premier Reconcile. Plus tard, l’administrateur exécute kubectl delete database prod-db. L’API server pose deletionTimestamp sur la CR mais ne la supprime pas (le finalizer la retient). Le contrôleur est notifié, détecte deletionTimestamp, se connecte au serveur PostgreSQL et supprime la base. Le nettoyage réussit : le contrôleur retire le finalizer et met à jour l’objet. La liste est vide — l’API server supprime la CR. Si le nettoyage avait échoué (serveur injoignable), l’exception serait propagée, la clé réenfilée avec backoff (chapitre 23-24), et la suppression resterait en attente jusqu’à convergence.

Cycle de vie d’un objet avec finalizer

PhasedeletionTimestampfinalizersÉtat de l’objet
Création + 1er Reconcileabsent["db.example.com/cleanup"]Actif, protégé
kubectl delete envoyéposé["db.example.com/cleanup"]Marqué pour suppression, non supprimé
Nettoyage terminéposé[]Prêt à disparaître
GC finaliseObjet supprimé de etcd

Code YAML — observer un objet en cours de suppression

# kubectl get database prod-db -o yaml (après un delete, finalizer non retiré)
apiVersion: db.example.com/v1
kind: Database
metadata:
  name: prod-db
  namespace: default
  deletionTimestamp: "2026-06-29T14:00:00Z"   # l'API veut le supprimer…
  finalizers:
    - db.example.com/cleanup                   # …mais ce finalizer le retient
spec:
  engine: postgresql
  version: "16"
status:
  phase: Deleting
  message: "Nettoyage de la base externe en cours"

Code kubectl — diagnostiquer un objet bloqué

# Un objet « stuck in Terminating » : le finalizer n'a pas été retiré
kubectl get database prod-db
# NAME      AGE   STATUS
# prod-db   7d    Terminating    ← deletionTimestamp posé, finalizer présent

# Inspecter les finalizers
kubectl get database prod-db -o jsonpath='{.metadata.finalizers}'
# ["db.example.com/cleanup"]

# En dernier recours (dangereux : saute le nettoyage), retirer manuellement :
kubectl patch database prod-db -p '{"metadata":{"finalizers":[]}}' --type=merge
# L'objet disparaît immédiatement — mais la base externe reste orpheline.

Code C# — finalizer complet en KubeOps

// KubeOps fournit IEntityFinalizer<T> : le framework ajoute automatiquement
// le finalizer au premier Reconcile et appelle FinalizeAsync à la suppression.
public class DatabaseCleanup : IEntityFinalizer<V1Database>
{
    private readonly IExternalDbClient _dbClient;

    public DatabaseCleanup(IExternalDbClient dbClient)
        => _dbClient = dbClient;

    public async Task FinalizeAsync(V1Database entity, CancellationToken token)
    {
        var dbName = entity.Spec.DatabaseName;

        // Idempotent : si la base n'existe plus (déjà nettoyée lors d'un
        // requeue précédent), ne pas échouer — l'écart est déjà nul.
        if (await _dbClient.ExistsAsync(dbName, token))
        {
            await _dbClient.DropAsync(dbName, token);
        }

        // En sortant sans exception, KubeOps retire automatiquement
        // le finalizer → l'API server supprime la CR.
        // Si une exception est levée, KubeOps réenfile avec backoff :
        // la suppression reste en attente jusqu'à convergence.
    }
}

Piège courant : « Un objet bloqué en Terminating est un bug de Kubernetes » est inexact. C’est le comportement nominal des finalizers : l’objet attend que chaque contrôleur responsable ait terminé son nettoyage et retiré son finalizer. Le vrai bug est soit un contrôleur arrêté qui ne traite plus les suppressions, soit un finalizer ajouté par un contrôleur qui n’existe plus. Retirer manuellement un finalizer avec kubectl patch supprime l’objet mais saute le nettoyage — les ressources externes restent orphelines.


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