12 — Helm : usage avancé#

Le livre Docker (chapitre 17) a couvert les bases de Helm : installation de charts, helm install / upgrade / rollback, templates, values et secrets basiques. Ce chapitre approfondit les mécanismes avancés qui font de Helm un outil de déploiement fiable à l’échelle : dépendances de charts, hooks de cycle de vie, tests automatisés, registres OCI et outillage complémentaire comme Helmfile.

Dépendances de charts#

Un chart peut déclarer des dépendances vers d’autres charts (sous-charts). Cela permet d’empaqueter une application avec ses services tiers (base de données, cache, broker) dans une unité deployable cohérente.

# Chart.yaml
apiVersion: v2
name: myapp
description: Application principale avec ses dépendances
type: application
version: 1.4.2
appVersion: "2.1.0"

dependencies:
  - name: postgresql
    version: "~15.2.0"          # plage sémantique : 15.2.x
    repository: oci://registry-1.docker.io/bitnamicharts
    condition: postgresql.enabled  # désactivable via values

  - name: redis
    version: "~19.0.0"
    repository: oci://registry-1.docker.io/bitnamicharts
    condition: redis.enabled

  - name: myapp-common
    version: "~1.0.0"
    repository: oci://ghcr.io/myorg/charts
    alias: common               # alias pour éviter les conflits de noms
# Télécharger et verrouiller les dépendances
helm dependency update ./myapp

# Résultat : Chart.lock (fichier de verrouillage des versions exactes)
# et charts/ (dossier contenant les .tgz des sous-charts)

Chart.lock joue le même rôle que package-lock.json ou Cargo.lock : il épingle les versions exactes résolues, garantissant la reproductibilité entre environnements.

Dépendances conditionnelles

Les conditions (condition: postgresql.enabled) permettent de désactiver les sous-charts selon l’environnement. En staging, on peut activer PostgreSQL embarqué ; en prod, le chart se connecte à une instance RDS externe et postgresql.enabled: false supprime le déploiement du sous-chart.

Hooks Helm#

Les hooks Helm permettent d’exécuter des pods à des moments précis du cycle de vie d’une release : avant l’installation, après une mise à jour, avant la suppression, etc.

# templates/job-db-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ .Release.Name }}-db-migrate"
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"          # ordre parmi les hooks (plus petit = premier)
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["python", "manage.py", "migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: "{{ .Release.Name }}-db-secret"
                  key: url

Hooks disponibles#

Hook

Moment d’exécution

pre-install

Avant la création des ressources de la release

post-install

Après la création de toutes les ressources

pre-upgrade

Avant la mise à jour des ressources

post-upgrade

Après la mise à jour réussie

pre-rollback

Avant un rollback

post-rollback

Après un rollback réussi

pre-delete

Avant la suppression (helm uninstall)

post-delete

Après la suppression de toutes les ressources

test

Exécuté par helm test

La politique de suppression (hook-delete-policy) contrôle quand le Job est nettoyé :

  • before-hook-creation : supprime l’ancien hook avant d’en créer un nouveau

  • hook-succeeded : supprime après succès

  • hook-failed : supprime après échec (utile pour libérer les resources même en cas d’erreur)

Tests Helm#

Helm intègre un mécanisme de test qui exécute des pods de vérification après le déploiement.

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ .Release.Name }}-test-connection"
  annotations:
    "helm.sh/hook": test
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox:1.36
      command: ["wget"]
      args:
        - "--spider"
        - "--timeout=5"
        - "http://{{ .Release.Name }}-myapp:{{ .Values.service.port }}/healthz"
# Exécuter les tests après le déploiement
helm test myapp-release --namespace production

# Résultat attendu :
# NAME: myapp-release
# LAST DEPLOYED: Thu Mar 26 08:00:00 2026
# NAMESPACE: production
# STATUS: deployed
# TEST SUITE:     myapp-release-test-connection
# Last Started:   Thu Mar 26 08:01:00 2026
# Last Completed: Thu Mar 26 08:01:05 2026
# Phase:          Succeeded

