Aller au contenu
Hooks et tests

Hooks et tests

200 Pratiquer ⏱ 1 h 15 helmkubernetesargocd

À la fin, vous saurez

  • Annoter une ressource comme hook Helm et prévoir sa place dans l'ordre d'une installation ou d'une mise à jour
  • Écrire un Job de migration en hook pre-upgrade, avec sa politique de suppression, et expliquer ce qui se passe quand il échoue
  • Distinguer ce que --wait, --atomic et --timeout couvrent, et ce qu'ils ne couvrent pas
  • Prévoir la traduction des hooks Helm en hooks Argo CD, et ses limites
  • Écrire un pod de test pour helm test, et contrôler un chart sans cluster avec helm lint --strict et helm template

Prérequis

Testé avec argocd 3.5 helm 3.16.3 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

Un déploiement n'est pas qu'une suite d'objets à créer. Signalements a une base de données, et la version 1.3.0 de l'application attend une colonne que la 1.2.0 ne connaît pas. Quelqu'un doit exécuter la migration du schéma, une fois, avant que les nouveaux pods ne démarrent. On pourrait la lancer à la main, ou depuis la CI, mais cela sort de l'application une étape qui fait partie de son déploiement, et qu'on oublie le jour où l'on est pressé.

Helm propose pour cela les hooks : des ressources du chart (le plus souvent des Jobs) que Helm exécute à des moments précis du cycle de vie d'une release, au lieu de les traiter comme des objets ordinaires. Mais un hook n'est pas un objet ordinaire, et c'est là que les surprises commencent : il n'est pas suivi par la release, il bloque le déploiement pendant qu'il tourne, et il n'est annulé par rien si la suite échoue. Une migration de base de données ne se « défait » pas avec un retour arrière de Helm.

La seconde moitié de la leçon traite la question symétrique : comment savoir qu'un chart est bon avant de le publier ? Helm propose helm test (qui exige un cluster), helm lint (qui n'en exige pas), et l'écosystème ajoute des tests de rendu et des outils de validation. On les empile du moins coûteux au plus complet.

Les sorties de cette leçon viennent de helm template et helm lint, exécutés sur le chart signalements de la leçon 6 (version 0.3.0). Tout ce qui demande un cluster (installer, tester, voir un Job échouer) est décrit d'après la documentation, sans sortie inventée.

Les concepts

Qu'est-ce qu'un hook

Un hook Helm est une ressource Kubernetes ordinaire (Job, Pod, ConfigMap...) portant l'annotation helm.sh/hook, dont la valeur nomme le ou les moments où Helm doit la créer. Neuf valeurs existent, d'après la documentation de Helm :

ValeurMoment
pre-installaprès le rendu des modèles, avant qu'aucune ressource de la release ne soit créée
post-installaprès que toutes les ressources sont chargées dans Kubernetes
pre-upgradeà une mise à jour, après le rendu, avant qu'aucune ressource ne soit mise à jour
post-upgradeà une mise à jour, après que toutes les ressources ont été mises à jour
pre-rollback, post-rollbackavant ou après un retour arrière (helm rollback)
pre-delete, post-deleteavant la suppression de la première ressource, ou après la dernière (helm uninstall)
testà l'appel de helm test

On peut combiner plusieurs valeurs avec une virgule (pre-install,pre-upgrade), ce que fait le Job de migration de Signalements. Le rendu confirme :

$ helm template s signalements --show-only templates/migration-job.yaml | grep -A3 annotations
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation

Le déroulé

Pour une mise à jour (helm upgrade), Helm fait dans cet ordre :

  1. rendre tous les modèles du chart ;
  2. créer les hooks pre-upgrade, et attendre qu'ils soient terminés ;
  3. mettre à jour les ressources ordinaires de la release ;
  4. créer les hooks post-upgrade, et attendre qu'ils soient terminés ;
  5. marquer la release comme deployed.

Si un hook échoue, l'opération s'arrête et la release passe à l'état failed. Pour un Job, « terminé » veut dire que le Job a abouti ; pour un Pod, qu'il a atteint la phase Succeeded. Les autres types de ressources sont considérés comme prêts dès leur création.

Poids et politiques de suppression

