❯ gitops · Running

Adopter 7 releases Helm dans ArgoCD sans redémarrer un seul pod Adopting 7 Helm Releases into ArgoCD Without Restarting a Single Pod

Sept composants étaient encore installés à la main avec Helm, hors GitOps — dont la couche de stockage et le controller de secrets. Voici la méthode d'adoption qui les a tous fait passer sous ArgoCD avec un diff de pré-adoption vide et zéro redémarrage de pod, et les pièges rencontrés en chemin. Seven components were still installed by hand with Helm, outside GitOps — including the storage layer and the secrets controller. Here is the adoption method that moved all of them under ArgoCD with an empty pre-adoption diff and zero pod restarts, and the traps found along the way.

· 11 min de lecture 11 min read ·
k3s-clusterargocdgitopshelmk8smigration
🇬🇧 Read this article in English

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/helm manuel 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

① 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

  1. Relever l’existant : helm list -A pour la version exacte du chart, helm get values <rel> -n <ns> pour la configuration réellement appliquée.
  2. 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.
  3. Versionner les values dans le dépôt GitOps, avec un commentaire d’en-tête citant leur provenance et leur date.
  4. Écrire l’Application en multiple-sources, targetRevision à l’iso-version, ServerSideApply=true, sans automated, sans prune. Pas de finalizer sur les composants dont la suppression serait catastrophique.
  5. Exiger un diff vide : helm template … --no-hooks | kubectl diff -f -. Toute ligne de sortie est un stop.
  6. Synchroniser à la main et vérifier l’absence de redémarrage de pod, pas seulement le statut Synced.
  7. Activer selfHeal dans un commit séparé, une fois le sync validé, en documentant la vérification dans le message de commit.
  8. 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.
k3s-clusterargocdgitopshelmk8smigration