Aller au contenu
Écrire un chart

Écrire un chart

200 Pratiquer ⏱ 1 h 25 helmkubernetes

À la fin, vous saurez

  • Créer l'arborescence d'un chart et renseigner Chart.yaml sans confondre version et appVersion
  • Transformer un manifeste en modèle en n'extrayant que ce qui doit varier
  • Factoriser noms et étiquettes dans des fonctions d'aide nommées avec le nom du chart
  • Séparer étiquettes de sélection stables et étiquettes communes
  • Vérifier un chart avec helm lint, helm template, helm package et un essai à blanc côté serveur
  • Concevoir values.yaml comme une interface : noms clairs, valeurs obligatoires, désactivations explicites

Prérequis

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

Pourquoi

Au cours Kubernetes, les manifestes de Signalements ont été écrits pour une installation : un nom d'hôte, un nombre de répliques, une étiquette d'image. Les réutiliser pour un deuxième client, ou pour la recette, impose de copier les fichiers et de les modifier. Chaque correction du Deployment (une sonde, un réglage de sécurité) doit alors être reportée à la main dans toutes les copies, et l'on sait comment cela finit : les copies divergent, et la production n'est plus celle qu'on a testée.

Un chart règle cela en séparant ce qui est commun (la structure du Deployment, ses sondes, son contexte de sécurité, écrits une fois) de ce qui varie (quelques valeurs que l'on expose). Mais un chart mal conçu fait pire que des copies : cent valeurs pour tout régler, des modèles illisibles, un sélecteur qui change à chaque version et rend les mises à jour impossibles. L'art est dans le choix de ce que l'on paramètre, et dans la rigueur des conventions (noms, étiquettes) qui permettent de déployer plusieurs fois le même chart sans conflit.

Cette leçon écrit le chart de Signalements de bout en bout, et les suivantes l'enrichissent (valeurs validées par un schéma, dépendances, migration en hook, publication).

Les concepts

Ce que contient un chart

Un chart est un répertoire dont le nom est celui du chart. Les fichiers qui comptent, d'après la documentation :

Fichier ou répertoireRôle
Chart.yamll'identité du chart : nom, version, version de l'application, description. Obligatoire.
values.yamlles valeurs par défaut, c'est-à-dire l'interface de configuration.
templates/les modèles qui produisent les manifestes.
templates/_helpers.tpldes fragments réutilisables (par convention, un fichier dont le nom commence par _ ne produit aucun objet).
templates/NOTES.txtle message affiché après l'installation.
.helmignoreles fichiers à exclure de l'archive.
values.schema.jsonun schéma qui valide les valeurs (leçon 6).
charts/les dépendances (leçon 7).
crds/des définitions de ressources installées avant les modèles.

On crée l'arborescence avec helm create, mais le modèle fourni est conçu pour nginx, avec un Ingress, un autoscaler et un compte de service dont Signalements n'a pas besoin. Il est instructif à lire, pas à conserver tel quel : on part plutôt d'un répertoire vide, et l'on n'ajoute que ce que l'application exige.

Chart.yaml : version et appVersion

apiVersion: v2
name: signalements
description: API de signalements citoyens (formation Lyneko)
type: application
version: 0.1.0
appVersion: "1.2.0"
  • apiVersion: v2 est le format de chart de Helm 3 et de Helm 4. Les charts v1 (Helm 2) sont obsolètes.
  • name est le nom du chart ; il doit correspondre au nom du répertoire, en minuscules, avec des tirets.
  • type vaut application (défaut) pour un chart installable, ou library pour un chart qui ne fournit que des fonctions à d'autres (leçon 7).
  • version est la version du chart, au format SemVer 2. C'est elle qui nomme l'archive (signalements-0.1.0.tgz) et qui identifie une publication.
  • appVersion est la version de l'application que le chart déploie par défaut. La documentation précise que c'est une information : elle n'entre pas dans les calculs de Helm.

Les deux évoluent séparément, et cela perd souvent les débutants. Une règle claire :

  • On augmente version à chaque changement du chart (un modèle corrigé, une valeur ajoutée, un réglage de sécurité), même si l'application n'a pas changé.
  • On augmente appVersion quand la version de l'application déployée par défaut change.
  • Une nouvelle version d'application sans changement de structure fait bouger appVersion (et, par convention, la version de correctif de version). Une nouvelle valeur ou un nouveau modèle fait bouger version (mineure) ; un changement qui casse les valeurs existantes la fait bouger en majeure.

Dans les modèles, .Chart.AppVersion donne l'appVersion, .Chart.Version la version du chart. Dans le chart de Signalements, l'étiquette de l'image vient de la valeur image.tag ; si elle est vide, de appVersion : on peut ainsi déployer une autre version sans republier le chart, et la valeur par défaut reste cohérente avec le chart.

Les valeurs : une interface publique

values.yaml n'est pas une copie de configuration : c'est l'interface que votre chart offre à ses utilisateurs. La documentation des bonnes pratiques recommande quelques règles que ce chart suit :

  • Des noms en camelCase, qui commencent par une minuscule (replicaCount, pas replica_count).
  • Des maps plutôt que des clés à plat : image.repository et image.tag plutôt que imageRepository et imageTag. Un groupe de valeurs liées se lit et se surcharge ensemble.
  • Des commentaires sur chaque valeur, parce que helm show values est la documentation que l'utilisateur lit.
  • Des valeurs par défaut qui fonctionnent, sauf pour ce qui ne peut pas avoir de défaut (un nom de Secret, un nom d'hôte) : on le laisse vide et le modèle le déclare obligatoire.
  • Peu de valeurs. Chaque valeur est un engagement : une fois publiée, la supprimer casse les utilisateurs. N'exposez que ce qui varie vraiment, et ajoutez au besoin plutôt que par précaution.
  • Une activation explicite pour ce qui est optionnel (route.enabled), plutôt qu'un objet dont l'absence se devine.

Le values.yaml de Signalements :

# Nombre de répliques de l'API.
replicaCount: 2

image:
  repository: ghcr.io/lyneko-formation/signalements
  # Vide : on utilise appVersion du chart.
  tag: ""
  pullPolicy: IfNotPresent

# Secret existant qui contient la clé DATABASE_URL (créé hors du chart).
databaseSecretName: ""

service:
  port: 80

containerPort: 8000

resources:
  requests:
    cpu: 100m
    memory: 192Mi
  limits:
    memory: 256Mi

# Publication par Gateway API (désactivée par défaut).
route:
  enabled: false
  hostname: ""
  gateway:
    name: ""
    sectionName: http

Ce qui n'est pas dans les valeurs est aussi un choix : le contexte de sécurité, les sondes, le délai d'arrêt et la stratégie de mise à jour sont écrits dans le modèle, parce que ce sont les choix de Lyneko pour cette application, validés par le cours Kubernetes, et qu'aucun client n'a de raison légitime de les désactiver. Les exposer inviterait à les affaiblir. Quand un besoin réel apparaît, on ajoute alors une valeur.

Les modèles et les fonctions d'aide

Un modèle est un fichier YAML dans lequel des actions entre {{ et }} sont remplacées par leur résultat : {{ .Values.replicaCount }} affiche la valeur. La leçon 5 détaille le langage ; celle-ci n'en utilise que quatre éléments :

  • .Values, .Release, .Chart : l'accès aux valeurs, aux informations de la release (.Release.Name, .Release.Namespace) et du chart (.Chart.Name, .Chart.AppVersion) ;
  • include "nom" . : appelle un fragment nommé et renvoie son texte ;
  • | nindent N : ajoute un retour à la ligne et indente chaque ligne de N espaces, indispensable pour insérer du texte multiligne dans du YAML ;
  • {{- ... }} : le tiret retire les espaces et retours à la ligne qui précèdent l'action.

Les fonctions d'aide (helpers) se définissent avec define dans _helpers.tpl. Une règle de nommage importante : les noms de fragments sont globaux à l'installation, ils partagent un seul espace avec ceux des dépendances. On les préfixe donc du nom du chart (signalements.fullname, pas fullname), sinon deux charts qui définissent fullname s'écrasent.

{{/* Nom de l'application. */}}
{{- define "signalements.name" -}}
{{- .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Nom complet : le nom de la release, sauf s'il contient déjà celui du chart. */}}
{{- define "signalements.fullname" -}}
{{- if contains .Chart.Name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/* Version de l'image : la valeur image.tag, sinon appVersion. */}}
{{- define "signalements.tag" -}}
{{- .Values.image.tag | default .Chart.AppVersion }}
{{- end }}

{{/* Étiquettes de sélection : stables, jamais la version. */}}
{{- define "signalements.selectorLabels" -}}
app.kubernetes.io/name: {{ include "signalements.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api
{{- end }}

{{/* Étiquettes communes. */}}
{{- define "signalements.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{ include "signalements.selectorLabels" . }}
app.kubernetes.io/version: {{ include "signalements.tag" . | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

Quelques points demandent explication.

Pourquoi fullname ? Les noms des objets doivent être uniques par namespace et déduits de la release, pour que deux releases cohabitent. La forme <release>-<chart> est la convention. Le test contains évite le bégaiement : une release nommée signalements donnerait signalements-signalements, alors qu'on veut simplement signalements. Les noms Kubernetes sont limités à 63 caractères pour beaucoup d'objets (noms DNS), d'où trunc 63 ; trimSuffix "-" retire un tiret final que la troncature aurait pu laisser.

Pourquoi deux jeux d'étiquettes ? C'est la leçon la plus importante du chapitre. Le sélecteur d'un Deployment est immuable : on ne peut pas le modifier après création. Si une étiquette du sélecteur change entre deux versions du chart (la version de l'application, la version du chart), la mise à jour échoue avec un message du serveur disant que le champ spec.selector est immuable, et il faut supprimer et recréer le Deployment. Les étiquettes de sélection (name, instance, component) sont donc volontairement stables. Les étiquettes communes y ajoutent des informations qui, elles, évoluent (helm.sh/chart, app.kubernetes.io/version) et vont sur les métadonnées et le modèle de pod, jamais dans selector.

Pourquoi instance ? Deux releases du même chart dans un même namespace produiraient deux Deployments avec le même sélecteur name: signalements : chaque Deployment adopterait les pods de l'autre. app.kubernetes.io/instance, qui vaut le nom de la release, les distingue. Ces noms d'étiquettes sont ceux de la documentation Kubernetes (Recommended Labels), reconnus par les tableaux de bord et par Helm lui-même.

Pourquoi .Release.Service ? Cette valeur intégrée vaut Helm ; l'étiquette app.kubernetes.io/managed-by: Helm indique aux outils (et aux humains) qui gère l'objet.

En pratique

1. Partir de rien

$ mkdir -p signalements/templates && cd signalements

Écrivez Chart.yaml et values.yaml comme ci-dessus, puis templates/_helpers.tpl. L'ordre est celui d'une démarche saine : l'identité, l'interface, les fragments communs, puis les objets.

2. Le Deployment, depuis le manifeste du cours Kubernetes

On prend le Deployment de la leçon 11 du cours Kubernetes et l'on remplace, une à une, les valeurs qui doivent varier. Voici le résultat :

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "signalements.fullname" . }}
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      {{- include "signalements.selectorLabels" . | nindent 6 }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        {{- include "signalements.labels" . | nindent 8 }}
    spec:
      terminationGracePeriodSeconds: 40
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: "{{ .Values.image.repository }}:{{ include "signalements.tag" . }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.containerPort }}
          env:
            - name: APP_VERSION
              value: {{ include "signalements.tag" . | quote }}
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ required "databaseSecretName est obligatoire" .Values.databaseSecretName }}
                  key: DATABASE_URL
          startupProbe:
            httpGet: {path: /sante, port: http}
            periodSeconds: 2
            failureThreshold: 30
          readinessProbe:
            httpGet: {path: /sante, port: http}
            periodSeconds: 5
            failureThreshold: 2
          livenessProbe:
            tcpSocket: {port: http}
            periodSeconds: 10
          lifecycle:
            preStop:
              sleep: {seconds: 5}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir:
            medium: Memory
            sizeLimit: 16Mi

Les remplacements suivent une logique, qu'il vaut mieux comprendre que recopier :

  • Le nom vient de fullname, pas d'une chaîne figée : c'est ce qui permet deux releases côte à côte.
  • Les étiquettes viennent des fragments, sur trois niveaux : l'objet (metadata.labels, étiquettes communes), le sélecteur (matchLabels, étiquettes de sélection), le modèle de pod (étiquettes communes, qui doivent contenir celles du sélecteur : c'est le cas, puisque les communes les incluent).
  • Le namespace n'est pas écrit : Helm applique celui de la release (--namespace). Écrire namespace: signalements dans un modèle empêcherait de déployer ailleurs.
  • replicaCount, l'image et le port sont des valeurs.
  • DATABASE_URL vient d'un Secret existant, désigné par databaseSecretName, et required fait échouer le rendu avec un message clair si la valeur est vide. Le chart ne crée volontairement pas le Secret : un mot de passe passé en valeur finirait dans le Secret de release (leçon 3). On y revient à la leçon 6, avec External Secrets.
  • Les resources sont insérées par toYaml : la valeur est un petit arbre YAML, que toYaml réécrit en texte et que nindent 12 indente au bon niveau (12 espaces, la profondeur de la clé resources).
  • Ce qui est figé (sondes, contexte de sécurité, arrêt gracieux) reste du YAML brut.

3. Le Service et la route

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "signalements.fullname" . }}
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
spec:
  selector:
    {{- include "signalements.selectorLabels" . | nindent 4 }}
  ports:
    - name: http
      port: {{ .Values.service.port }}
      targetPort: http
# templates/httproute.yaml
{{- if .Values.route.enabled }}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ include "signalements.fullname" . }}
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
spec:
  parentRefs:
    - name: {{ required "route.gateway.name est obligatoire" .Values.route.gateway.name }}
      sectionName: {{ .Values.route.gateway.sectionName }}
  hostnames:
    - {{ required "route.hostname est obligatoire" .Values.route.hostname }}
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: {{ include "signalements.fullname" . }}
          port: {{ .Values.service.port }}
{{- end }}

Le Service sélectionne par les étiquettes de sélection, pas par les communes : sinon une mise à jour du chart qui change helm.sh/chart ferait perdre au Service ses pods. La route est entièrement conditionnée par route.enabled, valeur par défaut false : un chart qui s'installe sans Gateway API fonctionne, un chart qui publie sa route le dit explicitement.

4. Les notes et le fichier d'exclusion

# templates/NOTES.txt
Signalements {{ .Chart.AppVersion }} déployé sous le nom {{ .Release.Name }} dans {{ .Release.Namespace }}.

Vérifier l'API depuis votre poste :
  kubectl -n {{ .Release.Namespace }} port-forward service/{{ include "signalements.fullname" . }} 8080:{{ .Values.service.port }}
  curl http://localhost:8080/sante
{{- if .Values.route.enabled }}

Adresse publique : http://{{ .Values.route.hostname }}/
{{- end }}

NOTES.txt est un modèle comme les autres, affiché après install et upgrade et par helm status : il dit à l'utilisateur quoi faire ensuite. Écrivez-y des commandes prêtes à copier, avec le bon namespace et le bon nom de Service.

# .helmignore
.git/
.gitignore
*.swp
*.bak
*.tmp
ci/
README.md

.helmignore suit la syntaxe de .gitignore et exclut des fichiers de l'archive produite par helm package. Ici : les fichiers d'éditeur, et le répertoire ci/ où l'on range des fichiers de valeurs pour les tests.

5. Vérifier : helm lint

$ helm lint signalements --set databaseSecretName=signalements-db
==> Linting signalements
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

helm lint examine la structure du chart (champs obligatoires, YAML valide, noms de fichiers) et rend les modèles avec les valeurs par défaut. L'avertissement sur l'icône est informatif. Voyons ce qu'il attrape. Avec une indentation fausse dans le Deployment (nindent 4 au lieu de nindent 12) :

$ helm lint bad --set databaseSecretName=x
==> Linting bad
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/deployment.yaml: unable to parse YAML: error converting YAML to JSON: yaml: line 75: mapping values are not allowed in this context

Error: 1 chart(s) linted, 1 chart(s) failed

Et avec une version absente de Chart.yaml :

$ helm lint bad --set databaseSecretName=x
[ERROR] templates/: validation: chart.metadata.version is required
[ERROR] : unable to load chart

Error: 1 chart(s) linted, 1 chart(s) failed

Mais helm lint laisse passer une chose importante. Sans la valeur databaseSecretName, le modèle contient un required qui devrait échouer :

$ helm lint signalements
engine.go:214: [INFO] Missing required value: databaseSecretName est obligatoire
==> Linting signalements
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Sortie réelle avec Helm 3.16.3 : le lint signale la valeur manquante en [INFO] et réussit. Seul un rendu réel (helm template, helm install) échoue :

$ helm template signalements signalements
Error: execution error at (signalements/templates/deployment.yaml:41:27): databaseSecretName est obligatoire

Use --debug flag to render out invalid YAML

Conclusion pratique : donnez au lint les valeurs obligatoires (--set ou -f ci/valeurs-lint.yaml), comme ci-dessus. L'option --strict fait échouer le lint sur les avertissements.

6. Vérifier : helm template

$ helm template signalements signalements --set databaseSecretName=signalements-db \
    --set route.enabled=true --set route.hostname=signalements.formation.test \
    --set route.gateway.name=passerelle --show-only templates/service.yaml
---
# Source: signalements/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: signalements
  labels:
    helm.sh/chart: signalements-0.1.0
    app.kubernetes.io/name: signalements
    app.kubernetes.io/instance: signalements
    app.kubernetes.io/component: api
    app.kubernetes.io/version: "1.2.0"
    app.kubernetes.io/managed-by: Helm
spec:
  selector:
    app.kubernetes.io/name: signalements
    app.kubernetes.io/instance: signalements
    app.kubernetes.io/component: api
  ports:
    - name: http
      port: 80
      targetPort: http

Le nom du Service est signalements, et pas signalements-signalements, grâce au test de fullname : la release et le chart portent le même nom. Avec une release client-a, ce serait client-a-signalements.

Les parties qui comptent du Deployment rendu avec --set image.tag=1.2.1 :

$ helm template s signalements --set databaseSecretName=x --set image.tag=1.2.1 \
    --show-only templates/deployment.yaml | grep -E "image:|APP_VERSION|version:" -A1
    app.kubernetes.io/version: "1.2.1"
    app.kubernetes.io/managed-by: Helm
--
        app.kubernetes.io/version: "1.2.1"
        app.kubernetes.io/managed-by: Helm
--
          image: "ghcr.io/lyneko-formation/signalements:1.2.1"
          imagePullPolicy: IfNotPresent
--
            - name: APP_VERSION
              value: "1.2.1"

L'étiquette de version, l'image et la variable APP_VERSION suivent la même valeur, grâce au fragment signalements.tag : sans lui, trois endroits à tenir synchronisés. C'est la raison d'être des fragments.

Une faute de frappe dans un nom de valeur, elle, ne produit pas d'erreur : en écrivant .Values.containerport au lieu de .Values.containerPort, le rendu donne containerPort: vide (réellement observé), et c'est l'API Kubernetes qui refusera l'objet à l'installation. D'où l'intérêt du schéma de la leçon 6, et de required pour les valeurs critiques.

7. Empaqueter

$ helm package signalements -d out
Successfully packaged chart and saved it to: out/signalements-0.1.0.tgz

L'archive est nommée <nom>-<version>.tgz, avec la version du Chart.yaml.

8. Vérifier côté serveur

helm template ne contacte pas le cluster. Pour savoir si l'API accepte les objets (schéma, admission, quotas, profil de sécurité du namespace), Helm propose un essai à blanc côté serveur :

$ helm install signalements ./signalements -n signalements \
    --set databaseSecretName=signalements-db --dry-run=server

Avec --dry-run=server (Helm 3.13 et suivants), Helm rend les modèles et envoie les objets à l'API en mode essai : le serveur valide tout, applique les contrôleurs d'admission, mais ne persiste rien. La sortie affiche les manifestes, comme helm get manifest. C'est la bonne commande à placer en dernière étape de CI avant un vrai déploiement. Il y a deux effets de bord à connaître : le rendu peut appeler la fonction lookup (qui lit le cluster, leçon 5), et un objet qui entre en conflit avec un objet existant est signalé comme à l'installation réelle. Avec --dry-run=client (ou --dry-run seul avec les anciennes versions), il n'y a aucun contact avec le cluster.

Sous le capot

Du texte, pas des objets. Le moteur de modèles produit une chaîne de caractères par fichier. Helm la lit ensuite comme du YAML, d'où les deux phases d'erreurs que vous avez vues : les erreurs d'exécution du modèle (required, une fonction mal appelée, un champ inexistant sur une valeur nulle), et les erreurs de YAML sur le texte produit (indentation). Le message de la seconde phase dit « line 75 » : c'est la ligne du texte rendu, pas celle du modèle. Pour la voir, helm template --debug affiche le texte invalide plutôt que de s'arrêter.

L'ordre de rendu et d'installation. Helm rend tous les fichiers de templates/ (sauf ceux dont le nom commence par _), puis les trie par type d'objet avec une liste d'ordre fixe écrite dans son code : Namespace, ResourceQuota, ServiceAccount, Secret, ConfigMap, Service, Deployment, et ainsi de suite, les types inconnus (comme HTTPRoute) en dernier. Vous n'avez pas de moyen simple d'imposer un autre ordre, sauf par les hooks (leçon 8). C'est aussi pourquoi helm template ne présente pas les objets dans l'ordre des fichiers : vous l'avez vu, le Service précède le Deployment.

Les fragments partagent un espace de noms global. define enregistre le fragment sous son nom dans une table unique pour tout le chart et ses dépendances. Deux fragments de même nom : le dernier chargé l'emporte, sans erreur. D'où le préfixe signalements..

Les étiquettes communes déclenchent des déploiements. Elles figurent dans le modèle de pod : changer version dans Chart.yaml modifie l'étiquette helm.sh/chart, donc le modèle, donc remplace tous les pods à la mise à jour suivante, même si rien d'autre n'a changé. C'est le choix du modèle de helm create ; il est acceptable, mais il faut le savoir. Pour l'éviter, on ne met que les étiquettes de sélection sur les pods.

.helmignore. Les motifs sont lus à la fabrication de l'archive et à l'installation depuis un répertoire : un fichier ignoré n'est ni empaqueté, ni accessible par .Files dans les modèles.

Pièges courants

Un sélecteur qui contient la version. La mise à jour du chart échoue sur un champ immuable. Il faut supprimer le Deployment, ce qui coupe le service, pour changer le sélecteur : mettez helm.sh/chart et la version applicative dans les étiquettes communes, jamais dans celles de sélection.

Un fragment sans préfixe de chart. {{ define "fullname" }} dans deux charts (le vôtre et une dépendance) : l'un écrase l'autre, les noms des objets changent sans erreur.

Le namespace écrit en dur. namespace: signalements dans un modèle ignore --namespace et fait échouer toute installation ailleurs, ou pire, la fait aboutir au mauvais endroit.

Les valeurs obligatoires qui ne passent pas le lint. Voir plus haut : helm lint réussit sans required. Fournissez un fichier de valeurs de lint.

Ne tester qu'avec les valeurs par défaut. Les bugs se cachent derrière les conditions (route.enabled). Rendez le chart avec chaque combinaison significative de valeurs (route activée, valeurs de production).

toYaml sans nindent. Le texte inséré commence sur la ligne courante, avec une indentation fausse ; l'erreur est un mapping values are not allowed in this context à une ligne qui n'a l'air de rien.

Sécurité

  • Les réglages de sécurité sont dans le modèle, pas dans les valeurs. Profil seccomp, runAsNonRoot, readOnlyRootFilesystem, capacités abandonnées : un chart qui les rend optionnels invite à les désactiver au premier obstacle. Si un client doit vraiment les adapter, exposez une valeur précise, pas un bloc securityContext libre.
  • Pas de secret dans les valeurs ni dans le chart. Le chart référence un Secret existant. Un mot de passe en valeur est copié dans le Secret de release et dans l'historique (leçon 3), et dans les journaux des pipelines.
  • L'image : étiquette ou empreinte. Par défaut, image.tag vaut appVersion, une étiquette que l'éditeur peut déplacer. Pour une garantie forte, acceptez aussi image.digest (leçon 6) et épinglez par empreinte.
  • Des droits minimaux. Le chart ne crée ni ClusterRole, ni ServiceAccount privilégié. Un chart qui demande plus doit le justifier dans son README.
  • required comme garde-fou. Un chart qui s'installe sans nom de Secret produirait un pod qui ne démarre pas, ou pire, qui démarre sans base. Échouer au rendu coûte moins cher que d'échouer en production.
  • Relire le rendu, pas le modèle. La revue d'un changement de chart se fait sur le diff des manifestes rendus (helm template avant et après), pas sur celui des modèles.

En production

  • Un chart par application, dans un dépôt versionné, relu comme du code. Chez Lyneko, le chart de Signalements vit avec le code de l'application, et les valeurs par environnement dans le dépôt de déploiement (cours GitOps).
  • Une CI pour les charts. À chaque demande de fusion : helm lint avec des valeurs de test, helm template avec chaque jeu de valeurs, une validation des manifestes rendus contre le schéma de Kubernetes (par un outil comme kubeconform), et la comparaison avec la version précédente. La leçon 8 ajoute les tests exécutés sur un cluster.
  • Suivre le SemVer pour la version du chart, et tenir un journal des modifications : la majeure pour tout changement incompatible des valeurs.

Exercices

1. Nommer les objets (niveau 100). Que donne fullname pour une release signalements, une release client-a, et une release mon-signalements-test ?

Solution

signalements (le nom de la release contient déjà celui du chart, on le garde tel quel), client-a-signalements (la release ne contient pas le nom du chart, on les concatène), et mon-signalements-test (la release contient signalements, donc elle est gardée telle quelle). Le test contains regarde si le nom du chart est une sous-chaîne du nom de la release, pas seulement s'il lui est égal.

2. Corriger un chart (niveau 200). Un collègue a écrit, dans deployment.yaml, selector: matchLabels: {{ include "signalements.labels" . | nindent 6 }}. Tout fonctionne à l'installation. Que se passe-t-il à la première mise à jour de la version du chart (0.1.0 vers 0.1.1), et comment le corriger ?

Solution

Les étiquettes communes contiennent helm.sh/chart: signalements-0.1.0, puis -0.1.1 : le sélecteur change, or il est immuable. Le serveur refuse la mise à jour (field is immutable) et l'on doit supprimer le Deployment pour le recréer, avec une coupure. Correction : utiliser signalements.selectorLabels dans matchLabels (et dans le sélecteur du Service). Pour un chart déjà déployé avec le mauvais sélecteur, il faut supprimer puis recréer le Deployment, ce qui provoque une coupure à planifier.

3. Ajouter un budget de perturbation (niveau 200). Écrivez templates/pdb.yaml : un PodDisruptionBudget qui garde au moins un pod disponible, activé par podDisruptionBudget.enabled (défaut false), en réutilisant les fragments du chart.

Solution
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ include "signalements.fullname" . }}
  labels:
    {{- include "signalements.labels" . | nindent 4 }}
