Un Helm chart déploie une configuration figée : il installe des ressources selon des valeurs fournies au moment du déploiement, puis n’agit plus jusqu’au prochain helm upgrade exécuté par un humain. Un Operator fait autre chose : un contrôleur qui tourne en continu dans le cluster, surveille un état et agit de lui-même, sans attendre qu’un humain relance quoi que ce soit.

Le pattern, en trois pièces

Un Operator combine une CRD (Custom Resource Definition), qui déclare un nouveau type d’objet Kubernetes propre au domaine géré (un cluster de base de données, un certificat, une sauvegarde planifiée), et un contrôleur qui applique la même boucle de réconciliation que celle qui maintient un Deployment : observer l’état déclaré, le comparer à l’état réel, agir pour combler l’écart, en continu.

# La CRD déclare un objet métier propre au domaine,
# "PostgresCluster" n'existe pas nativement dans Kubernetes
apiVersion: postgres-operator.example.com/v1
kind: PostgresCluster
metadata:
  name: production-db
spec:
  replicas: 3
  version: "16"

Le contrôleur qui surveille cet objet encode ce qu’un DBA ferait manuellement : provisionner les replicas, gérer le failover si le primaire tombe, orchestrer une montée de version majeure dans le bon ordre. Cette connaissance opérationnelle, normalement dans la tête d’une personne ou dans un runbook, devient du code qui s’exécute en continu.

Ce qu’un Helm chart ne peut structurellement pas faire

Un Helm chart s’exécute une fois, au moment du déploiement, puis disparaît : il ne surveille rien après coup. Si le primaire d’une base de données tombe une heure après le déploiement, aucun Helm chart ne réagit, puisqu’il n’existe plus en tant que processus actif. Un Operator, lui, continue de tourner : sa boucle de réconciliation détecte la panne du primaire et déclenche un failover, sans qu’un humain n’ait besoin d’exécuter quoi que ce soit.

// Simplifié : le contrôleur est réveillé par les watches posées sur
// les objets qu'il surveille, pas par une temporisation
func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // observe l'état réel, compare à spec, agit si écart
    // Result vide : réconcilié, rien à refaire tant que rien ne change
    return ctrl.Result{}, nil
}

Le Result vide n’est pas un raccourci d’exemple, c’est le cas normal. Le livre Kubebuilder le commente exactement ainsi : « nous retournons un résultat vide et aucune erreur, ce qui indique à controller-runtime que nous avons réconcilié cet objet avec succès et que nous n’avons pas besoin de réessayer jusqu’à ce qu’il y ait des changements ». Le champ RequeueAfter existe — il « demande au contrôleur de réenfiler la clé de réconciliation après la durée indiquée » — mais c’est un filet, utile quand l’état à surveiller ne produit pas d’événement observable. Ce qui fait tourner la boucle, ce sont les watches.

Le critère qui décide : la logique opérationnelle continue

Construire un Operator se justifie quand la gestion d’une ressource exige une logique continue, pas seulement un déploiement initial : un failover automatique, une sauvegarde planifiée avec vérification d’intégrité, une montée de version qui doit respecter un ordre strict entre plusieurs composants dépendants. Un Helm chart suffit largement quand le besoin se limite à installer une configuration correcte une fois, sans surveillance continue nécessaire par la suite : la majorité des applications stateless entrent dans ce second cas.

Trois marches, pas deux

Poser le choix comme « Helm ou Operator » saute une marche, et c’est celle qui couvre la majorité des besoins qu’on croit devoir traiter par un contrôleur.

Marche 1 — Helm seul. helm install pose les ressources, puis plus rien ne surveille. Si quelqu’un édite un Deployment à la main, l’écart tient jusqu’au prochain helm upgrade.

Marche 2 — Helm réconcilié par GitOps. Argo CD documente le principe sans détour : « Helm n’est utilisé que pour générer les manifestes avec helm template. Le cycle de vie de l’application est géré par Argo CD à la place de Helm. » Côté Flux, le HelmRelease du helm-controller expose un champ driftDetection : activé, le contrôleur compare le manifeste conservé par Helm à l’état réel du cluster via un dry-run côté serveur, et signale — ou corrige — dès qu’un écart apparaît. La dérive est rattrapée en continu, sans écrire une ligne de Go.

Marche 3 — Operator. Elle se justifie quand la logique n’est pas exprimable comme un état désiré de manifestes. Un failover suppose de savoir quel replica est primaire, de promouvoir le bon, dans un ordre qui dépend de l’état de la réplication : aucune quantité de YAML réconcilié ne fait ça.

Le besoin La réponse
Installer une configuration correcte, une fois Helm seul
Que la configuration installée reste conforme au dépôt Helm réconcilié par GitOps
Réagir à un état que le cluster ne décrit pas (réplication, sauvegarde vérifiée) Operator
Offrir un type d’objet métier à d’autres équipes Operator : CRD plus contrôleur

Le piège qui relie les deux moitiés : quand un Operator se distribue en Helm chart

Un Operator doit bien s’installer quelque part. Dès qu’il se distribue en Helm chart, Helm revient dans le circuit par la porte de derrière, et y apporte une limite qui n’a rien à voir avec le contrôleur. Helm sait installer des CRD — un répertoire crds/ est prévu pour ça — mais il ne sait pas les mettre à jour. La documentation Helm est catégorique : « Il n’existe à ce jour aucun support pour la mise à jour ou la suppression des CRD via Helm. » Elle précise au passage que si la CRD existe déjà, elle est ignorée avec un avertissement.

