Aller au contenu
Conteneurs & Orchestration medium

10 patterns Helm pour des charts production-ready

60 min de lecture

logo helm

Ce guide présente les 10 patterns essentiels pour transformer un chart Helm basique en chart production-ready. Vous apprendrez à ajouter les health probes, les limites de ressources, la sécurité des conteneurs, l'autoscaling, l'Ingress avec TLS, et les hooks. À la fin, votre chart respectera les standards Kubernetes pour la production.

Ces patterns s'appliquent à un chart existant : ils ajoutent des blocs aux templates et des clés aux values, ils ne créent pas la structure. Le chart de référence est celui du module précédent, mais n'importe quel chart généré par helm create convient.

  • Avoir créé un chart basique (voir module H2-01)
  • Connaître les concepts Kubernetes : Deployment, Service, ConfigMap
  • Comprendre les values et le templating Helm

Voici les 10 patterns que tout chart production-ready doit implémenter :

#PatternProblème résoluImpact production
1Health ProbesDétection des conteneurs défaillantsHaute dispo, auto-healing
2ResourcesConteneurs gourmands, évictionStabilité, QoS
3SecurityContextConteneurs root, faillesSécurité, compliance
4ServiceAccountPermissions excessivesPrincipe moindre privilège
5ConfigMapConfig en dur dans l'imageFlexibilité, 12-factor
6IngressExposition HTTP sans TLSAccès sécurisé
7Autoscaling (HPA)Charge variableÉlasticité, coûts
8SchedulingRépartition non optimaleHA, performance
9PodAnnotationsIntégrations manquantesObservabilité
10HooksMigrations, initialisationsOrchestration

Sans probes, Kubernetes considère qu'un conteneur démarré est un conteneur en bonne santé, y compris quand l'application est figée. Ce pattern rend les trois sondes configurables depuis les values et ajoute un drapeau enabled, indispensable pour installer le chart avec une application qui n'expose aucun endpoint de santé.

Les probes (sondes) permettent à Kubernetes de vérifier l'état de vos conteneurs :

ProbeQuestion poséeConséquence si échec
livenessProbe"Le conteneur est-il vivant ?"Kubernetes le redémarre
readinessProbe"Le conteneur est-il prêt à recevoir du trafic ?"Retiré du Service (plus de trafic)
startupProbe"Le conteneur a-t-il fini de démarrer ?"Les autres probes attendent

Chaque sonde porte son propre bloc, drapeau enabled en tête. Notez la startupProbe désactivée par défaut : son failureThreshold: 30 combiné à periodSeconds: 5 laisse jusqu'à 150 secondes à une application lente avant que la liveness ne commence à la redémarrer.

livenessProbe:
enabled: true
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
enabled: true
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
enabled: false
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30

Décryptage des paramètres :

ParamètreSignificationValeur typique
initialDelaySecondsAttente avant première vérification5-30s selon l'app
periodSecondsIntervalle entre vérifications10s
timeoutSecondsTimeout de la requête3s
failureThresholdÉchecs consécutifs avant action3

Le bloc if conditionne le rendu de la sonde entière : à false, aucun champ livenessProbe n'apparaît dans le Deployment. Chaque paramètre est repris explicitement, ce qui interdit d'ajouter une option sans modifier le template.

{{- if .Values.livenessProbe.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.livenessProbe.httpGet.path }}
port: {{ .Values.livenessProbe.httpGet.port }}
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}

--show-only restreint la sortie au fichier demandé plutôt que de rendre tout le chart. Le point à contrôler est que les deux sondes visent bien des chemins distincts.

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

Extrait du résultat :

livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3

Un conteneur sans plafond peut absorber toute la mémoire de son nœud et provoquer l'éviction de ses voisins. Ce pattern expose un bloc resources unique, recopié tel quel dans le Deployment, avec un mécanisme qui l'omet entièrement quand l'utilisateur le laisse vide.

Sans limites, un conteneur peut consommer toutes les ressources du nœud et impacter les autres applications. Kubernetes utilise les resources pour :

  • Requests : réserver des ressources minimales (scheduling)
  • Limits : plafonner la consommation (protection)

Les valeurs proposées correspondent à une petite API : un dixième de cœur réservé, un plafond au double. La requests sert au scheduler pour placer le pod, la limits au kubelet pour le contraindre une fois lancé.

resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

Notation des ressources :