spec:
  minAvailable: 1
  selector:
    matchLabels:
      {{- include "signalements.selectorLabels" . | nindent 6 }}
{{- end }}

Avec, dans values.yaml, podDisruptionBudget: {enabled: false}. Le sélecteur utilise les étiquettes de sélection, comme celui du Service. On vérifie avec helm template ... --set podDisruptionBudget.enabled=true --show-only templates/pdb.yaml, et on n'oublie pas qu'avec replicaCount: 1 un minAvailable: 1 interdit toute éviction.

Récapitulatif

  • Un chart minimal : Chart.yaml, values.yaml, templates/ avec _helpers.tpl et NOTES.txt, .helmignore ; on part des manifestes existants et l'on ne paramètre que ce qui varie.
  • version est la version du chart (SemVer, identifie la publication), appVersion celle de l'application ; elles évoluent séparément.
  • values.yaml est l'interface publique : camelCase, maps, commentaires, peu de clés, required pour les valeurs sans défaut possible, activation explicite des options.
  • Les fragments de _helpers.tpl sont globaux : préfixez-les du nom du chart. fullname (nom de release + chart, 63 caractères) permet plusieurs releases côte à côte.
  • Deux jeux d'étiquettes : celles de sélection (stables, immuables, dans selector et le Service) et les communes (version, chart, dans les métadonnées).
  • helm lint (avec les valeurs obligatoires) vérifie la structure ; helm template montre le rendu ; helm install --dry-run=server fait valider les objets par l'API sans rien persister ; helm package produit <nom>-<version>.tgz.
  • Les réglages de sécurité sont écrits dans le modèle, les secrets restent hors du chart.

Pour aller plus loin

  • Le guide Chart Best Practices de la documentation de Helm : conventions de noms, d'étiquettes, de valeurs et de modèles.
  • La page Recommended Labels de la documentation de Kubernetes.
  • Le code du modèle helm create (pkg/chartutil/create.go dans helm/helm), à lire pour voir comment il gère l'autoscaler, l'Ingress et le compte de service.
  • La leçon suivante, Les modèles en profondeur, qui détaille les fonctions, le contrôle de flux et les pièges d'indentation.
Voir ma constellation →

Sources