Aller au contenu
English
English
Conteneurs & Orchestration medium

Anatomie d'un chart Helm : structure, templates et helpers

70 min de lecture

logo helm

Ce guide vous apprend à créer votre premier chart Helm de zéro. Vous allez comprendre la structure d'un chart, maîtriser les templates Go, écrire des helpers réutilisables et suivre les conventions qui rendent un chart maintenable. En 30 minutes, vous saurez écrire des charts professionnels.

Un chart n'est qu'un dossier, mais chacun de ses fichiers a un rôle que Helm connaît par son nom. Il n'y a pas de fichier de configuration qui déclarerait « voici mes templates » : c'est l'emplacement qui décide. Comprendre cette convention vous évite la moitié des erreurs de débutant, à commencer par un template rangé au mauvais endroit et jamais rendu.

Un chart Helm est un dossier contenant des fichiers avec des rôles précis :

  • Répertoiremon-chart/
    • Chart.yaml Métadonnées du chart (nom, version, description)
    • values.yaml Valeurs par défaut configurables
    • Répertoiretemplates/ Templates Kubernetes (Deployment, Service, etc.)
      • _helpers.tpl Fonctions réutilisables (helpers)
      • deployment.yaml Template du Deployment
      • service.yaml Template du Service
      • NOTES.txt Message affiché après installation
    • Répertoirecharts/ Dépendances (subcharts)
      • …
    • .helmignore Fichiers à exclure du package
FichierRôleObligatoire ?
Chart.yamlIdentité du chart (nom, version, type)✅ Oui
values.yamlValeurs par défaut✅ Oui
templates/Templates Kubernetes✅ Oui
templates/_helpers.tplFonctions partagéesRecommandé
templates/NOTES.txtMessage post-installationRecommandé
charts/Dépendances téléchargéesSi dépendances
.helmignoreExclusions du packagingRecommandé

Helm peut générer un squelette complet :

Fenêtre de terminal
helm create mon-app

Résultat :

Creating mon-app

Structure générée :

  • Répertoiremon-app/
    • Chart.yaml
    • values.yaml
    • .helmignore
    • Répertoiretemplates/
      • _helpers.tpl
      • deployment.yaml
      • service.yaml
      • serviceaccount.yaml
      • hpa.yaml
      • ingress.yaml
      • httproute.yaml
      • NOTES.txt
      • Répertoiretests/
        • test-connection.yaml

Ce fichier est la carte d'identité du chart : son nom, sa version, et ce qu'il embarque. Deux de ses champs pèsent bien plus lourd qu'ils n'en ont l'air, le name, qui décide du nom de l'archive produite, et la version, qui décide du tag lors d'une publication OCI.

Ces six lignes suffisent à faire un chart valide. Tout le reste est facultatif, ce qui ne veut pas dire inutile : les champs optionnels vus plus bas sont ce qui rend un chart trouvable et compréhensible par quelqu'un d'autre que son auteur.

apiVersion: v2
name: mon-api
description: Chart minimal pour une API REST
type: application
version: 0.1.0
appVersion: "1.0.0"

Décryptage de chaque champ :

ChampSignificationExemple
apiVersionVersion de l'API Chart (v2 pour Helm 3)v2
nameNom du chart (doit correspondre au dossier)mon-api
descriptionDescription courte pour la rechercheChart pour une API REST
typeapplication (déployable) ou library (réutilisable)application
versionVersion du chart (SemVer)0.1.0
appVersionVersion de l'application déployée"1.0.0"

Les deux types de chart, et pourquoi le second existe

Section intitulée « Les deux types de chart, et pourquoi le second existe »

Le champ type n'a que deux valeurs, et celle que vous ne connaissez pas encore résout un problème qui arrive vite : la duplication. Au troisième chart écrit dans une équipe, le même _helpers.tpl a été copié trois fois, et la convention de labels part à la dérive dès qu'une des copies évolue seule.

Un chart library est fait pour ça. Il ne contient que des helpers et ne produit aucune ressource : d'autres charts le déclarent en dépendance et appellent ses définitions. C'est la factorisation portée au niveau du dépôt, là où _helpers.tpl ne la porte qu'au niveau du chart.

TypeCe qu'il contientCe qu'il produitInstallable
applicationtemplates, values, helpersdes ressources Kubernetesoui
libraryuniquement des helpersrien par lui-mêmenon