NotationSignification
100m100 millicores = 0.1 CPU
11 CPU complet
128Mi128 Mébioctets
1Gi1 Gibioctet

with remplace le contexte courant par la valeur qu'il teste, d'où le point seul passé à toYaml. C'est aussi la source d'erreur habituelle : à l'intérieur du bloc, .Values n'est plus accessible sans repasser par $.

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

Pourquoi ce pattern ?

  • Si resources: {} (vide), le bloc n'est pas rendu
  • toYaml convertit l'objet YAML proprement
  • nindent 2 ajoute une nouvelle ligne + 2 espaces d'indentation

toYaml recopie le bloc des values sans en vérifier le contenu. Une unité mal écrite comme 256Mib traverserait donc le rendu sans erreur et ne serait rejetée qu'à l'apply, par l'API server.

resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

Par défaut, un conteneur démarre en root avec les capabilities Linux que lui laisse le runtime. Ce pattern expose deux blocs distincts, l'un au niveau du pod, l'autre au niveau du conteneur, et applique le moindre privilège sans rien changer à l'image.

Par défaut, un conteneur peut s'exécuter en root et avoir des privilèges excessifs. Le securityContext applique le principe du moindre privilège.

Les deux blocs portent le même nom dans le manifest Kubernetes mais ne se placent pas au même endroit : podSecurityContext sous spec, securityContext sous chaque conteneur. Certains champs existent des deux côtés, et la valeur définie au niveau du conteneur l'emporte alors sur celle du pod.

NiveauScopeExemple d'usage
podSecurityContextTous les conteneurs du podfsGroup, runAsNonRoot
securityContextUn conteneur spécifiquecapabilities, readOnlyRootFilesystem

L'UID 65534 correspond au compte nobody présent dans la plupart des images de base. fsGroup s'applique aux volumes montés, ce qui évite qu'un conteneur non root se retrouve devant des fichiers qu'il n'a pas le droit de lire.

podSecurityContext:
fsGroup: 65534
runAsNonRoot: true
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534

Décryptage :

ParamètreSignification
runAsNonRoot: trueRefuse de démarrer si le conteneur tente de tourner en root
runAsUser: 65534UID nobody (utilisateur sans privilèges)
allowPrivilegeEscalation: falseEmpêche d'obtenir plus de droits qu'au départ
capabilities.drop: [ALL]Supprime toutes les capabilities Linux
readOnlyRootFilesystem: trueSystème de fichiers en lecture seule

Les deux blocs with sont indépendants : l'un peut rester vide sans empêcher l'autre de se rendre. L'indentation diffère selon la profondeur, nindent 4 sous spec et nindent 8 sous le conteneur, et une erreur ici produit un YAML invalide.

spec:
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 4 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}

Les champs ressortent triés par ordre alphabétique, toYaml ne conservant pas l'ordre du fichier de values. Vérifiez la présence de drop: [ALL] et de readOnlyRootFilesystem, les deux réglages qui empêchent le plus souvent une image non préparée de démarrer.

spec:
securityContext:
fsGroup: 65534
runAsNonRoot: true
containers:
- name: mon-api
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534

Tout pod s'exécute sous un ServiceAccount, celui du namespace par défaut s'il n'en désigne aucun. Ce pattern crée un compte dédié au chart, coupe le montage automatique de son token, et laisse malgré tout la possibilité de réutiliser un compte géré ailleurs.

Chaque pod Kubernetes a un ServiceAccount qui définit ses permissions d'accès à l'API Kubernetes. Le compte default est partagé par tous les pods sans SA explicite.

Problème : si vous montez un token dans un pod compromis, l'attaquant hérite des permissions du SA.

Un champ name laissé vide déclenche la génération d'un nom à partir de celui de la release. Les annotations servent aux intégrations d'identité cloud, où l'annotation porte le rôle IAM que le compte est autorisé à endosser.

serviceAccount:
create: true
automount: false
annotations: {}
name: ""
ParamètreSignification
createCréer un SA dédié (vs utiliser default)
automountMonter automatiquement le token ? (false recommandé)
annotationsPour IAM (AWS IRSA, GCP Workload Identity)
nameNom personnalisé (sinon généré automatiquement)

Ce helper centralise la logique en un seul endroit, appelé à la fois par le Deployment et par le template du ServiceAccount. Sans lui, les deux fichiers dupliqueraient la même condition et finiraient par diverger au premier changement.

