Contexte : le pattern app-of-apps et le fonctionnement général du GitOps sur ce cluster sont décrits dans GitOps avec ArgoCD — le pattern App-of-Apps.
Le symptôme : une documentation qui commence par un avertissement
Pendant des mois, le fichier de référence de mon infra a commencé par cette phrase :
⚠️ Lis d’abord la section « Méthode de déploiement » : tout n’est PAS GitOps. Éditer le repo pour un composant géré par Helm direct ne fait rien, et inversement un
kubectl/helmmanuel sur un composant ArgoCD est écrasé par le selfHeal.
Quand la documentation d’un cluster s’ouvre sur un avertissement, ce n’est pas de la pédagogie : c’est le symptôme. Le 5 septembre 2026, helm list -A sur le nœud control-plane renvoyait encore neuf releases :
NAME NAMESPACE REVISION CHART STATUS
traefik kube-system 3 traefik-40.1.3+up40.1.0 deployed
traefik-crd kube-system 5 traefik-crd-40.1.3+up40.1.0 deployed
grafana monitoring 6 grafana-10.5.15 deployed
vmagent monitoring 18 vmagent-0.41.2 deployed
vmsingle monitoring 9 victoria-metrics-single-… deployed
node-exporter monitoring 4 prometheus-node-exporter-… deployed
kube-state-metrics monitoring 2 kube-state-metrics-7.5.2 deployed
sealed-secrets kube-system 11 sealed-secrets-2.18.5 deployed
longhorn longhorn-system 7 longhorn-1.12.0 deployed
Deux de ces releases sont légitimes : traefik et traefik-crd sont gérées par k3s lui-même via son mécanisme de manifests embarqués. Les sept autres étaient de la dette pure — des composants installés à la main « en attendant », dont plusieurs remontaient au montage initial du cluster.
Le coût de cette dette n’est pas théorique. Il se paie en secondes d’hésitation, plusieurs fois par semaine : pour changer la rétention de vmsingle, j’édite le repo ou je fais un helm upgrade ? Une erreur de réponse dans un sens ne fait rien du tout ; dans l’autre, elle se fait écraser par le selfHeal trois minutes plus tard. Deux sources de vérité dans un même cluster, ce n’est pas deux fois plus de souplesse — c’est une question de plus à se poser avant chaque geste.
Le contexte : ArgoCD n’« adopte » rien, il applique
Il faut lever une ambiguïté avant d’aller plus loin, parce qu’elle détermine toute la méthode.
ArgoCD sait consommer un chart Helm, mais il ne se comporte pas comme le client Helm. Il n’installe pas une release : il exécute l’équivalent d’un helm template — le rendu du chart en manifests bruts — puis applique ces manifests au cluster comme n’importe quel YAML. Il ne lit pas les secrets sh.helm.release.v1.* qui stockent l’état d’une release, il n’en crée pas, il n’en met pas à jour.
Concrètement, « adopter » une release Helm existante veut dire : créer une Application ArgoCD dont le rendu tombe exactement sur les objets déjà présents dans le cluster. Il n’y a pas de transfert de propriété, pas de commande de migration. Il y a deux processus qui visent les mêmes objets, et on veut que le second produise rigoureusement ce que le premier avait produit.
Si le rendu est identique, l’application est un no-op : Kubernetes reçoit des objets qu’il a déjà, rien ne redémarre. Si le rendu diffère ne serait-ce que d’un champ — une valeur par défaut du chart qui a bougé entre deux versions mineures, une annotation absente — alors le sync déclenche un rollout. Sur un Deployment de monitoring, c’est un désagrément. Sur la couche de stockage qui sert les volumes de tout le cluster, c’est un incident.
D’où le pattern retenu, dit multiple-sources : une Application qui pointe vers deux sources à la fois — le chart officiel épinglé chez son éditeur, et le dépôt GitOps qui fournit le fichier de values.
spec:
sources:
- repoURL: https://prometheus-community.github.io/helm-charts
chart: prometheus-node-exporter
targetRevision: 4.53.1
helm:
releaseName: node-exporter
valueFiles:
- $values/monitoring/vm/node-exporter-values.yaml
- repoURL: <url-interne-du-repo-gitops>
targetRevision: actual-state
ref: values
La deuxième source ne déploie rien : le ref: values en fait un simple fournisseur de fichiers, référencé par $values dans la première. On garde ainsi le chart amont intact — pas de vendoring, pas de fork — tout en versionnant la configuration dans le dépôt.
La méthode : cinq temps, dont trois réversibles
la version déjà déployée, pas la dernièrethe deployed version, not the latest
adopter et mettre à jour sont deux changements : jamais dans le même gesteadopting and upgrading are two changes: never in the same move
la config réelle, pas celle du READMEthe real config, not the README one
la source de vérité est la release en cours, pas le souvenir de son installationthe source of truth is the running release, not the memory of its install
le rendu doit être identique au vivantthe render must match what runs
une seule ligne de diff = une modification déguisée en migration : on corrige avanta single diff line means a change disguised as a migration: fix it first
Application sans automatedApplication without automated
le premier sync se regarde : zéro restart de pod est le critère de réussitethe first sync is watched: zero pod restarts is the pass criterion
commit dédié, après coupdedicated commit, afterwards
prune reste absent : une app adoptée ne doit jamais pouvoir supprimer son socleprune stays absent: an adopted app must never be able to delete its own base
point de non-retourpoint of no return① Iso-version, sans exception
La tentation est réelle : puisqu’on touche au composant, autant passer à la dernière version du chart. Il faut y résister absolument. Adopter et mettre à jour sont deux changements de nature différente, et les mélanger rend le diagnostic impossible — si un pod redémarre, était-ce l’adoption ou le bump ?
La version cible est donc celle que helm list -A affiche déjà, au patch près : 4.53.1 pour node-exporter, 7.5.2 pour kube-state-metrics, 10.5.15 pour grafana, 2.18.5 pour sealed-secrets, 1.12.0 pour le chart Longhorn. Le bump viendra plus tard, comme un commit ordinaire — et c’est justement l’intérêt de l’opération.
② Les values vivantes, pas celles du README
La configuration à versionner est celle qui est effectivement appliquée à la release en cours, pas celle qu’on croit avoir appliquée à l’installation :
helm get values kube-state-metrics -n monitoring
Sur ce cluster, trois des sept composants avaient déjà leurs values dans le dépôt — héritées de l’installation initiale. Le contrôle valait quand même la peine : pour grafana, vmagent et vmsingle, le fichier du dépôt et la release vivante coïncidaient exactement, dix-huit révisions de vmagent comprises. Pour les autres, l’extraction a produit des fichiers courts et honnêtes :
# Values kube-state-metrics — consommées par l'Application ArgoCD
# Source : helm get values kube-state-metrics -n monitoring (release rev 2)
resources:
limits:
cpu: 200m
memory: 96Mi
requests:
cpu: 20m
memory: 32Mi
③ Le diff de pré-adoption doit être vide
C’est le contrôle qui porte toute la sécurité de l’opération, et il tient en une ligne :
helm template <release> <repo>/<chart> --version <X> \
-f values.yaml -n <namespace> | kubectl diff -f -
On rend le chart exactement comme ArgoCD le rendra, et on compare au cluster vivant. La sortie attendue est vide. Pas « courte », pas « sans changement important » : vide.
Toute ligne qui apparaît ici est un changement déguisé en migration. Il faut la comprendre et la faire disparaître — en ajustant les values, en corrigeant la version — avant de continuer. Sur les sept composants, le diff a été vide du premier coup à chaque fois, y compris sur les cas volumineux : dix ressources pour sealed-secrets, quarante-deux pour le chart Longhorn (CRDs comprises), cinq pour kube-state-metrics dont un ClusterRole et son binding.
Deux ajustements ont été nécessaires pour obtenir ce résultat. Le rendu doit se faire avec --no-hooks sur les charts qui embarquent des jobs de cycle de vie, sinon kubectl diff propose de créer des objets qui n’ont aucune raison d’exister en régime permanent. Et l’Application doit porter ServerSideApply=true : sans lui, ArgoCD compare des objets partiels sur des ressources volumineuses comme les CRDs Longhorn et voit des différences qui n’en sont pas.
④ Sync manuel, automated absent
L’Application est créée sans bloc automated. Le premier sync se déclenche à la main, et on le regarde :
# Adoption : sync manuel d'abord ; automated/selfHeal activés après le premier
# sync vérifié sain (commit dédié). prune volontairement absent.
syncPolicy:
syncOptions:
- ServerSideApply=true
Le critère de réussite n’est pas « ArgoCD affiche Synced ». C’est zéro redémarrage de pod : six pods node-exporter intacts, aucun restart ; le controller sealed-secrets toujours en vie et kubeseal --fetch-cert fonctionnel ; et pour Longhorn, 14 volumes sur 14 restés healthy pendant l’opération, 6 managers et leurs instance-managers inchangés.
⑤ selfHeal, dans un commit séparé
automated: { selfHeal: true } n’arrive qu’ensuite, dans un commit dédié dont le message documente la validation :
# Adoption validée : diff pré-adoption vide, premier sync Succeeded
# sans redémarrage de pod. prune volontairement absent (app adoptée depuis Helm).
syncPolicy:
automated:
selfHeal: true
syncOptions:
- ServerSideApply=true
Deux absences sont volontaires et permanentes.
prune reste désactivé sur les sept applications. Une app adoptée depuis Helm ne doit jamais avoir le droit de supprimer des objets : si une erreur de values fait disparaître une ressource du rendu, on veut un écart visible, pas une suppression silencieuse.
Le finalizer est absent sur sealed-secrets et sur le chart Longhorn. Par défaut on ajoute resources-finalizer.argocd.argoproj.io pour que la suppression d’une Application nettoie ses ressources. Sur ces deux-là, c’est exactement ce qu’on ne veut pas : une suppression accidentelle de l’Application ne doit jamais cascader sur le controller de chiffrement ou sur la couche de stockage.
Les pièges : ce qui n’était pas dans le plan
Le dépôt de charts qui déménage
L’adoption de sealed-secrets a fonctionné, puis a cassé quelques minutes plus tard. L’éditeur du chart avait migré son dépôt de bitnami-labs.github.io vers bitnami.github.io en juin 2026 — et GitHub Pages ne redirige pas : l’ancienne URL renvoie un 404 sec.
Le client Helm ne l’avait jamais signalé parce qu’il servait le chart depuis son cache local. ArgoCD, lui, résout le dépôt à chaque sync. Une release Helm installée depuis une URL morte peut vivre des mois sans que personne ne le sache ; l’adoption GitOps est ce qui rend la panne visible. C’est un argument pour la migration plus qu’un argument contre.
Longhorn : le job qui suppose un cycle Helm
Le chart Longhorn embarque un pre-upgrade-checker, un job qui vérifie l’état du système avant une montée de version. Il suppose un cycle de vie Helm classique et échoue sous ArgoCD — l’éditeur documente d’ailleurs sa désactivation comme prérequis pour les installations GitOps :
preUpgradeChecker:
jobEnabled: false
Les autres hooks du chart s’accommodent du modèle ArgoCD : le job de post-upgrade est idempotent et tourne en PostSync, le hook de pre-delete est simplement ignoré. Et une contrainte reste entière après l’adoption : les montées de version Longhorn doivent être strictement séquentielles, sans jamais sauter une version mineure. Passer par ArgoCD ne change rien à cette règle, elle est juste devenue plus facile à enfreindre — un bump de targetRevision est un caractère à changer.
Le PVC qui n’appartient à personne
vmsingle est un StatefulSet, et son volume est créé par un volumeClaimTemplate. Ces PVC-là sont générés par le contrôleur StatefulSet, pas par le chart : ils restent hors du périmètre géré par ArgoCD. Ce n’est pas un problème en soi, mais c’est à savoir avant de se demander pourquoi le stockage de la base de métriques n’apparaît nulle part dans l’arbre de l’Application.
Un nom de fichier interdit par sa propre protection
Détail trivial mais qui coûte cinq minutes : le manifeste de l’Application sealed-secrets ne pouvait pas s’appeler sealed-secrets.yaml. Le dépôt embarque une règle .gitignore sur *secrets.yaml — un garde-fou anti-fuite installé bien plus tôt, qui a fait exactement son travail sur un fichier parfaitement inoffensif. Le fichier s’appelle sealed-secrets-app.yaml.
La vérification qui ne se voit pas dans le diff
La clé privée du controller sealed-secrets — celle qui déchiffre tous les secrets du dépôt — n’est pas une ressource du chart. Elle est générée par le controller au premier démarrage et vit dans un Secret du cluster. Aucun diff, aucun rendu Helm, aucune sortie de commande ne l’aurait signalée.
C’est pourtant le seul point de l’opération où une erreur aurait été irréversible : perdre cette clé, c’est perdre la capacité de déchiffrer l’intégralité des secrets versionnés. La sauvegarde break-glass a donc été vérifiée dans le gestionnaire de mots de passe avant de toucher à l’Application. Une checklist d’adoption ne se limite pas à ce que les outils savent comparer.
Le résultat, et le point de non-retour
Après les sept adoptions :
$ helm list -A
NAME NAMESPACE REVISION CHART STATUS
traefik kube-system 3 traefik-40.1.3+up40.1.0 deployed
traefik-crd kube-system 5 traefik-crd-40.1.3+up40.1.0 deployed
Il ne reste que les deux releases gérées par k3s. Le cluster est passé de 24 à 31 Applications ArgoCD, toutes Synced et Healthy, et l’avertissement en tête de la documentation a été remplacé par une phrase affirmative.
Il faut être clair sur ce qui a été perdu au passage. Les secrets de release Helm sont purgés : helm list ne connaît plus ces composants, helm upgrade ne fonctionnera plus sur eux, et helm rollback non plus. Le binaire helm ne sert plus qu’à helm template pour du debug. C’est le point de non-retour de l’étape ⑤ — assumé, mais réel : la seule façon de mettre à jour ces composants est désormais de changer targetRevision dans le dépôt et de pousser.
Dernière observation, plus inconfortable : le chart grafana/grafana est marqué deprecated en amont. Le pin en 10.5.15 tient, mais l’adoption GitOps n’a pas fait disparaître cette dette-là — elle l’a juste rendue lisible dans un fichier versionné plutôt que dans la mémoire de celui qui a lancé le helm install. C’est à peu près tout ce qu’on peut demander à une migration : ne pas résoudre les problèmes de fond, mais arrêter de les cacher.
Runbook — adopter une release Helm dans ArgoCD
- Relever l’existant :
helm list -Apour la version exacte du chart,helm get values <rel> -n <ns>pour la configuration réellement appliquée. - Vérifier ce que le diff ne verra pas : clés, données générées par le composant lui-même, sauvegardes break-glass. Le faire avant, pas après.
- Versionner les values dans le dépôt GitOps, avec un commentaire d’en-tête citant leur provenance et leur date.
- Écrire l’Application en multiple-sources,
targetRevisionà l’iso-version,ServerSideApply=true, sansautomated, sansprune. Pas de finalizer sur les composants dont la suppression serait catastrophique. - Exiger un diff vide :
helm template … --no-hooks | kubectl diff -f -. Toute ligne de sortie est un stop. - Synchroniser à la main et vérifier l’absence de redémarrage de pod, pas seulement le statut
Synced. - Activer
selfHealdans un commit séparé, une fois le sync validé, en documentant la vérification dans le message de commit. - Un composant à la fois. Sept adoptions en une session, c’est possible ; sept adoptions dans un seul commit, non — quand quelque chose bouge, il faut savoir quoi.