Conséquence directe et rarement anticipée : helm upgrade installe le nouveau contrôleur avec les anciennes CRD. Le contrôleur en version N+1 réconcilie des objets dont le schéma est resté en version N, et un champ ajouté dans la nouvelle version est refusé par l’API server, qui ne le connaît pas.

Trois contournements sont documentés, et il vaut la peine de savoir qui suggère quoi. Helm n’en propose qu’un : sortir les CRD dans un chart séparé, installé à part. Le deuxième — les placer dans templates/ plutôt que dans crds/ — vient de la documentation Flux, qui ne crédite Helm que du chart séparé. Il fonctionne d’ailleurs en sortant du mécanisme crds/, dont Helm rappelle que les fichiers « ne peuvent pas être templatés, ce doivent être des documents YAML simples » ; le templating en a été retiré pour que helm conserve une vision valide des API disponibles dans le cluster. On récupère la mise à jour en rendant au chart l’incertitude que crds/ avait justement supprimée.

Le troisième vient aussi de Flux, mais comme fonctionnalité plutôt que comme conseil : une politique .spec.install.crds et .spec.upgrade.crds sur le HelmRelease, avec trois valeurs, Skip, Create et CreateReplace.

Les défauts méritent d’être lus deux fois : Create à l’installation, Skip à la mise à jour. Le comportement par défaut de Flux reproduit donc exactement la limitation de Helm. Il faut poser CreateReplace explicitement pour que les CRD suivent le chart.

spec:
  install:
    crds: CreateReplace
  upgrade:
    # Sans cette ligne, la valeur par défaut est Skip :
    # le contrôleur monte de version, ses CRD non.
    crds: CreateReplace

Le coût réel de construire un Operator

Un Operator est un logiciel à part entière : il a ses propres bugs, ses propres tests, sa propre release, et une boucle de réconciliation mal écrite peut créer des effets de bord difficiles à diagnostiquer (une action répétée en boucle si la condition de sortie est mal définie). Kubebuilder et Operator SDK réduisent le code nécessaire pour un Operator minimal, mais n’éliminent pas la responsabilité d’opérer ce logiciel supplémentaire sur la durée, un coût souvent sous-estimé face à la simplicité apparente d’un Helm chart.

Quatre coûts s’ajoutent, rarement présents au moment de la décision.

Le périmètre RBAC. Le scaffolding par défaut de Kubebuilder génère un ClusterRole, produit par controller-gen à partir des marqueurs +kubebuilder:rbac posés sur le reconciler. Un contrôleur cluster-scoped capable de créer des Secrets ou des Deployments dans n’importe quel namespace est un chemin d’escalade : qui obtient l’exécution de code dans ce pod obtient ses droits.

L’élection de leader. Dès deux replicas, elle devient nécessaire, sans quoi deux boucles agissent sur les mêmes objets. controller-runtime la fournit — le manager expose une option LeaderElection, et ses runnables tournent soit en permanence, soit sous le contrôle de l’élection. C’est une dépendance de plus vers l’API server, avec ses LeaseDuration et RenewDeadline à comprendre le jour où le contrôleur cesse d’agir sans message.

Les finalizers. C’est le mécanisme normal du nettoyage, et son point de défaillance. À la suppression, l’API server pose un deletionTimestamp, renvoie un 202, et « empêche l’objet d’être retiré tant que tous les éléments n’ont pas été retirés de son champ metadata.finalizers ». C’est le contrôleur qui les retire, une fois les conditions satisfaites. Contrôleur mort, finalizer jamais retiré, objet jamais supprimé — et la demande de suppression reste acceptée, donc silencieuse.

Les webhooks de conversion. Le coût le plus sous-estimé de tous, parce qu’il n’arrive qu’à la deuxième version. Une CRD peut porter plusieurs versions aux schémas différents, et la documentation Kubernetes indique que ce sont des webhooks de conversion qui convertissent les ressources d’une version à l’autre. Il faut écrire ce serveur, le déployer, lui fournir un certificat, et le garder disponible : faire évoluer une CRD, ce n’est pas éditer un fichier, c’est opérer un service de plus dans le chemin critique de l’API.

L’Operator SDK propose par ailleurs d’empaqueter un chart existant en contrôleur (operator-sdk init --plugins helm, avec --helm-chart pour partir d’un chart local ou distant). L’option existe et est documentée ; ce que ce mode sait et ne sait pas exprimer se juge chart par chart, et mérite d’être vérifié avant d’en faire un plan.

À retenir

Un Helm chart déploie une configuration figée puis disparaît ; un Operator combine une CRD et un contrôleur qui tourne en continu, appliquant une boucle de réconciliation pour encoder une connaissance opérationnelle qui continue d’agir après le déploiement initial. Le critère qui décide entre les deux est la présence ou non d’une logique opérationnelle continue à automatiser (failover, sauvegarde planifiée, montée de version orchestrée), pas une préférence technique. Construire un Operator a un coût réel de maintenance logicielle, à mettre en balance avec la simplicité d’un Helm chart, un arbitrage qui se pose dès qu’une migration Kubernetes doit automatiser une opération que personne ne veut refaire à la main.