Quand plusieurs hooks répondent au même moment, l'annotation helm.sh/hook-weight fixe l'ordre : les poids sont triés par ordre croissant, de la valeur la plus négative à la plus positive, 0 par défaut. Le poids est écrit entre guillemets : la documentation dit que ce sont des nombres représentés par des chaînes. À poids égal, Helm trie par type de ressource puis par nom.

L'annotation helm.sh/hook-delete-policy décide quand Helm supprime la ressource-hook :

PolitiqueEffet
before-hook-creation (défaut)supprime la ressource précédente avant de relancer le hook ; la dernière exécution reste donc visible
hook-succeededsupprime la ressource après un succès
hook-failedsupprime la ressource après un échec

Elles se combinent : before-hook-creation,hook-succeeded garde les journaux d'un échec, nettoie après un succès, et évite la collision de noms à la prochaine exécution.

Un hook n'appartient pas à la release

Voilà ce qui distingue un hook d'un objet ordinaire : Helm le crée, mais ne le suit pas dans le manifeste de la release. Il n'apparaît pas dans helm get manifest, ne fait pas partie de ce que helm uninstall supprime (sauf politique de suppression), et un retour arrière ne le défait pas. Un Job de migration terminé reste donc dans l'espace de noms jusqu'à la prochaine exécution, ou jusqu'à ce que sa politique le supprime.

--wait, --atomic, --timeout

Trois options de helm install et helm upgrade interagissent avec les hooks.

  • --wait : après avoir appliqué les ressources, Helm attend qu'elles soient prêtes (Deployments disponibles, Services avec des points de terminaison, etc.) avant de marquer la release comme réussie. D'après la documentation des hooks, avec --wait, les hooks post-* ne s'exécutent qu'une fois les ressources prêtes. --wait-for-jobs ajoute l'attente de la fin des Jobs de la release.
  • --timeout (5 minutes par défaut) : « temps d'attente pour chaque opération Kubernetes individuelle », y compris l'attente de la fin d'un hook. Un Job de migration qui dure dix minutes fait échouer l'opération si le délai n'est pas relevé.
  • --atomic : si l'opération échoue, Helm revient automatiquement à la révision précédente (implique --wait). Helm 4 le renomme --rollback-on-failure. Sur Helm 4, --wait accepte aussi plusieurs stratégies : watcher (par défaut), hookOnly et legacy, d'après le journal des changements. Le comportement détaillé de ces stratégies dépasse cette leçon ; si vous passez à Helm 4, relisez les notes de version avant de changer vos pipelines.

Les tests

Un test Helm est un hook de valeur test, le plus souvent un Pod qui exécute une vérification et sort avec le code 0 si tout va bien. Il se trouve par convention dans templates/tests/. helm test <release> crée ces pods dans le cluster, attend leur fin, et signale les échecs. Il ne se lance donc qu'après l'installation, sur une release existante : c'est un test de fumée de l'installation, pas un test de chart.

En pratique

Le Job de migration de Signalements

Le fichier templates/migration-job.yaml du chart :

{{- if .Values.migration.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "signalements.fullname" . }}-migration
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  backoffLimit: 1
  activeDeadlineSeconds: 300
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migration
          image: {{ include "signalements.image" . | quote }}
          command: {{ toJson .Values.migration.command }}
          {{- if .Values.externalSecret.enabled }}
          envFrom:
            - secretRef:
                name: {{ include "signalements.fullname" . }}-db
          {{- end }}
{{- end }}

Les points qui comptent :

  • La même image que l'application, par signalements.image : la migration est celle de la version qu'on déploie, jamais d'une autre.
  • restartPolicy: Never et backoffLimit: 1 : un Job de migration qui échoue ne doit pas se relancer indéfiniment ; un seul nouvel essai, puis l'échec est signalé.
  • activeDeadlineSeconds: 300 : une borne dure sur la durée totale. Sans elle, une migration bloquée sur un verrou de base de données attend jusqu'à l'expiration du --timeout de Helm, qui ne la tue pas pour autant. Gardez les deux cohérents : la borne du Job plus courte que --timeout, pour que l'échec vienne du Job (avec un message) et non de Helm.
  • La même source de DATABASE_URL que l'application : le secret <release>-signalements-db, produit par l'ExternalSecret.
  • before-hook-creation : le Job de la mise à jour précédente est supprimé avant que le nouveau soit créé, ce qui évite l'erreur de nom déjà existant et laisse le dernier Job consultable (kubectl logs job/...) en cas d'échec.

Voir ce que Helm crée, et ne crée pas

