---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```yaml
# 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
```

```bash
# 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.

```{admonition} Dépendances conditionnelles
:class: tip
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.

```yaml
# 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.

```yaml
# 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"
```

```bash
# 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 :

```bash
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.

```{admonition} Idempotence des upgrades
:class: note
`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.

```yaml
# 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
```

```bash
# 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é.

```bash
# 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
```

```{admonition} Authentification OCI
:class: important
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.

```bash
# 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`.

```bash
# 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

```bash
# 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

```{code-cell} python3
:tags: [hide-input]
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()
```

```{code-cell} python3
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()
```

```{code-cell} python3
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()
```

## 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.
