Écrire un chart
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épertoire | Rôle |
|---|---|
Chart.yaml | l'identité du chart : nom, version, version de l'application, description. Obligatoire. |
values.yaml | les valeurs par défaut, c'est-à-dire l'interface de configuration. |
templates/ | les modèles qui produisent les manifestes. |
templates/_helpers.tpl | des fragments réutilisables (par convention, un fichier dont le nom commence par _ ne produit aucun objet). |
templates/NOTES.txt | le message affiché après l'installation. |
.helmignore | les fichiers à exclure de l'archive. |
values.schema.json | un 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: v2est le format de chart de Helm 3 et de Helm 4. Les chartsv1(Helm 2) sont obsolètes.nameest le nom du chart ; il doit correspondre au nom du répertoire, en minuscules, avec des tirets.typevautapplication(défaut) pour un chart installable, oulibrarypour un chart qui ne fournit que des fonctions à d'autres (leçon 7).versionest 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.appVersionest 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
appVersionquand 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 deversion). Une nouvelle valeur ou un nouveau modèle fait bougerversion(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, pasreplica_count). - Des maps plutôt que des clés à plat :
image.repositoryetimage.tagplutôt queimageRepositoryetimageTag. Un groupe de valeurs liées se lit et se surcharge ensemble. - Des commentaires sur chaque valeur, parce que
helm show valuesest 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: httpCe 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: 16MiLes 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). Écrirenamespace: signalementsdans un modèle empêcherait de déployer ailleurs. replicaCount, l'image et le port sont des valeurs.DATABASE_URLvient d'un Secret existant, désigné pardatabaseSecretName, etrequiredfait é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
resourcessont insérées partoYaml: la valeur est un petit arbre YAML, quetoYamlréécrit en texte et quenindent 12indente 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 blocsecurityContextlibre. - 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.tagvautappVersion, une étiquette que l'éditeur peut déplacer. Pour une garantie forte, acceptez aussiimage.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.
requiredcomme 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 templateavant 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 lintavec des valeurs de test,helm templateavec chaque jeu de valeurs, une validation des manifestes rendus contre le schéma de Kubernetes (par un outil commekubeconform), 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.tpletNOTES.txt,.helmignore; on part des manifestes existants et l'on ne paramètre que ce qui varie. versionest la version du chart (SemVer, identifie la publication),appVersioncelle de l'application ; elles évoluent séparément.values.yamlest l'interface publique :camelCase, maps, commentaires, peu de clés,requiredpour les valeurs sans défaut possible, activation explicite des options.- Les fragments de
_helpers.tplsont 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
selectoret le Service) et les communes (version, chart, dans les métadonnées). helm lint(avec les valeurs obligatoires) vérifie la structure ;helm templatemontre le rendu ;helm install --dry-run=serverfait valider les objets par l'API sans rien persister ;helm packageproduit<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.godanshelm/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.