helm template rend les hooks comme le reste. Deux options de tri :

$ helm template s signalements | grep -E "^kind:|helm.sh/hook\""
kind: Service
kind: Deployment
kind: ExternalSecret
kind: Pod
    "helm.sh/hook": test
kind: Job
    "helm.sh/hook": pre-install,pre-upgrade
$ helm template s signalements --no-hooks | grep "^kind:"
kind: Service
kind: Deployment
kind: ExternalSecret
$ helm template s signalements --skip-tests | grep -E "^kind:|helm.sh/hook\""
kind: Service
kind: Deployment
kind: ExternalSecret
kind: Job
    "helm.sh/hook": pre-install,pre-upgrade

--no-hooks retire tous les hooks, --skip-tests retire seulement ceux de type test. Sur install et upgrade, --no-hooks est le moyen de court-circuiter une migration, par exemple pour un incident où le Job lui-même est en cause : à utiliser en connaissance de cause, puisque l'application se retrouve alors face à un schéma qui peut ne pas lui convenir.

Un déroulé de mise à jour, en prose

Sur un cluster, helm upgrade signalements ./signalements --wait --timeout 8m aurait cette séquence, d'après la documentation. Helm rend le chart. Il supprime le Job signalements-migration de l'exécution précédente (before-hook-creation), crée le nouveau, et attend qu'il se termine. Si le Job réussit, Helm met à jour le Deployment et le Service, attend que le Deployment soit disponible (--wait), et marque la révision deployed. Si le Job échoue, rien d'autre n'a été touché : les pods de la version 1.2.0 tournent toujours, avec l'ancien Deployment ; la release est marquée failed, et le Job échoué reste dans l'espace de noms, avec ses journaux.

C'est la qualité d'un hook pre-upgrade : l'échec de la migration empêche le déploiement de la nouvelle version. Mais cela suppose que la migration est compatible avec l'ancienne version de l'application.

Migrations compatibles : élargir, puis contracter

Pendant l'exécution du Job pre-upgrade, les anciens pods tournent encore, et ils continuent de tourner pendant la mise à jour progressive du Deployment qui suit. Un schéma de base de données doit donc être valable pour deux versions de l'application à la fois. La technique est connue sous le nom d'expand and contract : on ne modifie jamais en une étape un schéma de façon incompatible.

  1. Version N+1, migration « élargir » : ajouter la nouvelle colonne, nullable ou avec un défaut ; l'ancienne version l'ignore, la nouvelle l'utilise.
  2. Version N+2, migration « contracter » : une fois que plus aucun pod de la version N ne tourne, supprimer l'ancienne colonne.

Renommer une colonne en une seule migration est interdit par cette règle : l'ancienne version, encore en train de servir, échouerait à la prochaine requête. C'est une contrainte de conception de l'application, pas de Helm, mais c'est Helm (par l'ordre pre-upgrade, puis mise à jour progressive) qui la rend indispensable.

Tester le chart : helm test

Le pod de test de Signalements :

apiVersion: v1
kind: Pod
metadata:
  name: {{ include "signalements.fullname" . }}-test
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: sante
      image: {{ include "signalements.image" . | quote }}
      command: ["python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://{{ include "signalements.fullname" . }}:{{ .Values.service.port }}/sante', timeout=5).status == 200 else 1)"]

Il appelle GET /sante par le Service, depuis l'intérieur du cluster, avec l'image de l'application, qui contient Python mais pas forcément curl : tester ce que l'on a sous la main plutôt qu'ajouter une image. Il sort avec le code 0 si la réponse est 200, 1 sinon. Sur un cluster, la commande est :

$ helm test signalements --logs

--logs affiche la sortie des pods de test. La documentation de Helm donne trois cas qui valent d'être testés : que la configuration injectée par les valeurs est bien celle attendue, que l'authentification fonctionne (identifiants valides et invalides), et que les services répondent. Ne mettez pas de logique métier dans un test de chart : il vérifie que la release est saine, pas que l'application est correcte (c'est le rôle de ses propres tests).

Contrôler un chart sans cluster

Avant d'avoir un cluster, trois contrôles bon marché.

helm lint détecte les erreurs de structure et de rendu. Un modèle qui lit une valeur inexistante échoue :

