Helm#

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
import matplotlib.gridspec as gridspec
import numpy as np
import pandas as pd
import seaborn as sns
import json
import string
import random
from collections import OrderedDict

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams['figure.dpi'] = 110
plt.rcParams['font.family'] = 'DejaVu Sans'

Le problème que Helm résout#

Imaginez que vous devez installer WordPress sur Kubernetes. Cette application nécessite :

  • Un Deployment pour WordPress (avec la bonne image, les variables d’environnement, les volumes)

  • Un Deployment pour MySQL (avec mot de passe, volume persistant)

  • Deux Services (pour exposer les ports)

  • Un PersistentVolumeClaim (pour le stockage MySQL)

  • Un Secret (pour le mot de passe de la base de données)

  • Éventuellement un Ingress (pour le nom de domaine)

Ça représente 6 à 8 fichiers YAML, étroitement liés. Si vous voulez en installer une seconde instance (pour un autre client), vous devez dupliquer tous ces fichiers et modifier les noms. Si une mise à jour est disponible, vous devez modifier chaque fichier manuellement.

Helm résout ce problème. C’est le gestionnaire de paquets de Kubernetes.

Analogie apt / pip

Helm est à Kubernetes ce qu”apt est à Debian ou ce que pip est à Python.

  • pip install django → installe Django et toutes ses dépendances

  • helm install mon-wordpress bitnami/wordpress → installe WordPress et toutes ses ressources Kubernetes

Un Chart Helm est l’équivalent d’un package pip. Une Release est une instance installée (comme un paquet installé). Le Repository est l’équivalent de PyPI.

Les quatre concepts fondamentaux#

Hide code cell source

fig, axes = plt.subplots(2, 2, figsize=(14, 9))

concepts = [
    {
        "ax": axes[0, 0],
        "titre": "Chart",
        "icone": "📦",
        "couleur": "#2196F3",
        "desc": "Le package Helm",
        "details": [
            "Ensemble de fichiers templates YAML",
            "Un values.yaml avec les défauts",
            "Un Chart.yaml (métadonnées)",
            "Peut avoir des dépendances (charts/)",
            "Versionné (ex. wordpress-18.1.4)",
        ],
        "analogie": "≈ package pip / deb"
    },
    {
        "ax": axes[0, 1],
        "titre": "Release",
        "icone": "🚀",
        "couleur": "#4CAF50",
        "desc": "Une instance installée d'un Chart",
        "details": [
            "Créée avec helm install",
            "A un nom unique dans un namespace",
            "Peut être mise à jour (helm upgrade)",
            "A un historique de révisions",
            "Peut être rollbackée (helm rollback)",
        ],
        "analogie": "≈ instance déployée"
    },
    {
        "ax": axes[1, 0],
        "titre": "Repository",
        "icone": "🗄️",
        "couleur": "#FF9800",
        "desc": "Catalogue de Charts",
        "details": [
            "URL HTTP servant un index.yaml",
            "Charts publics : Artifact Hub",
            "Charts privés : Harbor, Nexus",
            "helm repo add bitnami https://...",
            "helm search repo bitnami",
        ],
        "analogie": "≈ PyPI / apt sources.list"
    },
    {
        "ax": axes[1, 1],
        "titre": "Values",
        "icone": "⚙️",
        "couleur": "#9C27B0",
        "desc": "Paramètres de configuration",
        "details": [
            "values.yaml : valeurs par défaut",
            "Override avec -f custom.yaml",
            "Override ponctuel avec --set clé=val",
            "Hiérarchie de surcharge",
            "Permettent la réutilisation du chart",
        ],
        "analogie": "≈ variables de configuration"
    },
]

for c in concepts:
    ax = c["ax"]
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis('off')

    # En-tête coloré
    rect = FancyBboxPatch((0.3, 7.8), 9.4, 2.0, boxstyle="round,pad=0.2",
                          facecolor=c["couleur"], edgecolor='none', alpha=0.9)
    ax.add_patch(rect)
    ax.text(5, 9.0, c["titre"], ha='center', va='center',
            fontsize=17, fontweight='bold', color='white')
    ax.text(5, 8.2, c["desc"], ha='center', va='center',
            fontsize=10, color='white', alpha=0.9)

    # Détails
    for i, d in enumerate(c["details"]):
        ax.text(1.0, 7.0 - i*1.1, f"• {d}", ha='left', va='center',
                fontsize=9, color='#333333')

    # Analogie
    rect2 = FancyBboxPatch((0.5, 0.3), 9.0, 0.9, boxstyle="round,pad=0.1",
                           facecolor=c["couleur"], edgecolor='none', alpha=0.15)
    ax.add_patch(rect2)
    ax.text(5, 0.75, c["analogie"], ha='center', va='center',
            fontsize=10, color=c["couleur"], fontweight='bold')

plt.suptitle("Les quatre concepts fondamentaux de Helm", fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig("helm_concepts.png", dpi=110, bbox_inches='tight')
plt.show()
_images/1d5c19744f228201103a667080f51c203a5eaddb42792513ce0106a69990049e.png

Structure d’un Chart Helm#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 11)
ax.axis('off')

# Structure d'arborescence
structure = [
    (0, 10.2, "mon-app/", "#1565C0", True, "Répertoire racine du chart"),
    (0.5, 9.4, "Chart.yaml", "#2196F3", False, "Métadonnées : nom, version, description, dépendances"),
    (0.5, 8.6, "values.yaml", "#4CAF50", False, "Valeurs par défaut (surchargeables par l'utilisateur)"),
    (0.5, 7.8, "values.schema.json", "#66BB6A", False, "Schéma JSON pour valider les values (optionnel)"),
    (0.5, 7.0, "templates/", "#FF9800", True, "Templates Go des manifestes Kubernetes"),
    (1.0, 6.2, "_helpers.tpl", "#FF9800", False, "Fonctions helper réutilisables (define/include)"),
    (1.0, 5.4, "deployment.yaml", "#FFA726", False, "Template du Deployment K8s"),
    (1.0, 4.6, "service.yaml", "#FFA726", False, "Template du Service K8s"),
    (1.0, 3.8, "ingress.yaml", "#FFA726", False, "Template de l'Ingress K8s"),
    (1.0, 3.0, "configmap.yaml", "#FFA726", False, "Template du ConfigMap"),
    (1.0, 2.2, "NOTES.txt", "#FF9800", False, "Message affiché après helm install/upgrade"),
    (0.5, 1.4, "charts/", "#9C27B0", True, "Dépendances (sous-charts, générées par helm dep update)"),
    (0.5, 0.6, ".helmignore", "#9E9E9E", False, "Fichiers exclus lors du packaging (comme .gitignore)"),
]

for x, y, nom, couleur, is_dir, description in structure:
    # Icône et nom
    icone = "📁" if is_dir else "📄"
    indent = "  " * int(x / 0.5)
    ax.text(0.3 + x, y, f"{indent}{icone} {nom}", ha='left', va='center',
            fontsize=10, fontweight='bold' if is_dir else 'normal',
            color=couleur, fontfamily='monospace')
    # Description
    ax.text(5.5, y, f"← {description}", ha='left', va='center',
            fontsize=8.5, color='#555555', style='italic')

    # Ligne de séparation légère
    ax.axhline(y=y - 0.35, xmin=0.02, xmax=0.98, color='#E0E0E0', linewidth=0.5)

ax.set_title("Structure d'un Chart Helm", fontsize=14, fontweight='bold', pad=10)
plt.tight_layout()
plt.savefig("helm_structure.png", dpi=110, bbox_inches='tight')
plt.show()
/tmp/ipykernel_23214/531314076.py:38: UserWarning: Glyph 128193 (\N{FILE FOLDER}) missing from font(s) DejaVu Sans Mono.
  plt.tight_layout()
/tmp/ipykernel_23214/531314076.py:38: UserWarning: Glyph 128196 (\N{PAGE FACING UP}) missing from font(s) DejaVu Sans Mono.
  plt.tight_layout()
/tmp/ipykernel_23214/531314076.py:39: UserWarning: Glyph 128193 (\N{FILE FOLDER}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("helm_structure.png", dpi=110, bbox_inches='tight')
/tmp/ipykernel_23214/531314076.py:39: UserWarning: Glyph 128196 (\N{PAGE FACING UP}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("helm_structure.png", dpi=110, bbox_inches='tight')
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128193 (\N{FILE FOLDER}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128196 (\N{PAGE FACING UP}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
_images/8c524acefa0d091c6c2e3d6a4bfb318fbc0c0e0334b4471b20f934ab9844dbfe.png

Chart.yaml#

# Chart.yaml — métadonnées du chart
apiVersion: v2
name: mon-app
description: Une application web simple avec PostgreSQL
type: application
version: 1.3.0        # Version du chart (semver)
appVersion: "2.4.1"   # Version de l'application empaquetée

keywords:
  - web
  - api

maintainers:
  - name: Lôc Cosnier
    email: loc@exemple.fr

dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled

values.yaml#

# values.yaml — valeurs par défaut
replicaCount: 2

image:
  repository: mon-registry/mon-app
  tag: "2.4.1"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  className: nginx
  host: mon-app.exemple.fr

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

postgresql:
  enabled: true
  auth:
    database: monapp
    username: monapp
    password: ""   # À surcharger !

Templates Go#

Les templates Helm utilisent le moteur de template Go, enrichi de fonctions Sprig :

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mon-app.fullname" . }}
  labels:
    {{- include "mon-app.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "mon-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "mon-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          {{- if .Values.resources }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- end }}
          env:
            - name: DATABASE_URL
              value: {{ printf "postgresql://%s@%s-postgresql/%s"
                .Values.postgresql.auth.username
                (include "mon-app.fullname" .)
                .Values.postgresql.auth.database | quote }}
# templates/_helpers.tpl
{{/*
Nom complet de l'application
*/}}
{{- define "mon-app.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Labels communs
*/}}
{{- define "mon-app.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

Structures de contrôle en Go Template#

# Conditionnel
{{- if .Values.ingress.enabled }}
# ... manifeste Ingress ...
{{- end }}

# Boucle sur une liste
{{- range .Values.env }}
- name: {{ .name }}
  value: {{ .value | quote }}
{{- end }}

# Boucle sur un dictionnaire
{{- range $key, $val := .Values.config }}
{{ $key }}: {{ $val | quote }}
{{- end }}

# With (change le contexte)
{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

# Default et quote
image: "{{ .Values.image.repository | default "nginx" }}:{{ .Values.image.tag | default "latest" | quote }}"

Commandes Helm essentielles#

Hide code cell source

fig, ax = plt.subplots(figsize=(15, 10))
ax.set_xlim(0, 15)
ax.set_ylim(0, 11)
ax.axis('off')

commandes = [
    ("Gestion des repos", "#1565C0", [
        ("helm repo add bitnami https://charts.bitnami.com/bitnami", "Ajouter un dépôt"),
        ("helm repo update", "Mettre à jour le cache des repos"),
        ("helm search repo bitnami/wordpress", "Chercher un chart"),
        ("helm show values bitnami/wordpress", "Voir les values d'un chart"),
    ]),
    ("Installation et mise à jour", "#2E7D32", [
        ("helm install ma-release bitnami/wordpress -n prod", "Installer un chart (nom + namespace)"),
        ("helm install ma-release -f custom-values.yaml bitnami/wordpress", "Avec values personnalisées"),
        ("helm install ma-release bitnami/wordpress --set image.tag=6.4", "Avec --set ponctuel"),
        ("helm upgrade ma-release bitnami/wordpress --reuse-values", "Mettre à jour (garde les values)"),
        ("helm upgrade --install ma-release bitnami/wordpress", "Installer ou mettre à jour"),
    ]),
    ("Rollback et désinstallation", "#7B1FA2", [
        ("helm rollback ma-release 2", "Revenir à la révision 2"),
        ("helm rollback ma-release 0", "Revenir à la précédente"),
        ("helm uninstall ma-release -n prod", "Désinstaller (supprime les ressources K8s)"),
        ("helm uninstall ma-release --keep-history", "Désinstaller mais garder l'historique"),
    ]),
    ("Inspection", "#E65100", [
        ("helm list -n prod", "Lister les releases dans un namespace"),
        ("helm list -A", "Lister toutes les releases (tous namespaces)"),
        ("helm history ma-release", "Historique des révisions"),
        ("helm status ma-release", "État d'une release"),
        ("helm get values ma-release", "Values utilisées par une release"),
        ("helm template ma-release bitnami/wordpress", "Prévisualiser les manifestes rendus"),
    ]),
]

y = 10.5
for categorie, couleur, cmds in commandes:
    # En-tête de catégorie
    rect = FancyBboxPatch((0.2, y - 0.4), 14.6, 0.55, boxstyle="round,pad=0.05",
                          facecolor=couleur, edgecolor='none', alpha=0.85)
    ax.add_patch(rect)
    ax.text(7.5, y - 0.12, categorie, ha='center', va='center',
            fontsize=10, fontweight='bold', color='white')
    y -= 0.7

    for cmd, desc in cmds:
        ax.text(0.5, y, cmd, ha='left', va='center',
                fontsize=8.5, fontfamily='monospace', color='#1A237E',
                bbox=dict(boxstyle='round,pad=0.15', facecolor='#E8EAF6',
                          edgecolor='#9FA8DA', linewidth=0.8))
        ax.text(9.5, y, f"← {desc}", ha='left', va='center',
                fontsize=8.5, color='#555555', style='italic')
        y -= 0.6

    y -= 0.2

ax.set_title("Commandes Helm essentielles", fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig("helm_commandes.png", dpi=110, bbox_inches='tight')
plt.show()
_images/a071b42ac7b5d7fe3870c893382bd0f75e581df022aad20933cb923d5ee38b3e.png

Flux helm install : de values.yaml aux ressources K8s#

Hide code cell source

fig, ax = plt.subplots(figsize=(16, 6))
ax.set_xlim(0, 16)
ax.set_ylim(0, 7)
ax.axis('off')

etapes = [
    (0.3, 2.5, 2.5, 2.0, "values.yaml\n(défauts)", "#4CAF50"),
    (3.2, 3.5, 2.5, 1.0, "custom.yaml\n(-f flag)", "#66BB6A"),
    (3.2, 2.0, 2.5, 1.0, "--set clé=val", "#81C784"),
    (6.2, 2.5, 2.5, 2.0, "Moteur\nde templates\nGo", "#FF9800"),
    (9.2, 2.5, 2.5, 2.0, "Manifestes\nYAML rendus", "#2196F3"),
    (12.2, 2.0, 2.5, 1.0, "Deployment", "#1565C0"),
    (12.2, 3.2, 2.5, 1.0, "Service", "#1565C0"),
    (12.2, 4.4, 2.5, 1.0, "Ingress…", "#1565C0"),
]

for x, y, w, h, text, color in etapes:
    rect = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.15",
                          facecolor=color, edgecolor='white', alpha=0.85, linewidth=2)
    ax.add_patch(rect)
    ax.text(x+w/2, y+h/2, text, ha='center', va='center',
            fontsize=9, fontweight='bold', color='white', multialignment='center')

# Flèches de fusion values → moteur
ax.annotate("", xy=(6.2, 3.5), xytext=(2.8, 3.5),
            arrowprops=dict(arrowstyle='->', color='#4CAF50', lw=2))
ax.annotate("", xy=(6.2, 3.2), xytext=(5.7, 4.0),
            arrowprops=dict(arrowstyle='->', color='#66BB6A', lw=2))
ax.annotate("", xy=(6.2, 2.8), xytext=(5.7, 2.5),
            arrowprops=dict(arrowstyle='->', color='#81C784', lw=2))

# Moteur → manifestes
ax.annotate("", xy=(9.2, 3.5), xytext=(8.7, 3.5),
            arrowprops=dict(arrowstyle='->', color='#FF9800', lw=2))

# Manifestes → ressources K8s
for y_target in [2.5, 3.7, 4.9]:
    ax.annotate("", xy=(12.2, y_target), xytext=(11.7, 3.5),
                arrowprops=dict(arrowstyle='->', color='#2196F3', lw=1.5))

# API Server
rect = FancyBboxPatch((12.2, 5.5), 2.5, 1.0, boxstyle="round,pad=0.15",
                      facecolor='#F44336', edgecolor='white', alpha=0.85, linewidth=2)
ax.add_patch(rect)
ax.text(13.45, 6.0, "K8s API Server", ha='center', va='center',
        fontsize=9, fontweight='bold', color='white')
ax.annotate("", xy=(13.45, 5.5), xytext=(13.45, 5.2),
            arrowprops=dict(arrowstyle='->', color='#F44336', lw=2))
ax.text(13.45, 5.35, "kubectl apply", ha='center', va='center',
        fontsize=8, color='#F44336', style='italic')

# Templates
rect_t = FancyBboxPatch((6.2, 0.3), 2.5, 1.5, boxstyle="round,pad=0.15",
                         facecolor='#9C27B0', edgecolor='white', alpha=0.7, linewidth=1.5)
ax.add_patch(rect_t)
ax.text(7.45, 1.05, "templates/\n*.yaml + _helpers.tpl", ha='center', va='center',
        fontsize=8, color='white', multialignment='center')
ax.annotate("", xy=(7.45, 2.5), xytext=(7.45, 1.8),
            arrowprops=dict(arrowstyle='->', color='#9C27B0', lw=1.5))

# Légendes de priorité
ax.text(3.8, 1.2, "Priorité croissante →", ha='left', va='center',
        fontsize=8.5, color='#555', style='italic')
ax.text(3.8, 0.7,
        "values.yaml < custom.yaml (-f) < --set",
        ha='left', va='center', fontsize=8.5, color='#2E7D32',
        bbox=dict(boxstyle='round,pad=0.2', facecolor='#E8F5E9', edgecolor='#4CAF50'))

ax.set_title("Flux helm install — Des values.yaml aux ressources Kubernetes",
             fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig("helm_flux.png", dpi=110, bbox_inches='tight')
plt.show()
_images/6191988c503cedfc6e35634cf829876df92e8dbe7e675cc0a6b5e6683671b438.png

Historique des releases et rollback#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 5))
ax.set_xlim(-0.5, 9)
ax.set_ylim(-1, 5)
ax.axis('off')

# Historique de releases
releases = [
    (0.5, "Rev 1\nv1.0.0", "#4CAF50", "2024-01-10\nInstallation initiale", "deployed"),
    (2.5, "Rev 2\nv1.1.0", "#4CAF50", "2024-02-15\nNouvelle feature", "superseded"),
    (4.5, "Rev 3\nv1.2.0", "#F44336", "2024-03-01\nBug critique !", "failed"),
    (6.5, "Rev 4\nv1.1.0", "#FF9800", "2024-03-01\nRollback → Rev 2", "deployed"),
    (8.5, "Rev 5\nv1.3.0", "#2196F3", "2024-04-01\nNouvelle version", "deployed"),
]

status_colors = {
    "deployed": "#4CAF50",
    "superseded": "#9E9E9E",
    "failed": "#F44336",
}

for i, (x, label, color, date, status) in enumerate(releases):
    # Cercle
    circle = plt.Circle((x, 2), 0.7, color=color, zorder=3, alpha=0.9)
    ax.add_patch(circle)
    ax.text(x, 2, label, ha='center', va='center',
            fontsize=8, fontweight='bold', color='white', zorder=4,
            multialignment='center')

    # Date et statut
    ax.text(x, 0.8, date, ha='center', va='center',
            fontsize=7.5, color='#555', multialignment='center')
    stat_color = status_colors[status]
    ax.text(x, -0.1, status, ha='center', va='center',
            fontsize=8, fontweight='bold', color=stat_color,
            bbox=dict(boxstyle='round,pad=0.15', facecolor=stat_color, alpha=0.15,
                      edgecolor=stat_color))

    # Flèche vers suivant
    if i < len(releases) - 1:
        next_x = releases[i+1][0]
        ax.annotate("", xy=(next_x - 0.7, 2), xytext=(x + 0.7, 2),
                    arrowprops=dict(arrowstyle='->', color='#555', lw=2, zorder=2))

# Flèche de rollback
ax.annotate("",
    xy=(4.5, 1.2), xytext=(6.5, 1.2),
    arrowprops=dict(arrowstyle='<-', color='#FF9800', lw=2.5,
                    connectionstyle='arc3,rad=-0.5'))
ax.text(5.5, 0.2, "helm rollback ma-release 2", ha='center', va='center',
        fontsize=8.5, color='#FF9800', fontweight='bold',
        bbox=dict(boxstyle='round,pad=0.2', facecolor='#FFF3E0', edgecolor='#FF9800'))

# Commande history
ax.text(4.5, 4.5,
        "$ helm history ma-release\n"
        "REV  UPDATED        STATUS      CHART           DESCRIPTION\n"
        "1    Jan 10 10:00   superseded  mon-app-1.0.0   Install complete\n"
        "2    Feb 15 14:30   superseded  mon-app-1.1.0   Upgrade complete\n"
        "3    Mar 01 09:15   failed      mon-app-1.2.0   Upgrade failed\n"
        "4    Mar 01 09:20   superseded  mon-app-1.1.0   Rollback to 2\n"
        "5    Apr 01 11:00   deployed    mon-app-1.3.0   Upgrade complete",
        ha='center', va='top', fontsize=7.5, fontfamily='monospace',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#F5F5F5', edgecolor='#BDBDBD'))

ax.set_title("Historique Helm et rollback", fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig("helm_historique.png", dpi=110, bbox_inches='tight')
plt.show()
_images/7a122e9b1e8d162c6593738cc0a4d422bc9e4aa3e5a2422313f50b4098d38c38.png

Créer son propre chart#

# Créer un chart avec la structure standard
helm create mon-app

# La commande crée :
# mon-app/
#   Chart.yaml
#   values.yaml
#   templates/
#     deployment.yaml
#     service.yaml
#     ingress.yaml
#     _helpers.tpl
#     NOTES.txt

# Vérifier la syntaxe des templates
helm lint mon-app/

# Prévisualiser les manifestes rendus (sans installer)
helm template ma-release mon-app/ -f custom-values.yaml

# Packager en archive .tgz
helm package mon-app/
# Produit : mon-app-0.1.0.tgz

# Pousser vers un registry OCI (Harbor, GHCR)
helm push mon-app-0.1.0.tgz oci://registry.exemple.fr/charts

Helmfile : orchestrer plusieurs releases#

Pour les environnements complexes, Helmfile permet de décrire toutes les releases Helm en un seul fichier déclaratif :

# helmfile.yaml
repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami
  - name: ingress-nginx
    url: https://kubernetes.github.io/ingress-nginx

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

releases:
  - name: ingress-nginx
    namespace: ingress-nginx
    chart: ingress-nginx/ingress-nginx
    version: 4.9.0

  - name: cert-manager
    namespace: cert-manager
    chart: jetstack/cert-manager
    version: v1.14.0
    set:
      - name: installCRDs
        value: true

  - name: mon-app
    namespace: production
    chart: ./charts/mon-app
    version: 1.3.0
    values:
      - values/mon-app.yaml
      - values/mon-app.{{ .Environment.Name }}.yaml
    needs:
      - ingress-nginx/ingress-nginx
# Appliquer toutes les releases pour l'environnement staging
helmfile -e staging sync

# Voir le diff avant d'appliquer
helmfile -e production diff

# Appliquer seulement une release
helmfile -e production apply --selector name=mon-app

Moteur de templates simplifié (simulation Python)#

Hide code cell source

# Simulation d'un moteur de templates Helm simplifié
# Utilise string.Template de la stdlib Python

import string
import json
from collections import ChainMap

def deep_merge(base, override):
    """Fusionne deux dictionnaires de manière récursive (override gagne)."""
    result = dict(base)
    for key, val in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(val, dict):
            result[key] = deep_merge(result[key], val)
        else:
            result[key] = val
    return result

def flatten_values(d, prefix=''):
    """Aplatit un dict imbriqué en clés pointées."""
    items = {}
    for k, v in d.items():
        full_key = f"{prefix}.{k}" if prefix else k
        if isinstance(v, dict):
            items.update(flatten_values(v, full_key))
        else:
            items[full_key] = v
    return items

def render_template(template_str, values, release_name, chart_name, chart_version):
    """Rend un template YAML simplifié en substituant les .Values."""
    # Substitutions simples (simulation du moteur Go)
    result = template_str
    flat = flatten_values(values)

    # .Values.xxx
    for key, val in flat.items():
        placeholder = "{{ .Values." + key + " }}"
        result = result.replace(placeholder, str(val))

    # .Release.Name, .Chart.Name, etc.
    result = result.replace("{{ .Release.Name }}", release_name)
    result = result.replace("{{ .Chart.Name }}", chart_name)
    result = result.replace("{{ .Chart.Version }}", chart_version)
    result = result.replace("{{ .Release.Name }}-{{ .Chart.Name }}", f"{release_name}-{chart_name}")

    return result


# Valeurs par défaut du chart
default_values = {
    "replicaCount": 2,
    "image": {
        "repository": "mon-registry/mon-app",
        "tag": "1.0.0",
        "pullPolicy": "IfNotPresent"
    },
    "service": {
        "type": "ClusterIP",
        "port": 80
    },
    "resources": {
        "requests": {"cpu": "100m", "memory": "128Mi"},
        "limits": {"cpu": "500m", "memory": "512Mi"}
    }
}

# Override utilisateur (simule -f custom.yaml)
custom_values = {
    "replicaCount": 3,
    "image": {
        "tag": "2.4.1"
    },
    "service": {
        "type": "LoadBalancer"
    }
}

# Override ponctuel --set
set_overrides = {"replicaCount": 5}  # simule --set replicaCount=5

# Fusion dans l'ordre de priorité
merged = deep_merge(default_values, custom_values)
merged = deep_merge(merged, set_overrides)

# Template de Deployment
deployment_template = """apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
    release: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080"""

rendered = render_template(
    deployment_template,
    merged,
    release_name="prod-release",
    chart_name="mon-app",
    chart_version="1.3.0"
)

# Visualisation
fig, axes = plt.subplots(1, 3, figsize=(16, 9))

def show_yaml(ax, title, content, color):
    ax.axis('off')
    ax.set_title(title, fontweight='bold', fontsize=11, color=color)
    ax.text(0.05, 0.95, content, transform=ax.transAxes,
            va='top', ha='left', fontsize=7.8, fontfamily='monospace',
            bbox=dict(boxstyle='round,pad=0.4', facecolor='#F8F8F8',
                      edgecolor=color, linewidth=2))

show_yaml(axes[0], "values.yaml (défauts)", json.dumps(default_values, indent=2, ensure_ascii=False), "#4CAF50")
show_yaml(axes[1], "custom.yaml + --set (overrides)", json.dumps(deep_merge(custom_values, set_overrides), indent=2, ensure_ascii=False), "#FF9800")
show_yaml(axes[2], "Manifeste rendu (helm template)", rendered, "#2196F3")

# Flèches entre les panneaux
for ax in axes[:2]:
    ax.annotate("", xy=(1.05, 0.5), xytext=(0.95, 0.5),
                xycoords='axes fraction', textcoords='axes fraction',
                arrowprops=dict(arrowstyle='->', color='#555', lw=2))

plt.suptitle("Simulation du moteur de templates Helm\n"
             "(priorité : défauts < -f custom.yaml < --set)",
             fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig("helm_template_simulation.png", dpi=110, bbox_inches='tight')
plt.show()

print("Valeurs finales après fusion :")
print(json.dumps(merged, indent=2, ensure_ascii=False))
print(f"\nreplicas finaux : {merged['replicaCount']} (--set a la priorité maximale)")
print(f"image tag final : {merged['image']['tag']} (override par custom.yaml)")
print(f"service type final : {merged['service']['type']} (override par custom.yaml)")
_images/d721922178ab928c75ae547a897c1a5806197d57cb5c281996a1f3c821a4e135.png
Valeurs finales après fusion :
{
  "replicaCount": 5,
  "image": {
    "repository": "mon-registry/mon-app",
    "tag": "2.4.1",
    "pullPolicy": "IfNotPresent"
  },
  "service": {
    "type": "LoadBalancer",
    "port": 80
  },
  "resources": {
    "requests": {
      "cpu": "100m",
      "memory": "128Mi"
    },
    "limits": {
      "cpu": "500m",
      "memory": "512Mi"
    }
  }
}

replicas finaux : 5 (--set a la priorité maximale)
image tag final : 2.4.1 (override par custom.yaml)
service type final : LoadBalancer (override par custom.yaml)

Artifact Hub#

Artifact Hub est le catalogue public de charts Helm (et aussi d’autres artefacts CNCF). On y trouve des charts pour quasiment tous les logiciels open-source populaires : nginx, PostgreSQL, Redis, Kafka, Prometheus, Grafana, etc.

# Chercher un chart dans les repos configurés
helm search repo postgres
# NAME                                CHART VERSION   APP VERSION
# bitnami/postgresql                  12.12.10        16.1.0
# bitnami/postgresql-ha               11.9.7          16.1.0

# Chercher dans Artifact Hub (nécessite helm hub search ou l'UI web)
helm search hub postgresql

# Voir toutes les versions d'un chart
helm search repo bitnami/postgresql --versions

# Installer en spécifiant une version précise
helm install my-postgres bitnami/postgresql --version 12.12.10

Récapitulatif#

Ce qu’il faut retenir

  1. Helm = gestionnaire de paquets K8s : il empaquette des ensembles de manifestes YAML en Charts réutilisables et paramétrables.

  2. Chart / Release / Repository / Values : les quatre concepts fondamentaux. Un Chart installé devient une Release. Les Values permettent de personnaliser sans modifier les templates.

  3. Templates Go : {{ .Values.xxx }}, {{ if }}, {{ range }}, {{ include }}. La fusion des values suit une hiérarchie : défauts < -f fichier < --set.

  4. Commandes clés : helm install, helm upgrade, helm rollback, helm history, helm template (pour prévisualiser).

  5. Helmfile orchestre plusieurs releases Helm en une configuration déclarative, avec support des environnements.

  6. Artifact Hub est le catalogue public de charts. bitnami est le dépôt le plus populaire.