Aller au contenu
Conteneurs & Orchestration medium

10 patterns Helm pour des charts production-ready

27 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.

  • 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

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
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
{{- 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 }}
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

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)
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 .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
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

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.

NiveauScopeExemple d’usage
podSecurityContextTous les conteneurs du podfsGroup, runAsNonRoot
securityContextUn conteneur spécifiquecapabilities, readOnlyRootFilesystem
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
spec:
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 4 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
securityContext:
fsGroup: 65534
runAsNonRoot: true
containers:
- name: mon-api
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534

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.

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)
{{/*
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
{{- 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

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.

config:
enabled: true
data:
LOG_LEVEL: "info"
APP_ENV: "production"
API_TIMEOUT: "30"
{{- 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)
{{- if .Values.config.enabled }}
envFrom:
- configMapRef:
name: {{ include "mon-api.fullname" . }}-config
{{- end }}
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"

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.)
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: api.example.local
paths:
- path: /
pathType: Prefix
tls: []
{{- 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
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

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
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 }}
{{- 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 }}
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) »

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
nodeSelector: {}
tolerations: []
affinity: {}
{{- 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
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

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
podAnnotations: {}
podLabels: {}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "mon-api.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
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"

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
hooks:
preInstall:
enabled: false
image: busybox:1.36
command: "echo 'Running pre-install hook'"
{{- 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)
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
command: ["sh", "-c", "echo \"Running migrations\""]
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

  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 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

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.