Helm chart best practices: onderhoudbare Kubernetes-packages schrijven

Een goed opgebouwde Helm-chart bespaart je uren bij elke upgrade, review en incident. Een slecht opgebouwde levert stille fouten op die pas in productie zichtbaar worden. Deze gids loopt door de conventies die charts draagbaar, testbaar en veilig genoeg maken om aan een collega of CI-pipeline te overhandigen zonder een README die langer is dan de chart zelf.

Wat je aan het einde hebt

Een Helm-chart die voldoet aan de officiële Helm best practices, door helm lint en kubeconform validatie komt, genaamde helpers met namespacing gebruikt, correcte labels draagt, secrets veilig afhandelt en geautomatiseerde tests in CI draait. De praktijken richten zich op Helm 4.1 (huidige stable), maar gelden net zo goed voor Helm 3.x charts die nog in productie draaien.

Vereisten

  • Helm 4.x of 3.x lokaal geïnstalleerd (installatiedocs)
  • kubectl geconfigureerd tegen een development-cluster (voor integratietests)
  • Een chart die je schrijft of auditet, of de bereidheid om helm create te draaien voor een starter
  • Kennis van Kubernetes Deployments, Services en ConfigMaps

Chartstructuur en naamgeving

Begin met helm create mychart. De gegenereerde scaffold volgt de officiële chartstructuur en levert je direct correct genaamde templates op. Pas het aan; gebruik het niet ongewijzigd.

mychart/
├── Chart.yaml                # vereist: name, version, apiVersion
├── values.yaml               # standaardconfiguratie
├── values.schema.json        # optioneel: JSON Schema voor validatie
├── .helmignore               # packaging-exclusies
├── charts/                   # subchart-dependencies
├── crds/                     # CRD-manifests (eenmalig geïnstalleerd)
├── templates/
│   ├── _helpers.tpl          # named templates (underscore = niet gerenderd)
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   ├── hpa.yaml
│   ├── NOTES.txt             # post-install bericht voor de gebruiker
│   └── tests/
│       └── test-connection.yaml

Naamgevingsregels uit de Helm naming conventions:

  • Chartnamen: alleen kleine letters en cijfers, koppeltekens toegestaan. Geen underscores, geen punten. Moet beginnen met een letter.
  • Templatebestanden: dashed notation (my-app-configmap.yaml), nooit camelCase.
  • Eén Kubernetes-resource per bestand, bestandsnaam weerspiegelt het resource-type.
  • .yaml-extensie voor templates die YAML-output produceren. .tpl voor helperbestanden die zelf geen output genereren.

Chart.yaml: version vs appVersion

Deze twee velden volgen verschillende dingen en evolueren onafhankelijk. version is de versie van de chart zelf (templatelogica, standaardwaarden, structuur). appVersion is de applicatie die de chart deployt (de containerimage-versie). Een templatefix bumpen van 1.2.3 naar 1.2.4 terwijl appVersion op 3.0.0 blijft is gewoon normaal. GitOps-controllers zoals ArgoCD en Flux detecteren nieuwe releases via version, niet via appVersion. De officiële docs zeggen het expliciet: "The appVersion field is not related to the version field."

values.yaml-ontwerp

Het values-bestand is de publieke API van je chart. Behandel het met dezelfde discipline als de functiesignaturen van een library.

Naamgeving en structuur

Gebruik camelCase met een kleine beginletter voor elke key. De officiële values-gids gebruikt chickenNoodleSoup: true als canoniek voorbeeld. Grote beginletters botsen met Go template built-ins. Koppeltekens in namen breken --set op de commandoregel.

Geef de voorkeur aan platte structuren boven diep geneste. Geneste values vereisen existence-checks op elk niveau in templates, en diepe nesting maakt --set overrides onhandig. Gebruik nesting alleen als een groep gerelateerde, niet-optionele values echt bij elkaar hoort (zoals resources.requests en resources.limits).

Documenteer elke key

Elke property in values.yaml moet een comment hebben dat begint met de property-naam. Dat dient twee doelen: grep werkt meteen, en helm-docs kan er automatisch README-tabellen van genereren.

# replicaCount -- Aantal pod-replica's voor de main deployment
replicaCount: 3