Upgrades robustes#

Les flags --atomic, --wait et --timeout transforment un helm upgrade en opération transactionnelle :

helm upgrade myapp ./myapp \
  --namespace production \
  --values values-prod.yaml \
  --atomic \          # rollback automatique si l'upgrade échoue
  --wait \            # attend que tous les pods soient Ready
  --timeout 5m \      # timeout global (défaut : 5m)
  --cleanup-on-fail \ # supprime les nouvelles ressources créées en cas d'échec
  --create-namespace
  • --wait : Helm attend que tous les Deployments, StatefulSets, DaemonSets et Jobs soient dans l’état souhaité avant de considérer l’upgrade réussi.

  • --atomic : implique --wait et déclenche automatiquement un helm rollback si le timeout est dépassé ou si un pod reste en état d’erreur.

Idempotence des upgrades

helm upgrade --install combine install et upgrade en une seule commande idempotente : si la release n’existe pas, elle est créée ; sinon, elle est mise à jour. C’est la forme recommandée dans les pipelines CI/CD.

Helmfile#

Helmfile est un outil déclaratif qui gère un ensemble de releases Helm dans un seul fichier YAML. Il résout le problème du multi-chart : une application réelle déploie souvent 5 à 20 charts distincts avec des dépendances entre eux.

# helmfile.yaml
environments:
  staging:
    values:
      - environments/staging.yaml
  production:
    values:
      - environments/production.yaml

repositories:
  - name: bitnami
    url: oci://registry-1.docker.io/bitnamicharts
    oci: true
  - name: myorg
    url: oci://ghcr.io/myorg/charts
    oci: true

releases:
  - name: postgresql
    chart: bitnami/postgresql
    version: "~15.2.0"
    namespace: databases
    values:
      - values/postgresql.yaml.gotmpl
    installed: {{ eq .Environment.Name "staging" }}   # uniquement en staging

  - name: redis
    chart: bitnami/redis
    version: "~19.0.0"
    namespace: caches
    values:
      - values/redis.yaml

  - name: myapp
    chart: myorg/myapp
    version: "~1.4.0"
    namespace: production
    values:
      - values/myapp-common.yaml
      - values/myapp-{{ .Environment.Name }}.yaml
    needs:
      - databases/postgresql   # dépendance : postgresql déployé en premier
      - caches/redis
# Déployer toutes les releases de l'environnement production
helmfile --environment production sync

# Visualiser les différences avant application
helmfile --environment production diff

# Détruire toutes les releases
helmfile --environment production destroy

Registres OCI pour les charts#

Depuis Helm 3.8, les charts peuvent être distribués via des registres OCI (Open Container Initiative), simplifiant l’infrastructure : plus besoin d’un chart museum séparé.

# Construire et publier un chart
helm package ./myapp                              # → myapp-1.4.2.tgz
helm push myapp-1.4.2.tgz oci://ghcr.io/myorg/charts

# Installer depuis un registre OCI
helm install myapp oci://ghcr.io/myorg/charts/myapp \
  --version 1.4.2 \
  --values values-prod.yaml

# Inspecter un chart sans l'installer
helm show values oci://ghcr.io/myorg/charts/myapp --version 1.4.2

Authentification OCI

Les registres OCI utilisent les mêmes mécanismes d’authentification que Docker : helm registry login ghcr.io --username $GITHUB_USER --password $GITHUB_TOKEN. Dans les pipelines CI, utiliser les secrets du workflow pour injecter les credentials.

Kustomize vs Helm#

Le positionnement de ces deux outils génère souvent de la confusion :

Critère

Helm

Kustomize

Paradigme

Templating (Go templates)

Overlays déclaratifs (patches)

Courbe d’apprentissage

Élevée (templating, chart structure)

Modérée (patches JSON/YAML)

Packaging

Oui (chart distributable)

Non (code source uniquement)

Gestion des secrets

Via plugins (helm-secrets, SOPS)

