Ce guide présente les 10 gabarits essentiels pour transformer un chart basique en chart production-ready. Vous apprendrez à ajouter les sondes de santé, 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 attendus en production.
Prérequis
Section intitulée « Prérequis »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
Vue d'ensemble des patterns
Section intitulée « Vue d'ensemble des patterns »Voici les 10 patterns que tout chart production-ready doit implémenter :
| # | Pattern | Problème résolu | Impact production |
|---|---|---|---|
| 1 | Health Probes | Détection des conteneurs défaillants | Haute dispo, auto-healing |
| 2 | Resources | Conteneurs gourmands, éviction | Stabilité, QoS |
| 3 | SecurityContext | Conteneurs root, failles | Sécurité, compliance |
| 4 | ServiceAccount | Permissions excessives | Principe moindre privilège |
| 5 | ConfigMap | Config en dur dans l'image | Flexibilité, 12-factor |
| 6 | Ingress | Exposition HTTP sans TLS | Accès sécurisé |
| 7 | Autoscaling (HPA) | Charge variable | Élasticité, coûts |
| 8 | Scheduling | Répartition non optimale | HA, performance |
| 9 | PodAnnotations | Intégrations manquantes | Observabilité |
| 10 | Hooks | Migrations, initialisations | Orchestration |
Pattern 1, Health Probes configurables
Section intitulée « Pattern 1, Health Probes configurables »Sans sondes, Kubernetes considère qu'un conteneur démarré est un conteneur en bonne santé, y compris quand l'application est figée. Ce gabarit 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é.
Pourquoi des probes ?
Section intitulée « Pourquoi des probes ? »Les probes (sondes) permettent à Kubernetes de vérifier l'état de vos conteneurs :
| Probe | Question posée | Consé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 |
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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: 30Décryptage des paramètres :
| Paramètre | Signification | Valeur typique |
|---|---|---|
initialDelaySeconds | Attente avant première vérification | 5-30s selon l'app |
periodSeconds | Intervalle entre vérifications | 10s |
timeoutSeconds | Timeout de la requête | 3s |
failureThreshold | Échecs consécutifs avant action | 3 |
Template avec flag enabled
Section intitulée « Template avec flag enabled »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, au prix d'un peu de verbosité.
containers: - name: {{ .Chart.Name }} {{- range $nom := list "startupProbe" "livenessProbe" "readinessProbe" }} {{- $p := index $.Values $nom }} {{- if $p.enabled }} {{ $nom }}: httpGet: path: {{ $p.httpGet.path }} port: {{ $p.httpGet.port }} initialDelaySeconds: {{ $p.initialDelaySeconds }} periodSeconds: {{ $p.periodSeconds }} {{- with $p.timeoutSeconds }} timeoutSeconds: {{ . }} {{- end }} failureThreshold: {{ $p.failureThreshold }} {{- end }} {{- end }}Les trois sondes partagent la même forme, donc une seule boucle les rend toutes. Écrire trois blocs quasi identiques marche aussi, mais chaque correction devrait alors être reportée trois fois, et c'est ainsi qu'une sonde finit par diverger des autres.
Trois détails méritent l'attention. $.Values et non .Values : à
l'intérieur d'un range, le contexte courant est l'élément parcouru, et le
dollar seul est le seul chemin de retour vers la racine. index va
chercher une clé dont le nom est une variable, ce que la notation par point
ne permet pas. Et le with autour de timeoutSeconds n'est pas décoratif :
startupProbe n'en déclare pas dans les values ci-dessus, et sans cette garde
la ligne sortirait vide.
L'ordre de la liste compte aussi, et il n'est pas alphabétique :
startup d'abord, parce que c'est l'ordre dans lequel Kubernetes les met en
œuvre, la sonde de démarrage suspendant les deux autres tant qu'elle n'a pas
réussi.
Rendu obtenu
Section intitulée « Rendu obtenu »--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, ce qu'un rendu complet noierait dans le reste.
helm template test-release mon-api --show-only templates/deployment.yamlExtrait du résultat :
livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3readinessProbe: httpGet: path: /readyz port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3Pattern 2, Resources (CPU/Memory)
Section intitulée « Pattern 2, Resources (CPU/Memory) »Un conteneur sans plafond peut absorber toute la mémoire de son nœud et provoquer l'éviction de ses voisins. Ce gabarit 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.
Pourquoi définir les resources ?
Section intitulée « Pourquoi définir les resources ? »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)
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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: 128MiNotation des ressources :
| Notation | Signification |
|---|---|
100m | 100 millicores = 0.1 CPU |
1 | 1 CPU complet |
128Mi | 128 Mébioctets |
1Gi | 1 Gibioctet |
Template avec pattern "with"
Section intitulée « Template avec pattern "with" »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 $.
containers: - name: {{ .Chart.Name }} {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }}Pourquoi ce pattern ?
- Si
resources: {}(vide), le bloc n'est pas rendu toYamlconvertit l'objet YAML proprementnindent 12ajoute une nouvelle ligne et douze espaces d'indentation
Le nombre passé à nindent est une colonne absolue, pas un décalage. C'est
le piège le plus coûteux du templating Helm, et la raison pour laquelle ce
gabarit est montré dans son contexte plutôt que seul. Ici resources: se
trouve en colonne 10, ses enfants doivent donc tomber en colonne 12. Recopier le
même bloc avec nindent 2, ce qui semble naturel puisque les enfants sont
« deux crans plus loin », produit ceci :
resources: limits: cpu: 200mEt le rendu s'arrête sur mapping values are not allowed in this context, une
erreur qui désigne la ligne fautive sans nommer la cause : elle parle du
YAML produit, jamais du gabarit. La règle tient en une phrase : comptez la
profondeur depuis le début de la ligne, jamais depuis la clé qui précède.
Rendu obtenu
Section intitulée « Rendu obtenu »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, bien plus tard dans la chaîne.
containers: - name: mon-api resources: limits: cpu: 200m memory: 256Mi requests: cpu: 100m memory: 128MiPattern 3, SecurityContext
Section intitulée « Pattern 3, SecurityContext »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.
Pourquoi sécuriser les conteneurs ?
Section intitulée « Pourquoi sécuriser les conteneurs ? »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.
Deux niveaux de sécurité
Section intitulée « Deux niveaux de sécurité »Les deux blocs portent le même nom dans le manifeste 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 sur celle du pod.
| Niveau | Scope | Exemple d'usage |
|---|---|---|
podSecurityContext | Tous les conteneurs du pod | fsGroup, runAsNonRoot |
securityContext | Un conteneur spécifique | capabilities, readOnlyRootFilesystem |
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »L'identifiant 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: 65534Décryptage :
| Paramètre | Signification |
|---|---|
runAsNonRoot: true | Refuse de démarrer si le conteneur tente de tourner en root |
runAsUser: 65534 | UID nobody (utilisateur sans privilèges) |
allowPrivilegeEscalation: false | Empêche d'obtenir plus de droits qu'au départ |
capabilities.drop: [ALL] | Supprime toutes les capabilities Linux |
readOnlyRootFilesystem: true | Système de fichiers en lecture seule |
Template
Section intitulée « Template »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 }}Rendu obtenu
Section intitulée « Rendu obtenu »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: 65534Pattern 4, ServiceAccount conditionnel
Section intitulée « Pattern 4, ServiceAccount conditionnel »Tout pod s'exécute sous un ServiceAccount, celui du namespace par défaut s'il n'en désigne aucun. Ce gabarit crée un compte dédié au chart, coupe le montage automatique de son jeton, et laisse malgré tout la possibilité de réutiliser un compte géré ailleurs.
Pourquoi un ServiceAccount dédié ?
Section intitulée « Pourquoi un ServiceAccount dédié ? »Chaque pod Kubernetes a un ServiceAccount qui définit ses permissions d'accès à l'API. Le compte default est partagé par tous les pods sans compte explicite, ce qui rend ses droits difficiles à restreindre sans effet de bord.
Problème : si vous montez un token dans un pod compromis, l'attaquant hérite des permissions du SA.
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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ètre | Signification |
|---|---|
create | Créer un SA dédié (vs utiliser default) |
automount | Monter automatiquement le token ? (false recommandé) |
annotations | Pour IAM (AWS IRSA, GCP Workload Identity) |
name | Nom personnalisé (sinon généré automatiquement) |
Helper serviceAccountName
Section intitulée « Helper serviceAccountName »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 oudefault
Template serviceaccount.yaml
Section intitulée « Template serviceaccount.yaml »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: v1kind: ServiceAccountmetadata: 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 }}Rendu obtenu
Section intitulée « Rendu obtenu »Le rendu ci-dessous montre les deux effets du gabarit : la création du ServiceAccount, et sa référence depuis le Deployment. C'est la paire à vérifier, car un ServiceAccount créé mais jamais référencé est une erreur silencieuse : les pods tournent, avec l'identité default.
Avec serviceAccount.create: true :
apiVersion: v1kind: ServiceAccountmetadata: 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: HelmautomountServiceAccountToken: falseAvec serviceAccount.create: false :
helm template test-release mon-api --set serviceAccount.create=false \ --show-only templates/deployment.yaml | grep serviceAccountName serviceAccountName: defaultPattern 5, ConfigMap dynamique
Section intitulée « Pattern 5, ConfigMap dynamique »Une configuration gravée dans l'image impose de la reconstruire à chaque changement de valeur. Ce gabarit génère une ConfigMap à partir d'un dictionnaire libre placé dans les values, puis l'injecte en bloc dans le conteneur avec envFrom.
Pourquoi externaliser la configuration ?
Section intitulée « Pourquoi externaliser la configuration ? »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.
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »Le sous-bloc data accepte n'importe quelle clé, le template les parcourant sans en connaître la liste. Les valeurs numériques 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"Template configmap.yaml
Section intitulée « Template configmap.yaml »range sur un dictionnaire rend les clés triées, ce qui rend le manifeste 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: v1kind: ConfigMapmetadata: 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.dataparcourt chaque paire clé-valeur{{ $key }}et{{ $value }}sont des variables locales| quotemet la valeur entre guillemets (sécurité YAML)
Injection dans le Deployment
Section intitulée « Injection dans le Deployment »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 }}Rendu obtenu
Section intitulée « Rendu obtenu »Le rendu ne contient que les clés déclarées dans les values. Piège classique de ce gabarit : mettre à jour la ConfigMap ne redémarre pas les pods, les variables d'environnement restant celles chargées au démarrage du conteneur.
apiVersion: v1kind: ConfigMapmetadata: 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: Helmdata: APP_ENV: "production" LOG_LEVEL: "info"Pattern 6, Ingress avec TLS optionnel
Section intitulée « Pattern 6, Ingress avec TLS optionnel »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.
Pourquoi un Ingress ?
Section intitulée « Pourquoi un Ingress ? »L'Ingress expose vos Services HTTP et HTTPS à l'extérieur du cluster. Il apporte trois choses qu'un Service seul ne fait pas : le routage par nom d'hôte, le routage par chemin, et la terminaison TLS.
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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: []Template ingress.yaml
Section intitulée « Template ingress.yaml »Les boucles imbriquées sont le point délicat : à l'intérieur d'un range, le point ne désigne plus la racine des values 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/v1kind: Ingressmetadata: 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
Rendu avec TLS
Section intitulée « Rendu avec TLS »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.
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/v1kind: Ingressmetadata: 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: 9898Pattern 7, Autoscaling (HPA)
Section intitulée « Pattern 7, Autoscaling (HPA) »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.
Pourquoi l'autoscaling ?
Section intitulée « Pourquoi l'autoscaling ? »L'Horizontal Pod Autoscaler ajuste automatiquement le nombre de réplicas selon la charge. Il apporte trois choses : absorber un pic de trafic sans intervention, réduire le coût en période creuse, et éviter le dimensionnement au pire cas qui gaspille des ressources en permanence.
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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, donc de perdre toute redondance.
autoscaling: enabled: false minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 70 targetMemoryUtilizationPercentage: 80Interaction avec le Deployment
Section intitulée « Interaction avec le Deployment »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 }}Template hpa.yaml
Section intitulée « Template hpa.yaml »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 : une divergence ici et le HPA ne trouve pas sa cible.
{{- if .Values.autoscaling.enabled }}apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: 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 }}Rendu obtenu
Section intitulée « Rendu obtenu »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, ce qui ferme la vérification.
helm template test-release mon-api --set autoscaling.enabled=trueExtrait deployment.yaml (sans replicas) :
spec: selector: matchLabels: app.kubernetes.io/name: mon-apihpa.yaml :
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: test-release-mon-apispec: 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: 80Pattern 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 manifeste rendu : c'est à l'installation, par un fichier de values dédié, qu'on impose une répartition.
Pourquoi contrôler le scheduling ?
Section intitulée « Pourquoi contrôler le scheduling ? »Par défaut, Kubernetes place les pods sur n'importe quel nœud disponible. En production, vous voulez :
| Besoin | Solution |
|---|---|
| Pods sur des nœuds spécifiques (SSD, GPU) | nodeSelector |
| Pods sur des nœuds taintés | tolerations |
| Pods répartis sur plusieurs zones | podAntiAffinity |
| Pods co-localisés avec d'autres | podAffinity |
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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. C'est une forme de documentation exécutable, qui ne dérive jamais du code.
nodeSelector: {}
tolerations: []
affinity: {}Template (dans le Deployment)
Section intitulée « Template (dans le Deployment) »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 »L'anti-affinité répond à une question précise : que se passe-t-il si le nœud qui porte mes pods tombe ? Sans elle, rien n'empêche le planificateur de placer les trois répliques au même endroit. Le choix décisif est entre la forme stricte, qui refuse de placer un pod faute de nœud libre, et la forme souple, qui préfère répartir mais accepte de concentrer.
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/hostnameRendu :
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: 100Exemple : nœuds dédiés avec tolerations
Section intitulée « Exemple : nœuds dédiés avec tolerations »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. Les confondre produit des pods qui restent en Pending sans raison apparente.
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: apiPattern 9, PodAnnotations et PodLabels
Section intitulée « Pattern 9, PodAnnotations et PodLabels »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.
Pourquoi des annotations sur les pods ?
Section intitulée « Pourquoi des annotations sur les pods ? »Les annotations permettent d'intégrer vos pods avec d'autres outils :
| Outil | Annotation | Usage |
|---|---|---|
| Prometheus | prometheus.io/scrape: "true" | Découverte des métriques |
| Vault | vault.hashicorp.com/agent-inject: "true" | Injection de secrets |
| Istio | sidecar.istio.io/inject: "true" | Service mesh |
| Datadog | ad.datadoghq.com/... | APM et logs |
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »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: {}Template
Section intitulée « Template »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 }}Exemple : scraping Prometheus
Section intitulée « Exemple : scraping Prometheus »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, jamais un nombre.
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"Pattern 10, Hooks Helm
Section intitulée « Pattern 10, Hooks Helm »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.
Quand utiliser les hooks ?
Section intitulée « Quand utiliser les hooks ? »Les hooks exécutent des Jobs à des moments précis du cycle de vie :
| Hook | Moment d'exécution | Cas d'usage |
|---|---|---|
pre-install | Avant l'installation | Vérifications préalables |
post-install | Après l'installation | Seed de données, notifications |
pre-upgrade | Avant la mise à jour | Migrations de BDD |
post-upgrade | Après la mise à jour | Tests de smoke |
pre-delete | Avant la suppression | Backup, nettoyage |
post-delete | Après la suppression | Notification |
Configuration dans values.yaml
Section intitulée « Configuration dans values.yaml »Le hook est désactivé par défaut, la commande servant de gabarit. L'image est épinglé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'"Template hook-pre-install.yaml
Section intitulée « Template hook-pre-install.yaml »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/v1kind: Jobmetadata: 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-succeededspec: 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 :
| Annotation | Signification |
|---|---|
helm.sh/hook | Quand exécuter le Job |
helm.sh/hook-weight | Ordre d'exécution (négatif = plus tôt) |
helm.sh/hook-delete-policy | Quand supprimer le Job (hook-succeeded, before-hook-creation, hook-failed) |
Rendu obtenu
Section intitulée « Rendu obtenu »helm template produit le Job comme n'importe quel manifeste : 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, ce qui en fait la partie du chart la moins testable hors cluster.
helm template test-release mon-api \ --set hooks.preInstall.enabled=true \ --set 'hooks.preInstall.command=echo "Running migrations"'apiVersion: batch/v1kind: Jobmetadata: 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-succeededspec: template: spec: restartPolicy: Never containers: - name: pre-install image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 command: ["sh", "-c", "echo \"Running migrations\""]Pattern 11, Lire l'état du cluster avec lookup
Section intitulée « Pattern 11, Lire l'état du cluster avec lookup »La fonction lookup interroge le cluster pendant le rendu : elle répond
« cette ressource existe-t-elle, et que contient-elle ». C'est le seul moyen,
dans un template, de tenir compte de ce qui est déjà déployé au lieu de
raisonner uniquement sur les values.
Pourquoi lookup existe
Section intitulée « Pourquoi lookup existe »Le besoin le plus courant est un mot de passe qu'il ne faut générer qu'une
fois. Un chart qui écrit randAlphaNum 24 dans un Secret produit une valeur
différente à chaque rendu : le premier helm upgrade change le mot de passe
sous les pieds de l'application, qui ne peut plus se connecter à sa base. Mesuré
sur deux releases identiques, l'une avec lookup et l'autre sans :
| Chart | Après install | Après upgrade |
|---|---|---|
Sans lookup | YXdFOVFSclVl… | WWdkUWpZRjJ2…, régénéré |
Avec lookup | bFNOTmpPYXVU… | bFNOTmpPYXVU…, conservé |
Le template lit d'abord le Secret existant, et ne tire une valeur au sort que s'il ne le trouve pas :
{{- $existant := lookup "v1" "Secret" .Release.Namespace (printf "%s-auth" .Release.Name) }}apiVersion: v1kind: Secretmetadata: name: {{ .Release.Name }}-authtype: Opaquedata: motdepasse: {{ if $existant }}{{ index $existant.data "motdepasse" }}{{ else }}{{ randAlphaNum 24 | b64enc | quote }}{{ end }}Les deux régimes de lookup
Section intitulée « Les deux régimes de lookup »lookup ne renvoie jamais d'erreur quand il ne peut pas interroger le
cluster : il renvoie une map vide. C'est ce qui le rend piégeux, parce que le
template continue son rendu comme si de rien n'était.
| Régime | kindOf | empty | Valeur lue |
|---|---|---|---|
helm template | map | true | rien |
helm install --dry-run=server | map | false | kube-system |
Déréférencer ce résultat vide fait échouer le rendu entier, avec un message qui
ne mentionne ni lookup ni le cluster :
Error: c/templates/s.yaml:6:39 executing "c/templates/s.yaml" at <"kube-system">: nil pointer evaluating interface {}.nameTestez donc toujours le résultat avec if ou default avant de le lire,
comme dans le gabarit ci-dessus. Un lookup non protégé transforme une simple
prévisualisation hors ligne en échec de rendu.
Les deux conséquences à connaître avant de s'en servir
Section intitulée « Les deux conséquences à connaître avant de s'en servir »Le rendu local devient mensonger. Le même chart rendu deux fois de suite par
helm template propose deux mots de passe différents, puisque lookup ne
trouve rien et que la branche else tire au sort à chaque appel. Un helm diff
exécuté sans cluster annoncera donc une modification du Secret qui n'aura pas
lieu. La revue de ces charts-là passe par --dry-run=server.
Les droits nécessaires sont plus larges qu'il n'y paraît. Même pour chercher
une seule ressource par son nom, lookup demande le verbe list sur le
type concerné. Un compte qui ne l'a pas fait échouer l'upgrade au rendu :
Error: UPGRADE FAILED: query: failed to query with labels: secrets is forbidden:User "system:serviceaccount:lk-lab:aveugle" cannot list resource "secrets"in API group "" in the namespace "lk-lab"Autrement dit, un chart qui utilise lookup sur des Secrets impose un droit de
lecture sur tous les Secrets du namespace à qui l'installe. C'est un arbitrage
de sécurité à faire consciemment, pas un détail d'implémentation : la
personne qui déploie hérite d'un privilège qu'elle n'a pas demandé.
Dépannage
Section intitulée « Dépannage »Ces symptômes viennent presque tous d'un décalage entre le chart et son environnement : une sonde 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ôme | Cause probable | Solution |
|---|---|---|
Pod en CrashLoopBackOff | Probe échoue, app pas prête | Augmenter initialDelaySeconds |
Pod OOMKilled | Limite memory dépassée | Augmenter limits.memory |
| Pod ne démarre pas (security) | Image incompatible securityContext | Vérifier UID de l'image |
HPA <unknown> | metrics-server absent | Installer metrics-server |
| Ingress sans effet | IngressController absent | Installer nginx-ingress ou traefik |
| Hook ne s'exécute pas | Mauvaise annotation | Vérifier helm.sh/hook |
Lab B2, Rendre un chart production-ready
Section intitulée « Lab B2, Rendre un chart production-ready »L'exercice reprend les gabarits 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.
-
Partir du chart du Lab B1
Fenêtre de terminal cd /tmp && cp -r mon-api mon-api-prodcd mon-api-prod -
Mettre à jour values.yaml avec les patterns
Ajoutez les sections : probes, resources, securityContext, serviceAccount, autoscaling.
-
Valider avec helm lint
Fenêtre de terminal helm lint . --strict -
Prévisualiser le rendu complet
Fenêtre de terminal helm template my-release . > rendered.yamlcat rendered.yaml -
Tester avec autoscaling activé
Fenêtre de terminal helm template my-release . --set autoscaling.enabled=true | grep -A5 "kind: HorizontalPodAutoscaler" -
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' -
Installer et vérifier sur le cluster
Fenêtre de terminal kubectl create namespace lab-b2helm install my-api . -n lab-b2kubectl get all,sa,cm -n lab-b2 -
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 -
Nettoyer
Fenêtre de terminal helm uninstall my-api -n lab-b2kubectl 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é
À retenir
Section intitulée « À retenir »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 bouclesrange
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Cinq questions sur les gabarits de production : ce que le squelette laisse vide, les deux régimes de lookup, et pourquoi un hook post-install ne protège de rien.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
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
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Dépendances et subcharts : Appliquer ces patterns quand un chart en embarque d'autres.
- Qualité, schéma et documentation : Contrôler automatiquement ce que ces templates produisent.
- Distribuer ses charts via OCI : Publier le chart une fois ses templates stabilisés.