La distinction n'est pas qu'une convention, Helm l'applique. Tenter d'installer un chart de bibliothèque, ou même d'en calculer le rendu, s'arrête net :

Fenêtre de terminal
helm install socle ./socle
Error: INSTALLATION FAILED: library charts are not installable

helm template rend exactement le même refus. C'est une bonne nouvelle : l'erreur arrive à la première seconde, pas après un déploiement silencieusement vide.

L'usage se fait donc toujours par la dépendance. Le chart de bibliothèque déclare ses helpers :

socle/Chart.yaml
apiVersion: v2
name: socle
version: 0.1.0
type: library
socle/templates/_commun.tpl
{{- define "socle.labels" -}}
app.kubernetes.io/managed-by: {{ .Release.Service }}
socle.example.invalid/version: "1"
{{- end -}}

Le chart applicatif le consomme comme n'importe quelle dépendance, puis appelle la définition par son nom :

metadata:
name: via-library
labels:
{{- include "socle.labels" . | nindent 4 }}

Rendu obtenu :

name: via-library
labels:
app.kubernetes.io/managed-by: Helm
socle.example.invalid/version: "1"

Le . passé à include est ce qui rend la chose possible : le helper reçoit le contexte du chart appelant, donc ses values et sa release. Un seul endroit décide des labels de toute une organisation, et une correction s'y fait une fois.

apiVersion: v2
name: mon-api
description: Chart minimal pour une API REST
type: application
version: 0.1.0
appVersion: "1.0.0"
# Métadonnées supplémentaires
maintainers:
- name: Stéphane Robert
email: stephane@example.com
keywords:
- api
- rest
- demo
home: https://github.com/example/mon-api
sources:
- https://github.com/example/mon-api
icon: https://example.com/icon.png

Ces champs sont affichés dans Artifact Hub et facilitent la découverte du chart.

C'est le seul fichier du chart que vos utilisateurs liront vraiment. Il définit à la fois les valeurs par défaut et, implicitement, la liste de ce qui est configurable : une clé absente d'ici est une clé que personne ne pensera à surcharger. Sa conception mérite donc autant de soin que celle des templates.

Le fichier values.yaml contient les valeurs par défaut que les utilisateurs peuvent surcharger. Un bon values.yaml doit être :

  • Auto-documenté : chaque section commentée
  • Avec des défauts sensés : le chart doit fonctionner sans surcharge
  • Organisé par composant : image, service, resources, etc.
# Configuration de l'image
image:
repository: ghcr.io/stefanprodan/podinfo
tag: "6.7.1"
pullPolicy: IfNotPresent
# Nombre de réplicas
replicaCount: 1
# Configuration du service
service:
type: ClusterIP
port: 9898
# Configuration applicative
config:
message: "Hello from Helm!"
logLevel: info
# Resources (à activer en production)
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 50m
# memory: 64Mi

Un template est un manifeste Kubernetes ordinaire dans lequel on a remplacé les valeurs variables par des expressions. C'est tout. Si vous savez écrire un Deployment à la main, vous savez écrire un template : il reste à apprendre les quelques constructions qui permettent de l'adapter au contexte, et surtout les pièges d'indentation propres au YAML généré.

Les templates Helm utilisent la syntaxe Go templates avec des extensions Sprig. Les délimiteurs sont {{ et }}.

Syntaxes de base :

SyntaxeEffet
{{ .Values.replicaCount }}Insère la valeur (avec espaces autour)
{{- .Values.replicaCount }}Supprime l'espace avant
{{ .Values.replicaCount -}}Supprime l'espace après
{{- .Values.replicaCount -}}Supprime les espaces des deux côtés

Helm fournit plusieurs objets accessibles dans les templates :

ObjetContenu
.ValuesValeurs de values.yaml + surcharges
.ReleaseInformations sur la release (nom, namespace, révision)
.ChartContenu de Chart.yaml
.CapabilitiesCapacités du cluster (versions API)

Variables .Release disponibles :

# Exemple d'utilisation
metadata:
name: {{ .Release.Name }}-config # Nom de la release
namespace: {{ .Release.Namespace }} # Namespace cible
annotations:
revision: {{ .Release.Revision | quote }} # Numéro de révision

Variables .Chart disponibles :

labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}