Via SecretGenerator + KSOPS

GitOps-friendly

Oui (Argo CD, Flux)

Très bien intégré (natif kubectl)

Cas d’usage idéal

Distribution de logiciels réutilisables

Personnalisation d’un chart existant

Helm + Kustomize : le meilleur des deux mondes#

ArgoCD et Flux supportent nativement la combinaison : Helm génère les manifestes de base, Kustomize applique des patches d’environnement sur le résultat.

# helm template génère les manifestes, kustomize les patch
helm template myapp ./myapp --values values-base.yaml | kubectl kustomize -

Helm Secrets#

Le plugin helm-secrets intègre SOPS dans le workflow Helm : les fichiers secrets.yaml sont chiffrés dans Git et déchiffrés à la volée lors du helm upgrade.

# Installation du plugin
helm plugin install https://github.com/jkroepke/helm-secrets

# Chiffrer un fichier de secrets
helm secrets enc secrets/prod-secrets.yaml

# Upgrade avec déchiffrement automatique
helm secrets upgrade myapp ./myapp \
  --values values-prod.yaml \
  --values secrets/prod-secrets.yaml

Debugging#

# Générer les manifestes sans déployer (dry-run local)
helm template myapp ./myapp --values values-prod.yaml

# Linter le chart
helm lint ./myapp --values values-prod.yaml --strict

# Dry-run serveur (validation contre l'API server)
helm upgrade myapp ./myapp --values values-prod.yaml --dry-run=server

# Inspecter une release déployée
helm get manifest myapp-release --namespace production
helm get values   myapp-release --namespace production

# Historique des révisions
helm history myapp-release --namespace production

Simulations Python#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# ── Machine à états d'un déploiement Helm ────────────────────────────────────
fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 7)
ax.axis("off")
ax.set_title("Machine à états — déploiement Helm avec rollback sur échec", fontsize=13, fontweight="bold")

STATES = {
    "pending":    (1.5, 5.5, "#4C72B0", "En attente\n(Pending)"),
    "hooks_pre":  (4.5, 5.5, "#8172B2", "Hooks pre-install\n/ pre-upgrade"),
    "deploying":  (7.5, 5.5, "#DD8452", "Déploiement\ndes ressources"),
    "wait":       (10.5, 5.5, "#4C72B0", "Attente Ready\n(--wait)"),
    "hooks_post": (10.5, 3.0, "#8172B2", "Hooks post-install\n/ post-upgrade"),
    "success":    (7.5, 1.2, "#55A868", "Succès\n(Deployed)"),
    "rollback":   (4.5, 1.2, "#C44E52", "Rollback\nautomatique"),
    "failed":     (1.5, 1.2, "#888888", "Échec\n(Failed)"),
}

for key, (x, y, color, label) in STATES.items():
    b = FancyBboxPatch((x - 1.1, y - 0.55), 2.2, 1.1,
                       boxstyle="round,pad=0.12",
                       facecolor=color, edgecolor="white", linewidth=2, alpha=0.9)
    ax.add_patch(b)
    ax.text(x, y, label, ha="center", va="center",
            fontsize=8.5, color="white", fontweight="bold", multialignment="center")

def arr(ax, x1, y1, x2, y2, label="", color="#444"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8))
    if label:
        ax.text((x1+x2)/2 + 0.05, (y1+y2)/2 + 0.12, label, fontsize=8, color=color)

arr(ax, 2.6, 5.5, 3.4, 5.5, "helm upgrade")
arr(ax, 5.6, 5.5, 6.4, 5.5, "OK")
arr(ax, 8.6, 5.5, 9.4, 5.5, "ressources créées")
arr(ax, 10.5, 4.45, 10.5, 3.55, "pods Ready")
arr(ax, 9.4, 3.0, 8.6, 1.65, "hooks OK", "#55A868")
arr(ax, 7.5, 4.95, 7.5, 1.75, "skip hooks", "#888")