$ helm lint w3 --strict
==> Linting w3
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/z.yaml: template: signalements/templates/z.yaml:1:13: executing "signalements/templates/z.yaml" at <.Values.inconnue.cle>: nil pointer evaluating interface {}.cle

Ici w3 est une copie du chart avec un modèle z.yaml qui lit .Values.inconnue.cle. De la même façon, une version 0.3 non quotée dans Chart.yaml donne [ERROR] Chart.yaml: version should be of type string but it's of type float64.

helm lint --strict transforme les avertissements en échecs. Sans --strict, un nom d'objet invalide laisse passer le lint avec un avertissement et un code de sortie 0 :

$ helm lint w4
==> Linting w4
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/service.yaml: object name does not conform to Kubernetes naming requirements: "Signalements_test-release": metadata.name: Invalid value: "Signalements_test-release": a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name',  or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')

1 chart(s) linted, 0 chart(s) failed
$ helm lint w4 --strict
...
Error: 1 chart(s) linted, 1 chart(s) failed

(Ici w4 est une copie du chart dont le nom du Service a été volontairement rendu invalide.) Les messages [INFO] ne comptent pas : icon is recommended n'empêche pas un --strict de réussir. Mettez --strict en CI.

helm template avec chaque fichier de valeurs rend les manifestes sans cluster. On le complète d'un validateur de schémas Kubernetes (par exemple kubeconform, helm template ... | kubeconform -strict), qui vérifie que les objets rendus sont valides pour la version de l'API visée. Seul, helm lint ne connaît pas les schémas des ressources : un champ mal orthographié dans un Deployment passe le lint et échoue à l'installation.

Des tests de rendu : helm-unittest

L'extension helm-unittest (greffon Helm, hébergé dans l'organisation helm-unittest) teste le rendu : on écrit des fichiers tests/*_test.yaml qui disent, pour des valeurs données, ce que tel modèle doit contenir. Voici la forme d'un test pour Signalements, à titre d'illustration (il n'a pas été exécuté dans cette leçon) :

suite: migration
templates:
  - templates/migration-job.yaml
tests:
  - it: ne rend rien quand la migration est désactivée
    set:
      migration.enabled: false
    asserts:
      - hasDocuments:
          count: 0
  - it: utilise la même image que l'application
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: ghcr.io/lyneko-formation/signalements:1.2.0

Ces tests s'exécutent en une seconde, sans cluster, et protègent contre les régressions de rendu : un if oublié, une valeur qui ne passe plus. Ils sont particulièrement utiles pour les charts de bibliothèque (leçon 7), dont une modification touche tous les consommateurs.

Tester sur un vrai cluster : chart-testing

L'outil chart-testing (ct, projet helm/chart-testing) automatise ce qui précède dans la CI d'un dépôt de charts : ct lint valide les charts modifiés (Chart.yaml, versions incrémentées, lint), et ct install les installe dans un cluster éphémère (typiquement un cluster kind créé par la CI) puis lance leurs tests. C'est le complément qui valide que le chart s'installe réellement, ce que ni le lint ni le rendu ne garantissent.

Sous le capot

Où vivent les hooks. À l'installation, Helm sépare les manifestes rendus en deux groupes : les ressources ordinaires, qu'il enregistre dans le secret de release (type helm.sh/release.v1, un par révision), et les hooks, qu'il exécute puis oublie. C'est pourquoi un hook n'apparaît pas dans helm get manifest et n'est pas supprimé par helm uninstall : il n'est pas dans la liste des ressources de la release. helm get hooks <release> les affiche à part.

Comment Helm attend un Job. Après avoir créé un hook, Helm surveille la ressource jusqu'à ce qu'elle soit « prête » au sens de Helm : pour un Job, qu'il ait réussi ; pour un Pod, qu'il soit Succeeded. Le temps d'attente est borné par --timeout. En cas de dépassement, Helm abandonne et marque la révision failed, sans tuer le Job : c'est pourquoi activeDeadlineSeconds sur le Job lui-même est le bon outil pour borner l'exécution.

Ce que --atomic ne défait pas. Un retour arrière automatique rétablit la révision précédente de la release, c'est-à-dire les manifestes ordinaires de l'ancienne version. Il ne défait pas ce que les hooks ont fait hors de Kubernetes : une migration de base de données exécutée reste exécutée. Si la mise à jour échoue après la migration (le nouveau Deployment ne devient jamais disponible), le retour arrière remet l'ancienne version de l'application devant un schéma que la nouvelle a déjà modifié. D'où la règle de compatibilité plus haut : la migration doit laisser l'ancienne version fonctionner, précisément parce que le retour arrière ne la défait pas. Les hooks pre-rollback et post-rollback existent pour y brancher une action inverse, mais défaire une migration de données est rarement sûr.

L'ordre de création au premier déploiement. pre-install s'exécute « avant qu'aucune ressource ne soit créée » : à la première installation, l'ExternalSecret, le Secret qu'il produit et le Deployment n'existent pas encore quand le Job démarre. Le Job de Signalements référence <release>-signalements-db : sur une installation initiale, ce Secret n'existe pas, et le pod du Job reste en CreateContainerConfigError jusqu'à l'expiration de activeDeadlineSeconds. C'est une conséquence directe de la définition du hook, et non une anomalie. Les solutions, avec leur prix :

  1. Fournir le secret avant le chart : créer l'ExternalSecret dans une release à part, plus tôt (c'est ce que fait une plateforme dont les secrets sont gérés à part, voir GitOps avec Argo CD, leçon 8). Le Job ne dépend alors de rien dans sa propre release.
  2. Faire de l'ExternalSecret lui-même un hook (pre-install,pre-upgrade, poids plus bas que celui du Job, par exemple "-10"). Il est alors créé avant la migration, mais il sort de la release : helm uninstall ne le supprime plus.
  3. Réserver pre-upgrade à la migration et lancer la migration initiale (pre-install) à la main une fois, ou par un post-install dont le prix est que l'application démarre avant son schéma.

Aucune n'est gratuite : choisissez en connaissance de ce que vous perdez, et gardez la même logique pour toute dépendance du Job (un ConfigMap, un compte de service).

Pièges courants

Un hook qui bloque l'upgrade sans fin. Un Job de migration sans activeDeadlineSeconds, bloqué sur un verrou, attend jusqu'au délai de Helm : l'opération échoue au bout de --timeout, la release est failed, et le Job tourne encore. Bornez le Job.

before-hook-creation retiré. Sans politique, le Job de l'exécution précédente existe encore : la création du suivant échoue (jobs.batch "..." already exists). La valeur par défaut protège contre cela ; ne la remplacez pas par hook-succeeded seul, car un échec laisse alors le Job en place et bloque la tentative suivante.

Le Job qui ne peut pas démarrer. Les ressources dont le Job dépend (Secret, ConfigMap, compte de service) doivent exister avant lui. À l'installation, vu ci-dessus. À la mise à jour, une dépendance nouvelle ajoutée dans la même révision n'existe pas encore non plus.

Une migration non idempotente. Un hook relancé (échec, nouvelle tentative, --force) doit pouvoir s'exécuter plusieurs fois sans dommage. Les outils de migration qui tiennent un registre des migrations appliquées le font, un script SQL à la main non.

Plusieurs répliques qui migrent en même temps. Le Job est unique, c'est son intérêt. Une migration lancée par un conteneur d'initialisation du Deployment s'exécute dans chaque pod : deux pods qui migrent en parallèle se disputent le verrou. Gardez un Job.

Oublier que helm test demande une release. helm test sans release installée échoue (release: not found). On ne l'utilise pas pour tester un chart « avant ». Il valide une installation.

--wait sans sonde. --wait attend que les pods soient prêts : sans sonde de disponibilité, un pod est prêt dès que le conteneur tourne. Le déploiement est « réussi » alors que l'application ne répond pas. La sonde sur /sante du chart est ce qui donne un sens à --wait.

Tests qui dépendent d'une image absente. Un pod de test qui utilise une image que le cluster ne peut pas tirer reste en ImagePullBackOff et le test échoue pour une raison étrangère à l'application. Utilisez l'image de l'application, que le cluster tire déjà.

Sécurité

Un hook s'exécute avec des droits. Le Job tourne avec le compte de service que vous lui donnez, et a accès au Secret de la base. C'est un identifiant à privilèges élevés (il peut modifier le schéma) hébergé dans un pod éphémère. Donnez-lui son propre compte de service, sans droit sur l'API Kubernetes (automountServiceAccountToken: false), et, si le fournisseur le permet, un utilisateur de base distinct de celui de l'application : celui qui migre a les droits de modifier le schéma, celui qui sert n'en a besoin que pour lire et écrire les données.

Une migration est du code arbitraire. Elle se relit comme le reste du dépôt. Un hook posé par un sous-chart tiers (leçon 7) s'exécute lui aussi avec les droits de la release : rendez le chart avec helm template et cherchez les annotations helm.sh/hook avant d'ajouter une dépendance.

Les mêmes durcissements que l'application. Le pod du Job a le même securityContext que l'application (utilisateur non privilégié, système de fichiers en lecture seule si possible) : un hook oublié de ces règles est le maillon faible.

Ne rien faire de secret dans un test. Les journaux de helm test --logs finissent dans la CI. Un test qui affiche une chaîne de connexion les y publie.

Qui peut lancer helm upgrade peut lancer un hook. Quiconque a le droit de mettre à jour une release peut y faire exécuter un Job avec les droits d'un compte de service de l'espace de noms. Sous GitOps, ce droit est celui qui peut fusionner dans le dépôt (voir la leçon 10) : la revue du dépôt est le contrôle d'accès des hooks.

En production

Hooks et Argo CD : la traduction. Argo CD ne fait pas helm install : il rend le chart avec helm template et applique le résultat lui-même (GitOps avec Argo CD, leçon 5). Les hooks Helm sont donc traduits en hooks Argo CD. D'après la documentation d'Argo CD :

Hook HelmHook Argo CD
pre-install, pre-upgradePreSync
post-install, post-upgradePostSync
pre-deletePreDelete
helm.sh/hook-weightargocd.argoproj.io/sync-wave

Et trois restrictions, toujours d'après cette documentation : test-success, test-failure, pre-rollback et post-rollback sont ignorés ; et si vous définissez un seul hook Argo CD dans le chart, tous les hooks Helm sont ignorés. Il n'y a pas de mélange.

Conséquences pour Signalements. Le Job pre-install,pre-upgrade devient un hook PreSync : il s'exécute avant les autres ressources, à chaque synchronisation, y compris à la première (où il joue le rôle de pre-install) : la distinction entre installation et mise à jour disparaît, puisqu'il n'y a pas de release. Le même Job doit donc être idempotent et convenir aux deux cas. Le pod de test (helm.sh/hook: test) n'est pas exécuté : helm test n'a pas de sens sans release Helm. Pour obtenir un test de fumée sous Argo CD, écrivez-le en hook PostSync (comme dans le cours GitOps), ou séparez-le du chart et exécutez-le depuis la CI. Les politiques de suppression existent des deux côtés avec des noms voisins (before-hook-creation côté Helm, BeforeHookCreation côté Argo CD) ; vérifiez dans la documentation d'Argo CD la correspondance exacte pour votre version avant de vous y fier. Enfin, les attentes de --wait n'existent pas : c'est la santé des ressources, évaluée par Argo CD, qui conditionne les vagues et la phase suivante.

Un chart destiné uniquement à Argo CD peut donc déclarer directement des hooks Argo CD (annotation argocd.argoproj.io/hook), avec plus de contrôle (vagues, phases SyncFail) ; un chart destiné aux deux mondes reste en hooks Helm, en acceptant les limites ci-dessus.

Les migrations à l'échelle. Passé quelques minutes, une migration ne doit plus bloquer un déploiement : on la sépare en un Job lancé à part, avec son propre suivi, et le chart ne fait que vérifier qu'elle a eu lieu (une sonde, un initContainer qui attend la version de schéma attendue). Pour les gros volumes, on découpe les migrations de données en lots, exécutés hors du chemin du déploiement. Le principe « élargir, puis contracter » reste la règle.

Le retour arrière. Rendez le retour arrière possible par construction, pas par bonne volonté : migrations compatibles avec N-1, aucune suppression de colonne dans la même version que celle qui cesse de l'utiliser, et un helm rollback ou un revert Git qui ramène l'application sans toucher au schéma.

Les tests, par couches. Dans la CI d'un dépôt de charts : helm lint --strict (secondes), helm template avec chaque fichier de valeurs réellement déployé, un validateur de schémas Kubernetes, les tests unitaires de rendu, puis ct install sur un cluster éphémère pour les charts modifiés. Dans le cluster : helm test après une installation, quand Helm gère la release.

Exercices

Exercice 1 : l'ordre

Un chart a trois hooks pre-upgrade : un Job sauvegarde (poids "-5"), un Job migration (poids "0", c'est la valeur par défaut), un Job verification (poids "5"). Dans quel ordre Helm les exécute-t-il, et quand met-il à jour le Deployment ? Que se passe-t-il si sauvegarde échoue ?

Solution

Les poids sont triés par ordre croissant : sauvegarde (-5), puis migration (0), puis verification (5). Chacun est attendu jusqu'à sa fin avant le suivant. Le Deployment (ressource ordinaire) n'est mis à jour qu'après les trois hooks pre-upgrade. Si sauvegarde échoue, Helm s'arrête : ni la migration, ni la vérification, ni la mise à jour du Deployment ne sont exécutées, la release est failed, et l'ancienne version continue de tourner. C'est la raison d'être d'une sauvegarde en pre-upgrade : l'échec empêche la migration qui la suit.

Exercice 2 : les pièges d'une première installation

Le Job de migration de Signalements est en pre-install,pre-upgrade et lit le Secret <release>-signalements-db, produit par l'ExternalSecret du même chart. Décrivez ce qui se passe sur une première installation, puis proposez deux corrections et leur inconvénient.

Solution

Au pre-install, aucune ressource de la release n'est encore créée : le Secret n'existe pas. Le pod du Job ne peut pas résoudre envFrom.secretRef et reste bloqué (CreateContainerConfigError), jusqu'à ce que activeDeadlineSeconds (300 s) fasse échouer le Job, et Helm marque la release failed. Correction 1 : créer l'ExternalSecret dans une release séparée, déployée plus tôt ; inconvénient : un chart de plus à ordonnancer, et le chart n'est plus installable seul. Correction 2 : annoter l'ExternalSecret en hook de poids plus bas que le Job ; inconvénient : il sort du manifeste de la release, et helm uninstall ne le supprime plus. Une troisième voie, la migration en post-install, fait démarrer l'application avant son schéma, et peut interdire à --wait de réussir si la sonde dépend de la base.

Exercice 3 : sous Argo CD

Le chart de Signalements est déployé par Argo CD. Qu'advient-il de (a) son Job de migration pre-install,pre-upgrade, (b) son pod helm.sh/hook: test, (c) le même chart si vous ajoutez un seul hook argocd.argoproj.io/hook: PostSync pour un test de fumée ?

Solution

(a) Il est traduit en hook PreSync : il s'exécute avant les autres ressources à chaque synchronisation (la première comprise) ; il doit donc être idempotent. (b) Il est ignoré : les hooks test-success et test-failure ne sont pas pris en charge ; helm test n'a pas de sens sous Argo CD. (c) Dès qu'un hook Argo CD est défini dans le chart, tous les hooks Helm sont ignorés : le Job de migration cesse d'être un hook, et serait appliqué comme une ressource ordinaire. Il faut alors aussi l'annoter en PreSync avec argocd.argoproj.io/hook.

Récapitulatif

  • Un hook est une ressource annotée helm.sh/hook que Helm exécute à un moment du cycle de vie (pre-install, post-upgrade, test...), ordonnée par helm.sh/hook-weight (chaîne, ordre croissant) et nettoyée selon helm.sh/hook-delete-policy (before-hook-creation par défaut).
  • Un hook bloque l'opération pendant qu'il tourne, n'est pas suivi par la release, et n'est pas défait par un retour arrière : une migration reste faite. D'où les migrations compatibles avec la version précédente (élargir, puis contracter).
  • Un Job de migration est borné (activeDeadlineSeconds, backoffLimit), idempotent, utilise la même image que l'application, et a ses propres droits restreints. pre-install s'exécute avant tout autre objet de la release : ses dépendances doivent exister avant.
  • --wait attend la disponibilité des ressources, --timeout borne l'attente (5 minutes par défaut), --atomic (Helm 4 : --rollback-on-failure) revient en arrière, mais ne défait pas ce que les hooks ont fait hors du cluster.
  • Sous Argo CD, pre-install et pre-upgrade deviennent PreSync, les poids deviennent des vagues, les hooks de test et de retour arrière sont ignorés, et un seul hook Argo CD désactive tous les hooks Helm.
  • helm test valide une installation existante. Sans cluster : helm lint --strict (avertissements = échecs), helm template sur chaque fichier de valeurs, un validateur de schémas, helm-unittest ; ct install sur un cluster éphémère pour la vérité.

Pour aller plus loin

Voir ma constellation →

Sources