{{/*
Nom du ServiceAccount à utiliser
*/}}
{{- define "mon-api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mon-api.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Logique :

  • Si create: true → utilise le nom personnalisé ou génère un nom
  • Si create: false → utilise le nom fourni ou default

Le if englobe le fichier entier : avec create: false, Helm produit un document vide, simplement ignoré à l'installation. automountServiceAccountToken est ici piloté depuis les values, alors que ce champ peut aussi être posé au niveau du pod.

{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "mon-api.serviceAccountName" . }}
labels:
{{- include "mon-api.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

Avec serviceAccount.create: true :

apiVersion: v1
kind: ServiceAccount
metadata:
name: test-release-mon-api
labels:
helm.sh/chart: mon-api-0.2.0
app.kubernetes.io/name: mon-api
app.kubernetes.io/instance: test-release
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/managed-by: Helm
automountServiceAccountToken: false

Avec serviceAccount.create: false :

Fenêtre de terminal
helm template test-release mon-api --set serviceAccount.create=false \
--show-only templates/deployment.yaml | grep serviceAccountName
serviceAccountName: default

Une configuration gravée dans l'image impose de reconstruire celle-ci à chaque changement de valeur. Ce pattern génère une ConfigMap à partir d'un dictionnaire libre placé dans les values, puis l'injecte en bloc dans le conteneur avec envFrom.

Le 12-factor app recommande de stocker la configuration dans l'environnement, pas dans le code. Les ConfigMaps permettent de modifier la config sans rebuilder l'image.

Le sous-bloc data accepte n'importe quelle clé, le template les parcourant sans en connaître la liste. Les valeurs numériques comme API_TIMEOUT sont écrites entre guillemets, une ConfigMap ne stockant que des chaînes de caractères.

config:
enabled: true
data:
LOG_LEVEL: "info"
APP_ENV: "production"
API_TIMEOUT: "30"

range sur un dictionnaire renvoie les clés triées, ce qui rend le manifest stable d'un rendu à l'autre. Le filtre quote n'est pas décoratif : sans lui, une valeur comme on ou yes serait interprétée comme un booléen par le parseur YAML.

{{- if .Values.config.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mon-api.fullname" . }}-config
labels:
{{- include "mon-api.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.config.data }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}

Le pattern range expliqué :

  • range $key, $value := .Values.config.data parcourt chaque paire clé-valeur
  • {{ $key }} et {{ $value }} sont des variables locales
  • | quote met la valeur entre guillemets (sécurité YAML)

envFrom importe toutes les clés de la ConfigMap comme variables d'environnement, sans avoir à les déclarer une par une. En contrepartie, une clé dont le nom n'est pas un identifiant valide est ignorée sans message.

{{- if .Values.config.enabled }}
envFrom:
- configMapRef:
name: {{ include "mon-api.fullname" . }}-config
{{- end }}

Le rendu ne contient que les clés déclarées dans les values. Piège classique de ce pattern : mettre à jour la ConfigMap ne redémarre pas les pods, les variables d'environnement restant celles chargées au démarrage du conteneur.

apiVersion: v1
kind: ConfigMap
metadata:
name: test-release-mon-api-config
labels:
helm.sh/chart: mon-api-0.2.0
app.kubernetes.io/name: mon-api
app.kubernetes.io/instance: test-release
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/managed-by: Helm
data:
APP_ENV: "production"
LOG_LEVEL: "info"

Un Service ne sort pas du cluster de lui-même : il faut un Ingress et un controller pour l'exposer en HTTP. Ce pattern rend l'objet entièrement optionnel, avec des règles de routage et des certificats TLS pilotés depuis les values.

L'Ingress expose vos Services HTTP/HTTPS à l'extérieur du cluster avec :

  • Routage basé sur le hostname ou le path
  • Terminaison TLS
  • Annotations pour le controller (rate limiting, auth, etc.)

L'Ingress est désactivé par défaut, choix habituel pour un chart réutilisable. La structure hosts est une liste de listes, chaque hôte portant ses propres chemins, et tls reste vide tant qu'aucun Secret de certificat n'existe dans le namespace.

ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: api.example.local
paths:
- path: /
pathType: Prefix
tls: []

Les boucles imbriquées sont le point délicat : à l'intérieur d'un range, le point ne désigne plus la racine des valeurs mais l'élément courant. D'où le $ devant .Values.service.port et devant l'appel au helper fullname, sans lequel le rendu échoue.

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mon-api.fullname" . }}
labels:
{{- include "mon-api.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mon-api.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

Points clés du template :

  • {{- if .Values.ingress.enabled -}} : tout le fichier est conditionnel
  • $ pour accéder au contexte global dans les boucles imbriquées
  • Double range : une boucle pour les hosts, une pour les paths

L'échappement des points est obligatoire dans --set : sans les antislashs, Helm lirait nginx.ingress.kubernetes.io/rewrite-target comme une arborescence de clés imbriquées et non comme un nom d'annotation.

Fenêtre de terminal
helm template test-release mon-api \
--set ingress.enabled=true \
--set 'ingress.annotations.nginx\.ingress\.kubernetes\.io/rewrite-target=/' \
--set 'ingress.tls[0].secretName=api-tls' \
--set 'ingress.tls[0].hosts[0]=api.example.local'

Résultat :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-release-mon-api
labels:
helm.sh/chart: mon-api-0.2.0
app.kubernetes.io/name: mon-api
app.kubernetes.io/instance: test-release
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/managed-by: Helm
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
tls:
- hosts:
- "api.example.local"
secretName: api-tls
rules:
- host: "api.example.local"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-release-mon-api
port:
number: 9898

Le HPA ajuste le nombre de réplicas selon les métriques observées. Le point sensible se situe ailleurs que dans l'objet lui-même : dès que l'autoscaling est actif, le champ replicas du Deployment doit disparaître, sans quoi Helm et le HPA se contredisent à chaque déploiement.

L'HPA (Horizontal Pod Autoscaler) ajuste automatiquement le nombre de réplicas selon la charge. Avantages :

  • Absorber les pics de trafic
  • Réduire les coûts en période creuse
  • Maintenir la performance

Les deux seuils sont facultatifs et traités séparément dans le template : n'en renseigner qu'un produit un HPA à une seule métrique. Le minReplicas: 2 empêche l'application de retomber à un pod unique pendant les périodes creuses.

autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80

Quand l'HPA est actif, c'est lui qui gère le nombre de réplicas. Le Deployment ne doit pas définir replicas :

spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}

L'apiVersion autoscaling/v2 est celle qui accepte plusieurs métriques dans un même objet. scaleTargetRef doit désigner le Deployment par son nom rendu, d'où l'appel au même helper fullname que dans le template du Deployment.

{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "mon-api.fullname" . }}
labels:
{{- include "mon-api.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "mon-api.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

Le contrôle est négatif du côté du Deployment : l'extrait rendu ne doit plus contenir de champ replicas. Le HPA produit à la suite reprend les deux métriques déclarées dans les values.

Fenêtre de terminal
helm template test-release mon-api --set autoscaling.enabled=true

Extrait deployment.yaml (sans replicas) :

spec:
selector:
matchLabels:
app.kubernetes.io/name: mon-api

hpa.yaml :

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: test-release-mon-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: test-release-mon-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

Pattern 8, Scheduling (affinity, tolerations, nodeSelector)

Section intitulée « Pattern 8, Scheduling (affinity, tolerations, nodeSelector) »

Trois champs distincts pilotent le placement des pods et se combinent dans le même Deployment. Le chart les expose vides, ce qui les fait disparaître du manifest rendu : c'est à l'installation, via un fichier de values dédié, qu'on impose une répartition.

Par défaut, Kubernetes place les pods sur n'importe quel nœud disponible. En production, vous voulez :

BesoinSolution
Pods sur des nœuds spécifiques (SSD, GPU)nodeSelector
Pods sur des nœuds taintéstolerations
Pods répartis sur plusieurs zonespodAntiAffinity
Pods co-localisés avec d'autrespodAffinity

Ces trois clés sont déclarées vides pour documenter leur existence : l'utilisateur du chart sait ainsi qu'il peut les remplir sans avoir à lire les templates.

nodeSelector: {}
tolerations: []
affinity: {}

Les trois blocs with suivent le même modèle et partagent la même indentation nindent 8, puisqu'ils se placent tous sous spec.template.spec du Deployment.

{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

Exemple : haute disponibilité avec podAntiAffinity

Section intitulée « Exemple : haute disponibilité avec podAntiAffinity »

Créez un fichier values-ha.yaml :

replicaCount: 3
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: mon-api
topologyKey: kubernetes.io/hostname

Rendu :

Fenêtre de terminal
helm template test-release mon-api -f values-ha.yaml --show-only templates/deployment.yaml | tail -15
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: mon-api
topologyKey: kubernetes.io/hostname
weight: 100

Une toleration n'attire pas le pod vers un nœud, elle l'autorise seulement à y aller malgré son taint. C'est le nodeSelector qui l'y envoie, d'où l'emploi des deux ensemble dans cet exemple.

Fenêtre de terminal
helm template test-release mon-api \
--set 'nodeSelector.disktype=ssd' \
--set 'tolerations[0].key=dedicated' \
--set 'tolerations[0].operator=Equal' \
--set 'tolerations[0].value=api' \
--set 'tolerations[0].effect=NoSchedule'
nodeSelector:
disktype: ssd
tolerations:
- effect: NoSchedule
key: dedicated
operator: Equal
value: api

Ces métadonnées ne changent rien au fonctionnement de l'application, mais elles commandent le comportement des outils qui l'entourent : collecte de métriques, injection de secrets, service mesh. Un chart qui ne les expose pas oblige à modifier ses templates pour chaque nouvelle intégration.

Les annotations permettent d'intégrer vos pods avec d'autres outils :

OutilAnnotationUsage
Prometheusprometheus.io/scrape: "true"Découverte des métriques
Vaultvault.hashicorp.com/agent-inject: "true"Injection de secrets
Istiosidecar.istio.io/inject: "true"Service mesh
Datadogad.datadoghq.com/...APM et logs

Les deux dictionnaires restent vides par défaut : leur contenu dépend entièrement des outils déployés dans le cluster de destination.

podAnnotations: {}
podLabels: {}

L'ordre compte dans le bloc labels : les selectorLabels sont rendus en premier, les podLabels de l'utilisateur ensuite. Un label personnalisé portant le même nom écraserait un label de sélection et détacherait le pod de son Deployment.

template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "mon-api.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}

Les points des clés d'annotation s'échappent comme pour l'Ingress. Le rendu montre que 9898 ressort entre guillemets : une annotation Kubernetes ne peut contenir qu'une chaîne de caractères.

Fenêtre de terminal
helm template test-release mon-api \
--set 'podAnnotations.prometheus\.io/scrape=true' \
--set 'podAnnotations.prometheus\.io/port=9898' \
--set 'podAnnotations.prometheus\.io/path=/metrics'
metadata:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "9898"
prometheus.io/scrape: "true"

Un hook est un objet Kubernetes ordinaire, marqué par des annotations qui indiquent à Helm de le créer à un moment précis, en dehors du cycle de vie normal de la release. C'est ce qui permet d'exécuter une migration de base de données avant que les nouveaux pods ne démarrent.

Les hooks exécutent des Jobs à des moments précis du cycle de vie :

HookMoment d'exécutionCas d'usage
pre-installAvant l'installationVérifications préalables
post-installAprès l'installationSeed de données, notifications
pre-upgradeAvant la mise à jourMigrations de BDD
post-upgradeAprès la mise à jourTests de smoke
pre-deleteAvant la suppressionBackup, nettoyage
post-deleteAprès la suppressionNotification

Le hook est désactivé par défaut, la commande servant de gabarit. L'image est pinnée sur une version précise : un hook lancé depuis un tag flottant peut changer de comportement entre deux déploiements sans qu'aucun fichier du chart n'ait bougé.

hooks:
preInstall:
enabled: false
image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
command: "echo 'Running pre-install hook'"

Les trois annotations pilotent tout : le moment d'exécution, l'ordre relatif par un poids, et le sort du Job une fois terminé. Sans hook-delete-policy, les Jobs s'accumulent à chaque déploiement et finissent par bloquer le suivant sur un conflit de nom.

{{- if .Values.hooks.preInstall.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mon-api.fullname" . }}-pre-install
labels:
{{- include "mon-api.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: pre-install
image: {{ .Values.hooks.preInstall.image | default "busybox:1.36" }}
command: ["sh", "-c", {{ .Values.hooks.preInstall.command | quote }}]
{{- end }}

Annotations expliquées :

AnnotationSignification
helm.sh/hookQuand exécuter le Job
helm.sh/hook-weightOrdre d'exécution (négatif = plus tôt)
helm.sh/hook-delete-policyQuand supprimer le Job (hook-succeeded, before-hook-creation, hook-failed)

helm template produit le Job comme n'importe quel manifest : ce n'est qu'à l'installation que Helm lit les annotations et décale son exécution. Un hook ne se valide donc jamais complètement par un simple rendu.

Fenêtre de terminal
helm template test-release mon-api \
--set hooks.preInstall.enabled=true \
--set 'hooks.preInstall.command=echo "Running migrations"'
apiVersion: batch/v1
kind: Job
metadata:
name: test-release-mon-api-pre-install
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: pre-install
image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
command: ["sh", "-c", "echo \"Running migrations\""]

Ces symptômes viennent presque tous d'un décalage entre le chart et son environnement : une probe trop stricte pour le temps de démarrage réel, un securityContext incompatible avec l'image, ou un composant de cluster absent. Le rendu est correct dans tous ces cas, l'erreur n'apparaît qu'à l'exécution.

SymptômeCause probableSolution
Pod en CrashLoopBackOffProbe échoue, app pas prêteAugmenter initialDelaySeconds
Pod OOMKilledLimite memory dépasséeAugmenter limits.memory
Pod ne démarre pas (security)Image incompatible securityContextVérifier UID de l'image
HPA <unknown>metrics-server absentInstaller metrics-server
Ingress sans effetIngressController absentInstaller nginx-ingress ou traefik
Hook ne s'exécute pasMauvaise annotationVérifier helm.sh/hook

L'exercice reprend les patterns un par un sur une copie du chart précédent, avec helm lint --strict et helm template comme garde-fous avant toute installation. La vérification finale lit le securityContext réellement appliqué au conteneur en cours d'exécution, seule preuve que le réglage a traversé le rendu puis l'admission du cluster.

  1. Partir du chart du Lab B1

    Fenêtre de terminal
    cd /tmp && cp -r mon-api mon-api-prod
    cd mon-api-prod
  2. Mettre à jour values.yaml avec les patterns

    Ajoutez les sections : probes, resources, securityContext, serviceAccount, autoscaling.

  3. Valider avec helm lint

    Fenêtre de terminal
    helm lint . --strict
  4. Prévisualiser le rendu complet

    Fenêtre de terminal
    helm template my-release . > rendered.yaml
    cat rendered.yaml
  5. Tester avec autoscaling activé

    Fenêtre de terminal
    helm template my-release . --set autoscaling.enabled=true | grep -A5 "kind: HorizontalPodAutoscaler"
  6. Tester avec Ingress et TLS

    Fenêtre de terminal
    helm template my-release . \
    --set ingress.enabled=true \
    --set 'ingress.tls[0].secretName=my-tls' \
    --set 'ingress.tls[0].hosts[0]=api.example.local'
  7. Installer et vérifier sur le cluster

    Fenêtre de terminal
    kubectl create namespace lab-b2
    helm install my-api . -n lab-b2
    kubectl get all,sa,cm -n lab-b2
  8. Vérifier le securityContext du pod

    Fenêtre de terminal
    kubectl get pod -n lab-b2 -o jsonpath='{.items[0].spec.containers[0].securityContext}' | jq
  9. Nettoyer

    Fenêtre de terminal
    helm uninstall my-api -n lab-b2
    kubectl delete namespace lab-b2

Critères de réussite :

  • Probes configurées et activées
  • Resources limits/requests présents
  • SecurityContext appliqué (runAsNonRoot, drop ALL capabilities)
  • ServiceAccount créé avec automount: false
  • HPA généré quand autoscaling.enabled=true
  • Ingress avec TLS quand activé
  • Pod Running avec securityContext vérifié

Les six premiers points portent sur les objets Kubernetes, les deux derniers sur le templating lui-même. Ce sont ces deux-là qui séparent un chart robuste d'un chart approximatif : with évite de rendre des blocs vides, et $ sauve les boucles imbriquées.

  • Les probes garantissent que Kubernetes détecte les conteneurs défaillants
  • Les resources protègent le cluster contre les conteneurs gourmands
  • Le securityContext applique le principe du moindre privilège
  • L'Ingress expose vos services avec TLS et routage intelligent
  • Le HPA adapte automatiquement le nombre de réplicas à la charge
  • Les hooks permettent d'orchestrer des actions au bon moment
  • Le pattern {{- with .Values.xxx }} évite de rendre des blocs vides
  • Utilisez $ pour accéder au contexte global dans les boucles range

Un chart production-ready reste un chart isolé. La suite porte sur son assemblage avec des subcharts, sur sa qualité via values.schema.json et helm-docs, puis sur sa publication dans un registry OCI.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. 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