# Échecs → rollback
arr(ax, 4.5, 4.95, 4.5, 1.75, "hook fail\n(--atomic)", "#C44E52")
arr(ax, 10.5, 4.95, 5.6, 1.2, "timeout / pod crash\n(--atomic)", "#C44E52")
arr(ax, 3.4, 1.2, 2.6, 1.2, "rollback fail")

plt.savefig("_static/12_helm_states.png", dpi=120, bbox_inches="tight")
plt.show()
_images/944d299e8d62edfac00e29f7c4c8e82707138eb7e499101b804e8e1450ef9389.png
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# ── Radar chart : Helm vs Kustomize vs Helm+Kustomize ────────────────────────
categories = [
    "Templating\n(expressivité)",
    "Packaging\n(distribuabilité)",
    "GitOps-\nfriendly",
    "Courbe\nd'apprentissage\n(inversée)",
    "Gestion\ndes secrets",
    "Personnalisation\nenv. spécifique",
]
N = len(categories)

scores = {
    "Helm seul":          [9, 10, 7, 4, 7, 7],
    "Kustomize seul":     [4,  2, 9, 8, 6, 9],
    "Helm + Kustomize":   [9,  9, 9, 3, 8, 10],
}

angles = np.linspace(0, 2 * np.pi, N, endpoint=False).tolist()
angles += angles[:1]

fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.set_title("Helm vs Kustomize vs Helm + Kustomize", fontsize=13, fontweight="bold", pad=20)

colors = ["#4C72B0", "#55A868", "#C44E52"]

for (label, vals), color in zip(scores.items(), colors):
    vals_plot = vals + vals[:1]
    ax.plot(angles, vals_plot, color=color, lw=2.5, label=label)
    ax.fill(angles, vals_plot, color=color, alpha=0.12)

ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, fontsize=9)
ax.set_yticks([2, 4, 6, 8, 10])
ax.set_yticklabels(["2", "4", "6", "8", "10"], fontsize=8)
ax.set_ylim(0, 10)
ax.legend(loc="upper right", bbox_to_anchor=(1.35, 1.15), fontsize=10)

plt.savefig("_static/12_helm_radar.png", dpi=120, bbox_inches="tight")
plt.show()
_images/6fce950c1edf73b4fb4e11b19398a317f40464d59e89c5690d6c16cb5314a3f3.png
import matplotlib.pyplot as plt
import networkx as nx
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# ── Structure d'un chart Helm avec dépendances ────────────────────────────────
G = nx.DiGraph()

# Nœuds : chart principal et ses fichiers / sous-charts
nodes = {
    "myapp\n(chart)":       {"level": 0, "color": "#4C72B0"},
    "Chart.yaml":           {"level": 1, "color": "#8172B2"},
    "values.yaml":          {"level": 1, "color": "#8172B2"},
    "templates/":           {"level": 1, "color": "#DD8452"},
    "charts/":              {"level": 1, "color": "#55A868"},
    "Chart.lock":           {"level": 1, "color": "#937860"},
    "Deployment.yaml":      {"level": 2, "color": "#DD8452"},
    "Service.yaml":         {"level": 2, "color": "#DD8452"},
    "HPA.yaml":             {"level": 2, "color": "#DD8452"},
    "hooks/migrate.yaml":   {"level": 2, "color": "#C44E52"},
    "tests/":               {"level": 2, "color": "#C44E52"},
    "postgresql/":          {"level": 2, "color": "#55A868"},
    "redis/":               {"level": 2, "color": "#55A868"},
}

for node in nodes:
    G.add_node(node)

edges = [
    ("myapp\n(chart)", "Chart.yaml"),
    ("myapp\n(chart)", "values.yaml"),
    ("myapp\n(chart)", "templates/"),
    ("myapp\n(chart)", "charts/"),
    ("myapp\n(chart)", "Chart.lock"),
    ("templates/", "Deployment.yaml"),
    ("templates/", "Service.yaml"),
    ("templates/", "HPA.yaml"),
    ("templates/", "hooks/migrate.yaml"),
    ("templates/", "tests/"),
    ("charts/", "postgresql/"),
    ("charts/", "redis/"),
]