# image.repository -- Container image registry en naam
# image.tag -- Overschrijft de chart appVersion
image:
  repository: registry.internal/myapp
  tag: ""

Typeveiligheid en override-ergonomie

Zet alle strings tussen aanhalingstekens om YAML-typecoercion te voorkomen. Een kale yes wordt boolean true. Een kale 012 wordt octaal getal 10. Grote integers kunnen stilletjes naar wetenschappelijke notatie converteren.

Ontwerp voor --set. Map-gebaseerde structuren zijn makkelijker te overriden dan arrays. --set ingress.hosts.myapp\.example\.com.paths[0]=/api is onhandig maar mogelijk. --set ingress.hosts[0].host=myapp.example.com is fragiel omdat array-indexering vereist dat je de huidige volgorde kent.

Schemavalidatie met values.schema.json

Een values.schema.json in de chart-root valideert types, verplichte velden, enums en patronen bij elke helm install, helm upgrade, helm lint en helm template. Het vangt misconfiguratie op voordat template-rendering begint, wat eerder en informatiever is dan een Go template-fout.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["replicaCount", "image"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string", "minLength": 1 },
        "tag": { "type": "string" }
      }
    }
  }
}

Dit is een van de meest onderbenutte Helm-features. Het valideert ook subchart-values, waardoor het een eerste verdedigingslijn is bij charts met complexe dependency trees. Arthur Koziels gids over values.schema.json behandelt geavanceerde patronen.

Secrets: nooit in values.yaml

Helm biedt geen encryptie. Values die je naar Git commit zijn plaintext. helm get values maakt ze zichtbaar voor iedereen met clustertoegang. Gebruik de Helm Secrets-plugin met Mozilla SOPS voor versleutelde values-bestanden, of verwijs naar secrets vanuit HashiCorp Vault of de Kubernetes External Secrets Operator.

Omgevingsscheiding

Houd een basis values.yaml met verstandige, productiewaardige defaults. Onderhoud per-omgeving override-bestanden (values-dev.yaml, values-staging.yaml) en geef ze mee met -f:

# Helm 4.x
helm upgrade --install myapp ./mychart \
  -f values-staging.yaml \
  --namespace staging

Template helpers en _helpers.tpl

Bestanden die beginnen met _ in templates/ worden niet gerenderd als Kubernetes-manifests. _helpers.tpl is de conventionele thuisbasis voor herbruikbare named templates.

Geef elke templatenaam een namespace

Templatenamen zijn globaal van scope over de chart en alle subcharts heen. Als jouw chart {{ define "labels" }} definieert en een subchart hetzelfde doet, wint de laatst geladen definitie stilletjes. Prefix altijd met de chartnaam:

{{- define "mychart.labels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Gebruik include, niet template

De ingebouwde template-actie voegt output inline in en kan niet gepipet worden. include retourneert een string, waardoor je indentatie kunt regelen:

# Correct: output kan geïndenteerd worden
metadata:
  labels:
    {{- include "mychart.labels" . | nindent 4 }}

# Fout: template kan niet naar nindent gepipet worden
metadata:
  labels:
    {{ template "mychart.labels" . }}

De officiële templategids noemt include "considered preferable" precies hierom.

Vereis verplichte waarden

Gebruik de required-functie om rendering te laten falen met een duidelijke foutmelding als een verplichte waarde ontbreekt, in plaats van een manifest te produceren met lege velden dat stil faalt bij apply:

{{ required "A valid .Values.database.host entry is required" .Values.database.host }}

De tpl-functie

Als een value zelf Go template-syntax bevat (veel voorkomend bij dynamische annotations of configuratiesnippets), render het dan met tpl:

annotations:
  {{- with .Values.podAnnotations }}
  {{- tpl (toYaml .) $ | nindent 4 }}
  {{- end }}

Documenteer je helpers

Elk {{ define ... }} blok hoort een {{/* ... */}} comment te hebben dat het doel uitlegt. Toekomstige beheerders gaan je logica niet reconstrueren uit de templatesyntax alleen.

Labels en annotations

De Helm labels-gids trekt een duidelijke grens: labels zijn voor identificatie en querying (kubectl get -l), annotations zijn voor niet-querybare metadata (Helm hooks, Prometheus scrape-config, cert-manager-directieven).

Aanbevolen labelset

Label Waarde Doel
app.kubernetes.io/name {{ include "mychart.name" . }} Identificeert de applicatie
helm.sh/chart {{ include "mychart.chart" . }} Volgt chartnaam + versie
app.kubernetes.io/managed-by {{ .Release.Service }} Identificeert de beheertool
app.kubernetes.io/instance {{ .Release.Name }} Onderscheidt meerdere installs
app.kubernetes.io/version {{ .Chart.AppVersion }} Volgt de applicatieversie

Selectorlabels moeten immutable zijn

De selectorLabels-helper (mychart.selectorLabels in de gegenereerde scaffold) mag alleen app.kubernetes.io/name en app.kubernetes.io/instance bevatten. Zet nooit helm.sh/chart of app.kubernetes.io/version in spec.selector.matchLabels. Die veranderen bij elke upgrade, en Kubernetes staat geen selectorwijzigingen toe op bestaande Deployments en StatefulSets.

Resourcebeheer in templates

Imagespecificatie

Gebruik nooit floating tags als latest, head of canary als standaard. Een values.yaml hoort te defaulten naar een vastgezette versietag of naar {{ .Chart.AppVersion }}. Scheid image.repository en image.tag in values, zodat operators de tag kunnen overriden zonder het registrypad aan te raken. Zet imagePullPolicy: IfNotPresent als default, want dat is wat helm create genereert en wat de Helm pods-gids aanbeveelt.

Resource requests en limits

Template altijd zowel requests als limits als configureerbare values. Containers zonder limits kunnen aangrenzende pods op dezelfde node uithongeren. Wil je beter begrijpen hoe de scheduler en kernel deze waarden gebruiken, lees dan Kubernetes resource requests en limits.

Security context

Default naar een restrictieve security-posture en laat operators het versoepelen als hun workload dat vereist:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL

Dit is ook een vereiste voor OpenShift-compatibiliteit en sluit aan bij het Kubernetes Pod Security Standards restricted-profiel.

Rolling updates bij ConfigMap-wijzigingen

Een Deployment-spec verandert niet wanneer de ConfigMap waarnaar verwezen wordt verandert. Pods blijven draaien met verouderde configuratie. De Helm tips and tricks-gids documenteert het checksum-annotatiepatroon:

spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

Dit triggert een rolling update zodra de ConfigMap-inhoud verandert.

Chart hooks

Helm hooks voegen imperatieve stappen in de chart-lifecycle. De hooks-documentatie definieert negen types: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback en test.

Een typische database-migratiehook:

apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ .Release.Name }}-db-migrate"
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"           # lager gewicht draait eerst
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["./migrate", "--target", "latest"]

Regels:

  • Zet altijd restartPolicy: Never op hook Jobs.
  • Definieer altijd helm.sh/hook-delete-policy. Zonder het hopen oude hook-resources zich op in het cluster.
  • Gebruik helm.sh/hook-weight als string (het is een annotatie-waarde) om executievolgorde te regelen.
  • Combineer lifecycle-events als dezelfde operatie voor zowel install als upgrade geldt: "helm.sh/hook": pre-install,pre-upgrade.

Wanneer hooks breken: het template-pipe-probleem

Hooks draaien alleen als Helm de installatie actief beheert. Als de chart wordt toegepast via helm template | kubectl apply (een veelgebruikt GitOps-patroon), worden hooks stilletjes overgeslagen. Een pre-install-hook die een ConfigMap aanmaakt draait niet, en de Deployment die ervan afhankelijk is faalt.

Als je chart ook moet werken met GitOps-tools als ArgoCD of Flux die manifests renderen voor ze toepassen, gebruik dan Kubernetes-native init containers in plaats van hooks voor dependency-sequencing. Documenteer de deployment-methode-aanname in de README van je chart als hooks kritiek zijn.

CRDs: gebruik de crds/ directory

Gebruik niet het oude crd-install hook-patroon uit Helm 2. Plaats CRD YAML-bestanden in de crds/-directory. Helm installeert ze bij de eerste install maar updatet ze niet bij upgrade om bestaande custom resource-data te beschermen. Voor CRD-lifecyclebeheer over upgrades heen, gebruik een aparte CRD-chart of een CRD-management-operator.

Je chart testen

helm lint valideert de chartstructuur. Het valideert niet of de gerenderde manifests geldige Kubernetes-objecten zijn. Een chart die door helm lint komt kan nog steeds manifests produceren die falen bij apply vanwege verkeerde veldnamen of verouderde API-versies. Alexandre Vazquez' testgids documenteert dit gat helder.

Vier lagen van chart-testing

1. Linting (geen cluster nodig):

helm lint ./mychart
yamllint ./mychart/values.yaml

Snel, vangt structurele fouten en YAML-syntaxproblemen op. Draai als pre-commit hook.

2. Schemavalidatie (geen cluster nodig):

# kubeconform is de onderhouden opvolger van het verouderde kubeval
helm template my-release ./mychart | kubeconform \
  --kubernetes-version 1.30.0 \
  --strict \
  --ignore-missing-schemas

Valideert gerenderde manifests tegen het Kubernetes API-schema voor een specifieke versie. Vangt veldnaamfouten, verouderde API-versies en typefouten op.

3. Unittesting (geen cluster nodig):

De helm-unittest plugin test templatelogica tegen fixture-waarden zonder cluster:

# tests/deployment_test.yaml
suite: Deployment tests
templates:
  - deployment.yaml
tests:
  - it: should set the correct replica count
    set:
      replicaCount: 5
    asserts:
      - equal:
          path: spec.replicas
          value: 5
  - it: should use the image from values
    asserts:
      - matchRegex:
          path: spec.template.spec.containers[0].image
          pattern: registry.internal/myapp:.*

4. Integratietesting (vereist een cluster):

De chart-testing (ct) tool, een officieel Helm/CNCF-project, installeert de chart in een live cluster, draait helm test en valideert upgrades van vorige versies:

ct install --config ct.yaml --charts ./mychart

helm test

Elke Pod of Job in templates/ met de annotatie "helm.sh/hook": test is een test-resource. De container moet met exit 0 afsluiten om te slagen. Organiseer tests onder templates/tests/ en voeg "helm.sh/hook-delete-policy": before-hook-creation toe om opeenhoping van test-pods tussen runs te voorkomen.

Minimum CI-pipeline

# Pre-commit (development-werkstation):
helm lint ./mychart
yamllint .

# Pull request (geautomatiseerd):
helm template my-release ./mychart | kubeconform --kubernetes-version 1.30.0 --strict
trivy helm --exit-code 1 --severity CRITICAL,HIGH ./mychart/

# Staging (pre-productie, vereist cluster):
ct install --config ct.yaml

Helm 4-wijzigingen voor chartauteurs

Helm 4.0, uitgebracht op 12 november 2025, introduceert wijzigingen waar chartauteurs rekening mee moeten houden:

  • Server-side apply (SSA) is de standaard voor nieuwe installaties. Releases die oorspronkelijk met Helm 3 geïnstalleerd zijn blijven client-side apply gebruiken, tenzij je --server-side expliciet meegeeft.
  • --atomic hernoemd naar --rollback-on-failure, en --force hernoemd naar --force-replace. Charts of CI-scripts die de oude flagnamen gebruiken moeten bijgewerkt worden.
  • Post-renderers vereisen nu plugin-namen. Willekeurige executables doorgeven via --post-renderer werkt niet meer.
  • Chart format v3 staat als "coming soon" in de docs maar heeft nog geen gepubliceerde specificatie. Charts met apiVersion: v2 blijven gewoon werken.

Helm 3 ontvangt securityfixes tot 11 november 2026 en bugfixes tot 8 juli 2026.

Veelvoorkomende problemen

  • helm lint slaagt maar kubectl apply faalt: lint valideert geen Kubernetes-schema. Pipe door kubeconform zoals beschreven in de testsectie.
  • Subchart overschrijft je helper-template: templatenamen zijn globaal. Geef elke {{ define }} een namespace met je chartnaam.
  • ConfigMap-wijzigingen triggeren geen pod-restart: voeg de checksum/config-annotatie toe aan de pod-template.
  • Hook Job draait niet in ArgoCD: hooks vereisen Helm-beheerde installs. Gebruik ArgoCD's resource hooks of init containers.
  • --set gedraagt zich onverwacht bij geneste waarden: controleer op camelCase-overtredingen, strings zonder aanhalingstekens, of fragiele array-indexering.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.