Automatiser le cycle de vie des charts Helm en CI/CD garantit la qualité (lint, tests), la reproductibilité (versioning automatique), et la traçabilité (qui a publié quoi, quand). Ce module présente une pipeline complète avec GitLab CI et GitHub Actions, ainsi que les patterns de promotion dev → staging → prod.
Prérequis
Section intitulée « Prérequis »- Un repo Git contenant vos charts
- Accès à un registry OCI (Harbor, GHCR, ECR)
- Module 10, OCI registries
- Module 11, Provenance (optionnel)
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Architecture d'une pipeline Helm
- Étape 1 : Lint et validation
- Étape 2 : Tests de chart
- Étape 3 : Package et versioning
- Étape 4 : Publish sur registry
- Promotion multi-environnements
- Intégration GitOps
Architecture d'une pipeline Helm
Section intitulée « Architecture d'une pipeline Helm »Un chart Helm est un artefact versionné, au même titre qu'une image de conteneur : il se valide, se construit, se publie, puis se consomme par sa version. La pipeline reprend donc les étapes classiques d'une chaîne de livraison, appliquées à un paquet YAML. Le déploiement, lui, est volontairement séparé : rien n'oblige à ce que la pipeline qui publie le chart soit celle qui l'installe.
Workflow standard
Section intitulée « Workflow standard »Les quatre premières étapes s'enchaînent sans intervention et produisent un chart publié. La cinquième, en pointillés sur le schéma, dépend de votre modèle de déploiement.
Ce que fait chaque étape
Section intitulée « Ce que fait chaque étape »Chaque étape ne valide qu'une chose, et échoue vite si cette chose ne va pas. Ce découpage a une raison pratique : quand la pipeline casse, le nom du job suffit à savoir ce qui est en cause.
| Étape | Outils | Objectif |
|---|---|---|
| Lint | helm lint, ct lint | Syntaxe YAML, bonnes pratiques |
| Test | helm template, kubeval, kubeconform | Manifests K8s valides |
| Package | helm package | Créer le .tgz versionné |
| Publish | helm push | Pousser sur registry OCI |
| Deploy | helm upgrade --install ou GitOps | Déployer sur cluster |
Étape 1 : Lint et validation
Section intitulée « Étape 1 : Lint et validation »Le lint contrôle le chart en tant que paquet : présence et cohérence de Chart.yaml, structure des répertoires, syntaxe des templates, conformité des values au schéma déclaré. Il ne dit rien de la validité Kubernetes des manifestes produits, c'est le rôle de l'étape suivante. C'est le job le plus rapide de la pipeline, à placer en premier pour couper court dès la première erreur.
helm lint (basique)
Section intitulée « helm lint (basique) »Sans --strict, helm lint sort en succès malgré des avertissements ; en CI, c'est ce qui laisse passer les problèmes.
# Lint un charthelm lint ./charts/my-app
# Lint strict (warnings = erreurs)helm lint ./charts/my-app --strictChart Testing (ct), recommandé
Section intitulée « Chart Testing (ct), recommandé »Chart Testing (ct) est l'outil officiel pour tester des charts en CI :
# Installer ctpip install chart-testing
# Lint tous les charts modifiésct lint --config .ct.yaml
# Lint tous les chartsct lint --all --config .ct.yamlConfiguration .ct.yaml :
chart-dirs: - chartsvalidate-maintainers: falsevalidate-chart-schema: truelint-conf: - --strictValidation du schema (values)
Section intitulée « Validation du schema (values) »Un fichier values.schema.json placé à la racine du chart décrit les valeurs attendues, leurs types et celles qui sont obligatoires. Helm l'applique automatiquement, sans option supplémentaire, et refuse un values non conforme.
# Si values.schema.json existehelm lint ./charts/my-app # Valide automatiquement les valuesÉtape 2 : Tests de chart
Section intitulée « Étape 2 : Tests de chart »Un chart peut passer le lint et produire des manifestes que Kubernetes refusera. Cette étape rend les templates hors cluster, puis confronte le YAML obtenu aux schémas de l'API Kubernetes. Elle ne demande aucun accès à un cluster, ce qui la rend exécutable sur n'importe quel runner.
helm template (rendu local)
Section intitulée « helm template (rendu local) »La commande écrit les manifestes rendus sur la sortie standard et renvoie un code de retour non nul en cas d'erreur de template, ce qui suffit à faire échouer le job :
# Générer les manifests sans clusterhelm template my-app ./charts/my-app -f values-test.yaml > rendered.yaml
# Vérifier que ça génère du YAML validehelm template my-app ./charts/my-app > /dev/nullecho $? # 0 = OKValidation des manifests K8s
Section intitulée « Validation des manifests K8s »Deux validateurs consomment le YAML rendu et le comparent aux schémas OpenAPI de Kubernetes. kubeconform est le choix par défaut aujourd'hui : il est maintenu, rapide et gère les ressources personnalisées via des schémas additionnels. kubeval rend le même service mais son dépôt n'est plus actif.
# Installer kubeconform (rapide, maintenu)# https://github.com/yannh/kubeconform
helm template my-app ./charts/my-app | kubeconform -strict -summary# Installer kubeval# https://github.com/instrumenta/kubeval
helm template my-app ./charts/my-app | kubeval --strictTests helm (intégration)
Section intitulée « Tests helm (intégration) »Dernier niveau, le seul qui exige un vrai cluster : Helm installe la release, puis exécute les Pods marqués de l'annotation helm.sh/hook: test et considère le test réussi si leur conteneur sort en code 0. Si votre chart définit des tests dans templates/tests/ :
# Déployer puis testerhelm install my-app ./charts/my-app -n test --waithelm test my-app -n testhelm uninstall my-app -n testÉtape 3 : Package et versioning
Section intitulée « Étape 3 : Package et versioning »Le champ version de Chart.yaml est ce qui identifie le chart sur le registry, et un registry OCI refuse d'écraser une version déjà publiée. Toute la difficulté du packaging tient donc à une seule question : qui décide du numéro. Le champ appVersion, lui, désigne la version de l'application embarquée et évolue indépendamment.
Versioning automatique
Section intitulée « Versioning automatique »Trois approches, du plus simple au plus automatisé. La première repose sur la discipline humaine et finit toujours par produire un oubli ; les deux autres dérivent le numéro de l'historique Git, ce qui le rend reproductible.
Option 1 : Version depuis Chart.yaml (manuelle)
version: 1.2.3 # Incrémenter manuellementappVersion: "2.0.0"Option 2 : Version depuis Git tag
# Récupérer la version depuis le tag GitVERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
# Remplacer dans Chart.yamlsed -i "s/^version:.*/version: $VERSION/" charts/my-app/Chart.yaml
# Packagerhelm package ./charts/my-appOption 3 : Semantic Release
# .releaserc.yaml pour semantic-releaseplugins: - "@semantic-release/commit-analyzer" - "@semantic-release/release-notes-generator" - ["@semantic-release/exec", { "prepareCmd": "sed -i 's/^version:.*/version: ${nextRelease.version}/' charts/my-app/Chart.yaml" }]Package avec métadonnées
Section intitulée « Package avec métadonnées »helm package produit une archive nommée d'après le nom et la version du chart, dans le répertoire courant. C'est ce fichier, et lui seul, qui sera poussé sur le registry :
# Packagerhelm package ./charts/my-app
# Résultat : my-app-1.2.3.tgzÉtape 4 : Publish sur registry
Section intitulée « Étape 4 : Publish sur registry »Depuis Helm 3.8, un chart se publie sur un registry OCI, la même infrastructure que celle qui héberge vos images de conteneurs. Vous réutilisez donc l'authentification, les quotas et les politiques de rétention déjà en place, sans serveur de charts dédié à maintenir.
Push OCI
Section intitulée « Push OCI »Le mot de passe transite par l'entrée standard avec --password-stdin : passé en argument, il apparaîtrait dans la liste des processus et dans les journaux du runner.
# Login (une fois par job)echo "$REGISTRY_PASSWORD" | helm registry login $REGISTRY_URL -u $REGISTRY_USER --password-stdin
# Pushhelm push my-app-1.2.3.tgz oci://$REGISTRY_URL/chartsVérification post-push
Section intitulée « Vérification post-push »Un push réussi ne garantit pas que le chart soit lisible par ses futurs consommateurs, notamment si les droits du dépôt sont restreints. Cette lecture depuis le registry ferme la boucle :
# Vérifier que le chart est accessiblehelm show chart oci://$REGISTRY_URL/charts/my-app --version 1.2.3Pipeline complète GitLab CI
Section intitulée « Pipeline complète GitLab CI »Ce fichier assemble les quatre étapes précédentes en quatre stages. Deux mécanismes structurent l'ensemble : le modèle .helm-base, réutilisé par extends pour ne déclarer l'image qu'une fois, et les blocs rules, qui décident quand chaque job s'exécute. Le lint et les tests tournent sur les merge requests, la publication uniquement sur un tag.
stages: - lint - test - package - publish
variables: HELM_VERSION: "3.14.0" CHART_PATH: "charts/my-app" REGISTRY_URL: "harbor.example.com/charts"
.helm-base: image: alpine/helm:${HELM_VERSION} before_script: - helm version
lint: extends: .helm-base stage: lint script: - helm lint ${CHART_PATH} --strict - helm template test ${CHART_PATH} > /dev/null rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
test: extends: .helm-base stage: test image: alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b before_script: - apk add --no-cache helm curl - curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xz - mv kubeconform /usr/local/bin/ script: - helm template test ${CHART_PATH} | kubeconform -strict -summary rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
package: extends: .helm-base stage: package script: # Version depuis le tag Git ou CI_COMMIT_TAG - | if [ -n "$CI_COMMIT_TAG" ]; then VERSION=$CI_COMMIT_TAG else VERSION="0.0.0-${CI_COMMIT_SHORT_SHA}" fi - sed -i "s/^version:.*/version: $VERSION/" ${CHART_PATH}/Chart.yaml - helm package ${CHART_PATH} artifacts: paths: - "*.tgz" expire_in: 1 week rules: - if: $CI_COMMIT_TAG - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
publish: extends: .helm-base stage: publish script: - echo "${REGISTRY_PASSWORD}" | helm registry login ${REGISTRY_URL} -u ${REGISTRY_USER} --password-stdin - helm push *.tgz oci://${REGISTRY_URL} dependencies: - package rules: - if: $CI_COMMIT_TAGPipeline complète GitHub Actions
Section intitulée « Pipeline complète GitHub Actions »Même logique transposée aux workflows GitHub, avec deux différences notables. Les dépendances entre jobs s'expriment par needs au lieu des stages, et la publication vers GHCR ne réclame aucun secret à créer : le jeton fourni automatiquement au workflow suffit, à condition de lui accorder la permission packages: write.
name: Helm CI/CD
on: push: branches: [main] tags: ['v*'] pull_request: branches: [main]
permissions: {}
env: CHART_PATH: charts/my-app REGISTRY: ghcr.io REGISTRY_PATH: ${{ github.repository_owner }}/charts
jobs: lint: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Set up Helm uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 with: version: '3.14.0'
- name: Lint chart run: | helm lint ${{ env.CHART_PATH }} --strict helm template test ${{ env.CHART_PATH }} > /dev/null
test: runs-on: ubuntu-latest needs: lint permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Set up Helm uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0
- name: Install kubeconform run: | VERSION=v0.6.7 curl -fsSLO "https://github.com/yannh/kubeconform/releases/download/$VERSION/kubeconform-linux-amd64.tar.gz" curl -fsSLO "https://github.com/yannh/kubeconform/releases/download/$VERSION/CHECKSUMS" grep " kubeconform-linux-amd64.tar.gz$" CHECKSUMS | sha256sum -c - tar -xzf kubeconform-linux-amd64.tar.gz kubeconform sudo mv kubeconform /usr/local/bin/
- name: Validate manifests run: helm template test ${{ env.CHART_PATH }} | kubeconform -strict -summary
publish: runs-on: ubuntu-latest needs: test if: startsWith(github.ref, 'refs/tags/v') permissions: contents: read packages: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Set up Helm uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0
- name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update Chart.yaml version run: | sed -i "s/^version:.*/version: ${{ steps.version.outputs.VERSION }}/" ${{ env.CHART_PATH }}/Chart.yaml
- name: Package chart run: helm package ${{ env.CHART_PATH }}
- name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
- name: Push chart run: helm push *.tgz oci://${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}Promotion multi-environnements
Section intitulée « Promotion multi-environnements »Promouvoir, c'est déployer exactement le même artefact d'un environnement au suivant, sans le reconstruire. Un chart repackagé entre la recette et la production n'est plus le chart testé, et la recette ne prouve alors plus rien. Seule la configuration change d'un environnement à l'autre, jamais la version du chart.
Pattern : même chart, values différents
Section intitulée « Pattern : même chart, values différents »Les fichiers de values vivent hors du chart, dans un répertoire dédié. Le chart reste ainsi générique et publiable tel quel, tandis que les particularités d'environnement restent visibles et relisibles dans le dépôt.
Répertoirecharts/
Répertoiremy-app/
- Chart.yaml
- values.yaml Defaults
Répertoiretemplates/
- …
Répertoirevalues/
- values-dev.yaml Dev overrides
- values-staging.yaml
- values-prod.yaml
Déploiement par environnement
Section intitulée « Déploiement par environnement »Les trois commandes sont identiques à deux détails près : le fichier de values et le namespace. Le --version reste le même partout, c'est précisément ce qui fait la promotion. La forme upgrade --install installe la release si elle n'existe pas encore et la met à jour sinon, ce qui rend la commande rejouable :
# Dev (automatique sur main)helm upgrade --install my-app oci://registry/charts/my-app \ --version 1.2.3 \ -f values/values-dev.yaml \ -n dev
# Staging (manuel ou tag)helm upgrade --install my-app oci://registry/charts/my-app \ --version 1.2.3 \ -f values/values-staging.yaml \ -n staging
# Prod (approval + tag)helm upgrade --install my-app oci://registry/charts/my-app \ --version 1.2.3 \ -f values/values-prod.yaml \ -n prodPipeline avec gates
Section intitulée « Pipeline avec gates »La mise en production ne doit pas se déclencher toute seule. Le mot-clé when: manual transforme le job en bouton à cliquer dans l'interface, et le bloc environment fait apparaître la release dans le suivi des environnements GitLab avec son historique de déploiements.
# GitLab CI - promotion avec approvaldeploy-staging: stage: deploy script: - helm upgrade --install my-app oci://${REGISTRY}/my-app --version ${VERSION} -f values/values-staging.yaml -n staging environment: name: staging rules: - if: $CI_COMMIT_TAG
deploy-prod: stage: deploy script: - helm upgrade --install my-app oci://${REGISTRY}/my-app --version ${VERSION} -f values/values-prod.yaml -n prod environment: name: production when: manual # Approval requis rules: - if: $CI_COMMIT_TAGIntégration GitOps
Section intitulée « Intégration GitOps »Dans les pipelines précédentes, c'est le runner de CI qui détient les accès au cluster et pousse les changements. L'approche GitOps inverse ce sens : un agent installé dans le cluster surveille une source de vérité et applique lui-même l'état déclaré. La CI s'arrête alors à la publication du chart, et plus aucun identifiant de cluster ne circule dans les variables du pipeline.
Argo CD avec Helm
Section intitulée « Argo CD avec Helm »Argo CD consomme le chart directement depuis le registry OCI : le champ targetRevision porte la version du chart, et syncPolicy.automated avec selfHeal ramène le cluster à l'état déclaré si quelqu'un le modifie à la main.
# Application Argo CDapiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: my-app namespace: argocdspec: project: default source: repoURL: oci://harbor.example.com/charts chart: my-app targetRevision: 1.2.3 helm: valueFiles: - values-prod.yaml destination: server: https://kubernetes.default.svc namespace: prod syncPolicy: automated: prune: true selfHeal: trueFlux avec HelmRelease
Section intitulée « Flux avec HelmRelease »Flux exprime la même intention avec une ressource HelmRelease, qui référence une source déclarée séparément. Le champ interval fixe la fréquence de réconciliation, ici toutes les cinq minutes.
# HelmRelease FluxapiVersion: helm.toolkit.fluxcd.io/v2kind: HelmReleasemetadata: name: my-app namespace: prodspec: interval: 5m chart: spec: chart: my-app version: "1.2.3" sourceRef: kind: HelmRepository name: my-charts namespace: flux-system values: replicaCount: 3 # ... prod valuesLab C3, Pipeline CI/CD complète
Section intitulée « Lab C3, Pipeline CI/CD complète »Ce lab met bout à bout tout ce qui précède sur un chart généré par helm create, donc valide dès le départ : l'objet de l'exercice est la pipeline, pas le chart. Comptez une trentaine de minutes, dont l'essentiel passe dans la configuration des accès au registry.
Objectif : Créer une pipeline qui lint, package et publie un chart sur push vers main ou tag.
-
Structure du repo
Fenêtre de terminal mkdir helm-cicd-lab && cd helm-cicd-labgit init# Créer un chartmkdir -p chartshelm create charts/my-app# Créer les values par envmkdir -p valuescp charts/my-app/values.yaml values/values-dev.yamlcp charts/my-app/values.yaml values/values-prod.yaml -
Créer la pipeline (GitLab ou GitHub selon votre environnement)
Copier le fichier
.gitlab-ci.ymlou.github/workflows/helm.ymlci-dessus. -
Configurer les secrets
-
GitLab : Settings → CI/CD → Variables
REGISTRY_URLREGISTRY_USERREGISTRY_PASSWORD
-
GitHub : Settings → Secrets and variables → Actions
GITHUB_TOKENest automatique pour GHCR
-
-
Pousser et déclencher
Fenêtre de terminal git add .git commit -m "feat: initial chart"git push origin main# Créer un tag pour publishgit tag v0.1.0git push origin v0.1.0 -
Vérifier
- Pipeline lint/test passe
- Chart publié sur le registry
helm show chart oci://registry/charts/my-app --version 0.1.0
Critères de réussite :
- Pipeline déclenchée sur push
-
helm lintexécuté sans erreur - Manifests validés avec kubeconform
- Chart packagé et versionné depuis le tag
- Publication automatique vers registry OCI
À retenir
Section intitulée « À retenir »- Lint :
helm lint --strict+ct lintpour la qualité - Test :
helm template+kubeconformpour valider les manifests - Package : versioning automatique depuis Git tag
- Publish :
helm pushvers registry OCI - Promotion : même chart, values différents par environnement
- GitOps : Argo CD / Flux pour les déploiements prod (recommandé)