Le chart Helm, écrit de zéro
helm create génère 11 fichiers et une centaine de lignes de conditions que personne dans l'équipe n'a écrites ni ne relira. Six mois plus tard, quelqu'un active serviceAccount.create: false sans savoir ce que ça fait, et le pod perd ses droits. Un chart écrit à la main, limité à ce que le service utilise, se relit en dix minutes et se teste. Ici : 8 objets Kubernetes, 1 fichier de valeurs, 9 tests de rendu.
Le mécanisme
Helm combine des templates Go et des valeurs pour produire des manifestes, les envoie à l'API Kubernetes et garde chaque version rendue dans un Secret de type helm.sh/release.v1 du namespace. C'est ce stockage qui rend possible helm rollback (leçon 6).
flowchart LR V["values.yaml + --set"] --> T["templates/*.yaml + _helpers.tpl"] T -->|"helm template / lint"| R["8 manifestes rendus"] R -->|"helm upgrade --install --wait"| API["API Kubernetes"] API --> REL["Révision N stockée dans le namespace"] KC["kubectl create secret (hors chart)"] --> S["Secret inference-gemini"] S -.->|"secretKeyRef"| R
| Fichier | Objet | Ce qu'il garantit |
|---|---|---|
deployment.yaml | Deployment | sondes, ressources, non-root, arrêt propre, mise à jour sans perte |
service.yaml | Service | adresse stable devant les pods |
ingress.yaml | Ingress | entrée HTTP par nom d'hôte, délai de lecture de 60 s pour les réponses lentes du modèle |
configmap.yaml | ConfigMap | modèle, limites, backend ; aucune donnée secrète |
serviceaccount.yaml | ServiceAccount | identité dédiée, sans jeton monté |
hpa.yaml | HorizontalPodAutoscaler | 2 à 6 pods (leçon 5) |
pdb.yaml | PodDisruptionBudget | au moins 1 pod pendant un drain (leçon 4) |
networkpolicy.yaml | NetworkPolicy | seul l'Ingress entre, seuls DNS et HTTPS sortent (leçon 4) |
La mise en œuvre
La clé n'entre jamais dans le chart
Le chart référence un Secret par son nom et refuse de se rendre sans lui quand le backend est Gemini :
{{- if and (eq .Values.config.LLM_BACKEND "gemini") (not .Values.existingSecret) }}
{{- fail "existingSecret est obligatoire avec LLM_BACKEND=gemini (créez le Secret avec kubectl, jamais dans le chart)" }}
{{- end }}
...
- name: GEMINI_API_KEY
valueFrom:
secretKeyRef: { name: {{ .Values.existingSecret }}, key: GEMINI_API_KEY, optional: {{ ne .Values.config.LLM_BACKEND "gemini" }} }Le Secret est créé par make secret, qui lit la clé depuis .env et la passe par l'entrée standard : elle n'apparaît ni dans l'historique du shell, ni dans la liste des processus, ni dans une révision Helm.
secret:
@KEY="$${GEMINI_API_KEY:-$$(sed -n 's/^GEMINI_API_KEY=//p' .env 2>/dev/null)}"; \
test -n "$$KEY" && test "$$KEY" != "remplacez-moi" || { echo "GEMINI_API_KEY absente de .env"; exit 1; }; \
printf '%s' "$$KEY" | $(K) -n $(NS) create secret generic inference-gemini \
--from-file=GEMINI_API_KEY=/dev/stdin --dry-run=client -o yaml | $(K) apply -f - >/dev/nullLe Makefile n'inclut ni n'exporte .env : la clé n'est lue que dans cette cible, et une variable passée en ligne de commande (GEMINI_API_KEY=... make secret) l'emporte toujours sur le fichier.
Un changement de configuration redémarre les pods
Une variable d'environnement lue depuis une ConfigMap n'est relue qu'au démarrage du conteneur. Sans précaution, helm upgrade --set config.MODEL=... met à jour la ConfigMap et les pods gardent l'ancien modèle. L'empreinte de la ConfigMap dans les annotations du pod force un nouveau ReplicaSet :
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}L'HPA possède le nombre de répliques
Si le Deployment déclare replicas: 2 et que l'HPA en a créé 5, chaque helm upgrade remet 2, puis l'HPA remonte : trois pods tués pour rien en pleine charge. Le template omet replicas quand l'autoscaling est actif :
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}Tester le rendu sans cluster
tests/test_chart.py appelle helm template et vérifie les garanties, pas la syntaxe :
def test_never_renders_a_secret():
docs = render()
assert "Secret" not in docs
env = docs["Deployment"]["spec"]["template"]["spec"]["containers"][0]["env"]
key = next(e for e in env if e["name"] == "GEMINI_API_KEY")
assert key["valueFrom"]["secretKeyRef"]["name"] == "inference-gemini"
def test_hpa_owns_replicas():
docs = render()
assert "replicas" not in docs["Deployment"]["spec"]Puis le déploiement réel et le test de bout en bout (scripts/e2e.py) :
make up # cluster + addons + image + Secret + helm upgrade --install --wait
make e2eCe qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
Après helm upgrade, les pods utilisent l'ancien modèle | ConfigMap modifiée, pods non redémarrés | annotation checksum/config |
| À chaque déploiement, le nombre de pods retombe à 2 puis remonte | replicas rendu alors que l'HPA est actif | omettre replicas si autoscaling.enabled |
Clé d'API retrouvée dans helm get values d'une ancienne révision | clé passée en --set | Secret créé hors chart, référencé par nom |
helm upgrade réussit mais le service est en panne | upgrade sans --wait : Helm n'attend pas la readiness | --wait --timeout (et --atomic en CI) |
Noms d'objets inference-inference | modèle release-chart appliqué sans test | réutiliser le nom de release s'il contient déjà le nom du chart (_helpers.tpl) |
La règle de décision
- Écrivez le chart à la main tant qu'il sert un seul service ; un chart « bibliothèque » partagé se justifie à partir de trois services qui ont la même forme.
- Aucun secret dans
values.yaml, ni dans--set, ni dans Git : le chart référence, il ne contient pas. - Chaque garantie de production (non-root, sondes, pas de Secret, HPA propriétaire des répliques) a un test de rendu qui échoue si quelqu'un la retire.
Pratique
Un développeur propose d'ajouter templates/secret.yaml avec la clé passée par --set gemini.apiKey=$KEY dans la CI, « parce que c'est plus simple qu'un kubectl à part ». Quel argument tranche ?
Helm stocke chaque révision rendue, valeurs comprises, dans un Secret de release. L'ancienne clé reste dans l'historique jusqu'à purge des révisions. Helm crée très bien des Secrets, l'ordre de création est géré (le pod attend son Secret), et lint ne détecte pas les secrets.
Vous passez MODEL de gemini-3.5-flash à gemini-3.5-flash-lite avec helm upgrade. La ConfigMap est à jour, mais les réponses indiquent toujours l'ancien modèle une heure plus tard. Que manque-t-il ?
Les variables d'environnement issues d'une ConfigMap sont figées au démarrage du conteneur. Sans modification du template de pod, le Deployment ne crée pas de nouveau ReplicaSet. L'empreinte de la ConfigMap dans les annotations change le template à chaque changement de configuration. La ConfigMap elle-même est déjà à jour, et l'image n'est pas en cause.
Exercice
Ajoutez au chart une valeur ingress.tlsSecret qui, si elle est renseignée, ajoute une section tls à l'Ingress. Écrivez le test de rendu correspondant (avec et sans la valeur).
Voir la solution
{{- with .Values.ingress.tlsSecret }}
tls:
- hosts: [{{ $.Values.ingress.host | quote }}]
secretName: {{ . }}
{{- end }}def test_tls_is_optional():
assert "tls" not in render()["Ingress"]["spec"]
tls = render("ingress.tlsSecret=assistant-tls")["Ingress"]["spec"]["tls"]
assert tls[0]["secretName"] == "assistant-tls"Ajoutez tlsSecret: "" dans values.yaml pour documenter la valeur. Le certificat lui-même est créé hors chart (cert-manager), pour la même raison que la clé Gemini.
Résultat obtenu
Exécution du 2026-10-15 :
$ helm lint --strict chart/inference
==> Linting chart/inference
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
$ helm template inference chart/inference | grep "^kind:"
kind: NetworkPolicy
kind: PodDisruptionBudget
kind: ServiceAccount
kind: ConfigMap
kind: Service
kind: Deployment
kind: HorizontalPodAutoscaler
kind: Ingress
$ make e2e TAG=0.2.0
[OK] pods prêts : 2 : inference-6ddbf6f4ff-2b6g6, inference-6ddbf6f4ff-sz8b2
[OK] répartis sur plusieurs nœuds : lab-k8s-ia-worker, lab-k8s-ia-worker2
[OK] conteneur non-root : uid=10001
[OK] aucun Secret dans la release Helm
[OK] réponse Gemini via l'Ingress : HTTP 200
pod=inference-6ddbf6f4ff-2b6g6 version=0.2.0 modèle=gemini-3.5-flash latence=3953 ms
Assurez-vous d'abord que vous êtes bien connecté au Wi-Fi de l'hôtel et que vous avez validé
la page d'authentification (portail captif) dans votre navigateur internet. Certains réseaux
d'hôtel bloquent les protocoles VPN ; essayez de tester votre connexion en utilisant le partage
de données de votre smartphone. [...]
[OK] backend réel
[OK] NetworkPolicy bloque un pod hors Ingress : connexion expirée après 3 s
e2e : tout est vertJe sais faire
- Écrire un chart sans
helm create, limité aux objets utilisés - Référencer un Secret créé hors chart et faire échouer le rendu s'il manque
- Redémarrer les pods à chaque changement de configuration
- Tester les garanties du chart avec
helm templateet pytest