G.add_edges_from(edges)

# Positionnement manuel pour une mise en page lisible
pos = {
    "myapp\n(chart)":     (5.0, 4.0),
    "Chart.yaml":         (1.5, 2.5),
    "values.yaml":        (3.0, 2.5),
    "templates/":         (5.5, 2.5),
    "charts/":            (7.5, 2.5),
    "Chart.lock":         (9.5, 2.5),
    "Deployment.yaml":    (4.0, 1.0),
    "Service.yaml":       (5.2, 1.0),
    "HPA.yaml":           (6.4, 1.0),
    "hooks/migrate.yaml": (7.6, 1.0),
    "tests/":             (8.8, 1.0),
    "postgresql/":        (7.0, 0.2),
    "redis/":             (8.5, 0.2),
}

node_colors = [nodes[n]["color"] for n in G.nodes()]

fig, ax = plt.subplots(figsize=(13, 6))
ax.set_title("Structure d'un chart Helm avec dépendances", fontsize=13, fontweight="bold")

nx.draw(G, pos, ax=ax, with_labels=True,
        node_color=node_colors,
        node_size=2200,
        font_size=7.5,
        font_color="white",
        font_weight="bold",
        edge_color="#666666",
        arrows=True,
        arrowstyle="-|>",
        arrowsize=18,
        width=1.8)

# Légende
legend_items = [
    mpatches.Patch(color="#4C72B0", label="Chart racine"),
    mpatches.Patch(color="#8172B2", label="Métadonnées"),
    mpatches.Patch(color="#DD8452", label="Templates"),
    mpatches.Patch(color="#C44E52", label="Hooks / Tests"),
    mpatches.Patch(color="#55A868", label="Sous-charts"),
    mpatches.Patch(color="#937860", label="Lockfile"),
]
ax.legend(handles=legend_items, loc="upper left", fontsize=9, framealpha=0.9)

plt.savefig("_static/12_helm_structure.png", dpi=120, bbox_inches="tight")
plt.show()
_images/4675f4f0d70ff2fc051f28edffc9a3e7f24cbaa5882386c0475baf6f2b781eb3.png

Résumé#

  1. Les dépendances de charts (Chart.yaml + helm dependency update) permettent d’empaqueter une application avec ses services tiers ; Chart.lock garantit la reproductibilité exacte des versions résolues.

  2. Les hooks Helm (pre-install, post-upgrade, pre-delete…) permettent d’exécuter des pods de migration ou de vérification à des moments précis du cycle de vie d’une release.

  3. helm test exécute des pods de vérification post-déploiement ; c’est le mécanisme natif pour valider que la release fonctionne correctement après un upgrade.

  4. Les flags --atomic, --wait et --timeout rendent les upgrades transactionnels : Helm revient automatiquement à la révision précédente si le déploiement échoue.

  5. Helmfile déclare l’ensemble des releases d’un projet dans un fichier versionnable, gère les dépendances entre charts et supporte plusieurs environnements via des values contextuelles.

  6. Les registres OCI simplifient la distribution des charts en réutilisant l’infrastructure existante des registres Docker ; plus besoin d’un chart museum séparé.

  7. Helm et Kustomize sont complémentaires : Helm excelle pour le packaging et la distribution, Kustomize pour la personnalisation d’overlays par environnement — leur combinaison est supportée nativement par Argo CD et Flux.

  8. Le plugin helm-secrets intègre SOPS dans le workflow Helm pour chiffrer les fichiers de valeurs sensibles tout en les versionnant dans Git.

  9. La commande helm template est l’outil de debug fondamental : elle génère les manifestes sans les appliquer, permettant d’inspecter le rendu des templates et de détecter les erreurs avant tout déploiement.

  10. La combinaison helm lint --strict + helm template + --dry-run=server constitue un pipeline de validation complet qui détecte les erreurs de templating, de schéma et de validation API avant tout contact avec le cluster de production.