Ce template montre le motif central de tout chart : les valeurs figées sont remplacées par des expressions {{ .Values.* }}, et les blocs répétés, labels et sélecteurs, sont factorisés dans des helpers appelés avec include. Observez le couple include ... | nindent N : le helper rend un fragment sans indentation, et nindent l'aligne au bon niveau.

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mon-api.fullname" . }}
labels:
{{- include "mon-api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "mon-api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mon-api.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}

Le Service réutilise exactement les mêmes helpers que le Deployment, et c'est tout l'intérêt de les avoir factorisés. Le point à ne pas manquer est le lien entre les deux objets : le selector du Service s'appuie sur mon-api.selectorLabels, les mêmes labels que ceux posés sur les Pods. Si ces deux ensembles divergent, le Service ne trouve aucun Pod et le trafic n'aboutit nulle part, sans que helm lint ni helm template ne signalent quoi que ce soit : le YAML reste valide.

apiVersion: v1
kind: Service
metadata:
name: {{ include "mon-api.fullname" . }}
labels:
{{- include "mon-api.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mon-api.selectorLabels" . | nindent 4 }}

Dès qu'un chart dépasse deux ou trois templates, les mêmes blocs se répètent : les labels, le nom des ressources, le sélecteur. Les helpers permettent de les définir une seule fois, et donc de les corriger une seule fois. C'est la différence entre un chart qu'on maintient et un chart qu'on réécrit.

Les helpers évitent la duplication de code. Au lieu de répéter les labels dans chaque template, on les définit une fois dans _helpers.tpl.

Ces quatre helpers forment le socle repris par tous les charts sérieux, et helm create les génère à l'identique. Le plus subtil est mon-api.fullname : il tronque le nom à 63 caractères parce que c'est la limite d'un label DNS Kubernetes, et évite de préfixer deux fois le nom de la release quand celle-ci contient déjà le nom du chart. Notez la distinction entre labels et selectorLabels : les seconds sont un sous-ensemble stable des premiers. Un selector de Deployment est immuable après création, il ne doit donc jamais contenir la version, qui change à chaque mise à jour.

{{/*
Nom court du chart
*/}}
{{- define "mon-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Nom complet (release + chart)
Tronqué à 63 caractères (limite DNS Kubernetes)
*/}}
{{- define "mon-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Labels communs (pour tous les objets K8s)
*/}}
{{- define "mon-api.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 }}
app.kubernetes.io/name: {{ include "mon-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Labels de sélection (selector du Deployment/Service)
*/}}
{{- define "mon-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mon-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
SyntaxeUsage
{{ include "mon-api.labels" . }}✅ Recommandé : permet le piping
{{ template "mon-api.labels" . }}⚠️ Ancien style : pas de piping

Exemple avec include et piping :

metadata:
labels:
{{- include "mon-api.labels" . | nindent 4 }}

Le | nindent 4 ajoute un retour à la ligne puis indente de 4 espaces.

La bibliothèque Sprig apporte plus de cent fonctions, et vous n'en utiliserez qu'une poignée. Les sections qui suivent retiennent celles qui reviennent dans presque tous les charts publics, regroupées par usage plutôt que par ordre alphabétique.

Ces trois fonctions reviennent dans presque tous les templates. Retenez surtout la différence entre default et required : la première fournit une valeur de repli quand l'utilisateur n'a rien précisé, la seconde bloque le rendu avec un message d'erreur si la valeur manque. Utilisez required pour tout ce qui n'a pas de défaut raisonnable (un mot de passe, un nom d'hôte), afin d'échouer tôt et clairement plutôt que de déployer un manifeste incomplet.

FonctionDescriptionExemple
quoteAjoute des guillemets{{ .Values.name | quote }} → "valeur"
defaultValeur par défaut si vide{{ .Values.port | default 8080 }}
requiredErreur si valeur absente{{ required "port requis" .Values.port }}

Ces fonctions servent surtout à respecter les contraintes de nommage de Kubernetes. Les deux plus utilisées sont trunc et trimSuffix, presque toujours combinées : trunc 63 coupe à la longueur maximale d'un label, mais peut laisser un tiret en fin de chaîne, et trimSuffix "-" le retire pour produire un nom valide. C'est exactement le motif des helpers plus haut.

FonctionDescriptionExemple
upper / lowerCasse{{ "hello" | upper }} → HELLO
titlePremière lettre majuscule{{ "hello" | title }} → Hello
replaceRemplacement{{ "a-b" | replace "-" "_" }} → a_b
truncTronquer{{ "long" | trunc 3 }} → lon
trimSuffixEnlever suffixe{{ "app-v1" | trimSuffix "-v1" }} → app

Ces trois fonctions règlent le piège le plus fréquent du templating : insérer une structure dans du YAML sans casser l'indentation. La règle de choix tient en une phrase : nindent quand vous partez d'une nouvelle ligne, indent quand le contenu suit déjà quelque chose sur la ligne courante.

FonctionDescriptionQuand l'utiliser
toYamlConvertir en YAMLObjets complexes (resources, env)
nindent NNouvelle ligne + indentationAprès toYaml ou include
indent NIndentation (sans nouvelle ligne)Rarement utilisé

Exemple complet :

{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}

Rendu :

resources:
limits:
cpu: 100m
memory: 128Mi

Les structures de contrôle : écrire un template, pas le copier

Section intitulée « Les structures de contrôle : écrire un template, pas le copier »

Trois mots-clés suffisent à écrire n'importe quel chart : if, range et with. Les gabarits que vous récupérez ailleurs ne contiennent rien d'autre, et les lire cesse d'être intimidant une fois qu'on sait ce que chacun fait au point (.), cette variable qui désigne « le contexte courant » et qui est la seule vraie difficulté du langage.

Les exemples ci-dessous s'appuient tous sur le même fichier de values :

values.yaml
environnements:
- nom: dev
replicas: 1
- nom: prod
replicas: 3
ingress:
enabled: true
hote: app.example.com
journal:
niveau: info

La conditionnelle sert à rendre un bloc facultatif. C'est ce qui permet à un même chart de créer un Ingress chez l'un et pas chez l'autre, sans le forker.

data:
{{- if .Values.ingress.enabled }}
expose: "oui"
{{- else }}
expose: "non"
{{- end }}
niveau: "{{ .Values.journal.niveau | upper }}"

Rendu avec les values ci-dessus :

data:
expose: "oui"
niveau: "INFO"

Puis avec --set ingress.enabled=false :

data:
expose: "non"

Le | upper de la dernière ligne est un pipeline : la valeur traverse la fonction, exactement comme dans un shell. C'est la construction la plus fréquente dans les charts, et default, quote et nindent s'emploient de la même manière.

range boucle sur une liste et, à chaque tour, fait pointer le point sur l'élément courant. C'est ce qui permet de générer autant de ressources qu'il y a d'entrées dans les values.

{{- range .Values.environnements }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: conf-{{ .nom }}
data:
replicas: "{{ .replicas }}"
{{- end }}

Ce template ne produit pas un ConfigMap mais deux :

kind: ConfigMap
name: conf-dev
replicas: "1"
kind: ConfigMap
name: conf-prod
replicas: "3"

Notez le --- à l'intérieur de la boucle : c'est lui qui sépare les documents YAML. Sans lui, Helm rend un seul document malformé.

Quand il faut aussi connaître la position dans la liste, range accepte deux variables, l'indice et l'élément :

data:
{{- range $i, $env := .Values.environnements }}
rang-{{ $i }}: "{{ $env.nom }}"
{{- end }}
data:
rang-0: "dev"
rang-1: "prod"

Les noms $i et $env sont des variables de template, déclarées avec :=. Elles gardent leur valeur dans tout le bloc, ce qui les rend précieuses dès que le point change de sens.

with déplace le point sur une sous-partie des values. Il évite de répéter .Values.ingress. dix fois :

data:
{{- with .Values.ingress }}
hote: "{{ .hote }}"
tls: "{{ .tls }}"
{{- end }}
data:
hote: "app.example.com"
tls: "true"

Et c'est là que presque tout le monde se fait prendre. À l'intérieur du bloc, le point ne désigne plus la racine : tout ce qui n'est pas sous .Values.ingress devient inaccessible. Ce template, qui paraît anodin, ne compile pas :

{{- with .Values.ingress }}
hote: "{{ .hote }}"
release: "{{ .Release.Name }}" # ❌ .Release n'existe plus ici
{{- end }}
Error: templates/t.yaml:8:23
executing "templates/t.yaml" at <.Release.Name>:
nil pointer evaluating interface {}.Name

Le nil pointer evaluating interface {} est le message le plus fréquent du débutant, et il vient neuf fois sur dix d'un with. La parade tient en un caractère : $ désigne toujours la racine, où que vous vous trouviez dans le template.

{{- with .Values.ingress }}
release: "{{ $.Release.Name }}" # rend « c », le nom de la release
{{- end }}

required : refuser de rendre plutôt que rendre faux

Section intitulée « required : refuser de rendre plutôt que rendre faux »

Une value absente ne provoque pas d'erreur : Helm rend une chaîne vide et le manifeste part au cluster, amputé. La fonction required inverse ce comportement et arrête le rendu avec le message de votre choix.

data:
hote: "{{ required "ingress.hote est obligatoire pour exposer l'application" .Values.ingress.hote }}"
Error: execution error at (templates/t.yaml:6:12): ingress.hote est obligatoire pour exposer l'application

Le message est celui que vous écrivez. C'est la différence entre un utilisateur de votre chart qui comprend en dix secondes, et un qui ouvre vos templates. Réservez required aux values sans lesquelles le déploiement n'a aucun sens, et donnez une valeur par défaut au reste avec | default.

Le tiret des accolades : lisibilité, pas validité

Section intitulée « Le tiret des accolades : lisibilité, pas validité »

{{- supprime l'espace blanc qui précède la balise, -}} celui qui la suit. Sans lui, chaque ligne de contrôle laisse une ligne vide dans le rendu :

# sans tiret # avec {{-
data: data:
a: "1" a: "1"
b: "2"
b: "2"

Le YAML reste valide dans les deux cas, vérification faite : c'est une question de lisibilité, pas de correction. La nuance compte, car beaucoup de charts mettent des tirets partout par superstition. La règle utile est plus simple : mettez-en sur les lignes qui ne portent que du contrôle, et relisez le rendu avec helm template plutôt que de deviner.

Deux fonctions qui savent ce que le template ignore

Section intitulée « Deux fonctions qui savent ce que le template ignore »

tpl et .Capabilities répondent à des questions que les structures de contrôle ne peuvent pas poser, et les deux se rencontrent dès qu'un chart sert à plusieurs équipes.

tpl évalue une chaîne comme un template. Sans elle, une value contenant des accolades est rendue littéralement :

values.yaml
message: "Bienvenue sur {{ .Values.hote }}, release {{ .Release.Name }}"
data:
brut: "{{ .Values.message }}"
evalue: "{{ tpl .Values.message . }}"
data:
brut: "Bienvenue sur {{ .Values.hote }}, release {{ .Release.Name }}"
evalue: "Bienvenue sur app.example.com, release mon-app"

Le second argument est le contexte d'évaluation, presque toujours le point. C'est ce qui permet à l'utilisateur de votre chart d'écrire des values paramétrées par d'autres values, sans que vous ayez à prévoir chaque combinaison.

.Capabilities interroge le cluster plutôt que les values. Son usage courant est de rendre une ressource seulement si son API existe :

{{- if .Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1/HTTPRoute" }}
# ... la HTTPRoute
{{- end }}

.Capabilities interroge le cluster, et helm template ne parle à aucun cluster : la réponse est donc false pour tout. Mesuré sur un cluster où l'Ingress existe bel et bien :

CommandeAPIVersions.Has "networking.k8s.io/v1/Ingress"
helm templatefalse
helm install --dry-run=servertrue

La conséquence est plus gênante qu'une simple valeur fausse : un bloc conditionné par .Capabilities disparaît de votre prévisualisation locale et réapparaît à l'installation. Vous validez donc un rendu qui n'est pas celui qui sera appliqué. Ces charts-là se valident avec --dry-run=server, seul régime où la question est posée au cluster.

.Capabilities.KubeVersion mérite un traitement à part, parce qu'il répond dans les deux cas et paraît donc fiable. Il ne l'est pas : hors ligne, la valeur ne vient pas du cluster mais d'un défaut porté par le binaire. Le drapeau --kube-version le démontre, puisqu'il la change sans qu'aucun cluster ne soit consulté :

Fenêtre de terminal
helm template sonde ./mon-chart # kubeVersion: "v1.37.0"
helm template sonde ./mon-chart --kube-version 1.30.0 # kubeVersion: "v1.30.0"

Un template qui compare KubeVersion pour choisir une API teste donc, en local, la version que Helm suppose, pas celle de votre cluster : le rendu peut être juste chez vous et faux en production. Le même drapeau devient en revanche précieux en intégration continue, où il permet de vérifier qu'un chart rend correctement sur plusieurs versions de Kubernetes sans en provisionner aucune.

Embarquer un fichier de configuration dans le chart

Section intitulée « Embarquer un fichier de configuration dans le chart »

Recopier un nginx.conf de quarante lignes à l'intérieur d'un template est la mauvaise réponse à un besoin courant. Le fichier devient illisible, perd la coloration syntaxique de son éditeur, et son indentation se mélange à celle du YAML. L'objet .Files évite tout cela : le fichier reste un fichier, dans le chart, et le template va le chercher.

mon-chart/
├── config/
│ ├── nginx.conf
│ └── alertes.ini
└── templates/
└── configmap.yaml
data:
nginx.conf: |
{{ .Files.Get "config/nginx.conf" | indent 4 }}
data:
nginx.conf: |
server {
listen 80;
}

Le | indent 4 est obligatoire : sans lui, le contenu s'insère à la colonne zéro et casse le bloc YAML. C'est la cause la plus fréquente d'un error converting YAML to JSON sur un chart qui semblait correct.

Pour embarquer plusieurs fichiers d'un coup, .Files.Glob parcourt un motif, et la variable $ redevient nécessaire puisque range a déplacé le point :

{{- range $chemin, $_ := .Files.Glob "config/*.ini" }}
{{ base $chemin }}: |
{{ $.Files.Get $chemin | indent 4 }}
{{- end }}

Trois commandes, du moins coûteux au plus engageant. helm lint lit vos fichiers, helm template montre ce qu'ils produisent, helm package fige le résultat dans une archive. Les enchaîner dans cet ordre attrape les erreurs au niveau où elles coûtent le moins cher à corriger, et les deux premières ne demandent aucun cluster.

Fenêtre de terminal
helm lint mon-api

Résultat (succès) :

==> Linting mon-api
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed

Avec --strict pour plus de rigueur :

Fenêtre de terminal
helm lint mon-api --strict

Les [INFO] deviennent des erreurs en mode strict.

Fenêtre de terminal
helm template ma-release mon-api

Cette commande génère le YAML sans l'appliquer au cluster. Idéal pour vérifier le rendu.

Options utiles :

OptionEffet
--debugAffiche les infos de debug
--show-only templates/deployment.yamlRend un seul template
-n productionSimule le namespace cible
--set replicaCount=3Surcharge une valeur

Exemple : vérifier un template spécifique

Fenêtre de terminal
helm template ma-release mon-api --show-only templates/deployment.yaml

Résultat :

mon-api/templates/deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ma-release-mon-api
labels:
helm.sh/chart: mon-api-0.1.0
app.kubernetes.io/name: mon-api
app.kubernetes.io/instance: ma-release
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
...
Fenêtre de terminal
helm package mon-api

Résultat :

Successfully packaged chart and saved it to: /path/to/mon-api-0.1.0.tgz

Cette archive .tgz peut être publiée dans un registry.

Le nom de l'archive suit une règle qui surprend au premier essai : il vient du champ name du Chart.yaml, jamais du nom du dossier. Un dossier appelé src dont le Chart.yaml déclare name: paquet produit donc paquet-0.1.0.tgz, et rien dans la sortie ne rappelle le dossier d'origine :

/chemin/paquet-0.1.0.tgz
helm package ./src

Le piège apparaît au moment où l'on duplique un chart pour en dériver un second : copier mon-api/ vers mon-api-interne/ sans changer le name fait produire deux archives du même nom, et la seconde écrase la première sans un mot. Changez le name en même temps que le dossier, c'est le seul champ qui compte ici.

Helm recommande d'utiliser les labels standard app.kubernetes.io/* :

LabelContenuExemple
app.kubernetes.io/nameNom de l'applicationmon-api
app.kubernetes.io/instanceNom de la releaseprod-mon-api
app.kubernetes.io/versionVersion de l'application1.0.0
app.kubernetes.io/componentComposant dans l'appfrontend
app.kubernetes.io/part-ofApplication parentemon-stack
app.kubernetes.io/managed-byOutil de gestionHelm
helm.sh/chartChart et versionmon-api-0.1.0

Ces labels permettent de filtrer et organiser les ressources dans le cluster.

Ces quatre symptômes se produisent au rendu, donc avant le moindre appel au cluster : ils se reproduisent à l'identique avec helm template, hors ligne. C'est une bonne nouvelle pour le diagnostic, puisque vous pouvez les rejouer autant de fois que nécessaire sans rien déployer.

SymptômeCause probableSolution
parse errorSyntaxe Go template invalideVérifier {{ }} et tirets
nil pointerAccès à une valeur inexistanteUtiliser default ou if
YAML invalideIndentation incorrecteUtiliser nindent avec la bonne valeur
Nom trop longDépasse 63 caractèresUtiliser trunc 63 | trimSuffix "-"
required échoueValeur obligatoire manquanteFournir la valeur avec --set

Ce lab construit un chart de zéro, sans passer par helm create, justement pour voir ce que le squelette cache. Vous écrirez le Chart.yaml, un values.yaml, un template et un helper, puis vous validerez le tout sans cluster. Comptez une vingtaine de minutes.

  1. Créer le squelette

    Fenêtre de terminal
    mkdir -p mon-api/templates
  2. Créer Chart.yaml

    Fenêtre de terminal
    cat > mon-api/Chart.yaml << 'EOF'
    apiVersion: v2
    name: mon-api
    description: Mon premier chart Helm
    type: application
    version: 0.1.0
    appVersion: "1.0.0"
    EOF
  3. Créer values.yaml

    Fenêtre de terminal
    cat > mon-api/values.yaml << 'EOF'
    image:
    repository: ghcr.io/stefanprodan/podinfo
    tag: "6.7.1"
    pullPolicy: IfNotPresent
    replicaCount: 1
    service:
    type: ClusterIP
    port: 9898
    config:
    message: "Hello from my first chart!"
    EOF
  4. Créer _helpers.tpl

    Fenêtre de terminal
    cat > mon-api/templates/_helpers.tpl << 'EOF'
    {{- define "mon-api.fullname" -}}
    {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
    {{- end }}
    {{- define "mon-api.labels" -}}
    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 }}
    {{- define "mon-api.selectorLabels" -}}
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    {{- end }}
    EOF
  5. Créer deployment.yaml

    Fenêtre de terminal
    cat > mon-api/templates/deployment.yaml << 'EOF'
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: {{ include "mon-api.fullname" . }}
    labels:
    {{- include "mon-api.labels" . | nindent 4 }}
    spec:
    replicas: {{ .Values.replicaCount }}
    selector:
    matchLabels:
    {{- include "mon-api.selectorLabels" . | nindent 6 }}
    template:
    metadata:
    labels:
    {{- include "mon-api.selectorLabels" . | nindent 8 }}
    spec:
    containers:
    - name: {{ .Chart.Name }}
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    ports:
    - containerPort: {{ .Values.service.port }}
    EOF
  6. Créer service.yaml

    Fenêtre de terminal
    cat > mon-api/templates/service.yaml << 'EOF'
    apiVersion: v1
    kind: Service
    metadata:
    name: {{ include "mon-api.fullname" . }}
    labels:
    {{- include "mon-api.labels" . | nindent 4 }}
    spec:
    type: {{ .Values.service.type }}
    ports:
    - port: {{ .Values.service.port }}
    targetPort: {{ .Values.service.port }}
    selector:
    {{- include "mon-api.selectorLabels" . | nindent 4 }}
    EOF
  7. Valider avec lint et template

    Fenêtre de terminal
    helm lint mon-api
    helm template test-release mon-api
  8. Installer et vérifier

    Fenêtre de terminal
    helm install mon-api-test mon-api -n default
    kubectl get all -l app.kubernetes.io/instance=mon-api-test
  9. Nettoyer

    Fenêtre de terminal
    helm uninstall mon-api-test

Critères de réussite :

  • helm lint passe sans erreur
  • helm template génère du YAML valide
  • Le chart s'installe et crée un pod Running
  • Les labels sont correctement appliqués

  • Un chart = Chart.yaml + values.yaml + templates/
  • helm create génère un squelette complet (utile en production)
  • _helpers.tpl centralise les fonctions réutilisables (labels, noms)
  • Utilisez include plutôt que template pour pouvoir piper
  • toYaml | nindent N est le pattern pour injecter des objets complexes
  • helm lint et helm template sont vos meilleurs amis pour débugger
  • Les labels app.kubernetes.io/* sont la convention recommandée

Six questions sur l'écriture d'un template : le piège de portée de with, le rôle de required, ce qu'est un chart de type library, et ce que .Files refuse de lire.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

7 questions
5 min.
80% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn