⎈ Helm Lektion 5/12 ~9 Min. Fortgeschritten

Values und Go Templates

Values überschreiben und Templates rendern.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du Go-Templates in Helm-Charts lesen und schreiben: Zugriff über .Values, Built-in-Objekte wie .Release und .Chart, Sprig-Funktionen wie default, toYaml und nindent sowie Kontrollstrukturen mit if, range und with. Du beherrschst die Values-Hierarchie — welche Quelle welche überschreibt — und kennst die Merge-Regel, die bei Listen regelmäßig für Überraschungen sorgt.

Das Problem: Ein Manifest, viele Umgebungen

Dein Deployment soll in Dev mit 1 Replica und ohne Ressourcen-Limits laufen, in Produktion mit 5 Replicas, strikten Limits und einem anderen Image-Tag. Der Unterschied ist Konfiguration, nicht Struktur — und genau diese Trennung leistet die Template-Engine: Die Struktur steht einmal in templates/, die variablen Teile kommen aus den Values. Helm nutzt dafür Go-Templates (die Template-Sprache der Go-Standardbibliothek), angereichert um die Sprig-Funktionsbibliothek und einige Helm-eigene Funktionen.

Ein Ausschnitt aus einem typischen Deployment-Template:

spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Alles zwischen {{ und }} wertet die Engine aus; der Rest ist YAML, das unverändert durchläuft. Diese eine Stelle enthält bereits die drei Mechanismen, die du sicher können musst: Built-in-Objekte, Pipelines mit Funktionen und Whitespace-Kontrolle.

Die Built-in-Objekte: woher die Daten kommen

Der Punkt am Anfang (.Values) ist der aktuelle Kontext — beim Rendern eines Templates das Wurzelobjekt mit allen Built-ins:

  • .Values — das Ergebnis der zusammengeführten Values (dazu gleich mehr). {{ .Values.image.repository }} navigiert durch die YAML-Struktur.
  • .Release — Informationen zur Installation: .Release.Name, .Release.Namespace, .Release.Revision. Damit baust du Ressourcennamen, die mehrere Releases desselben Charts im selben Cluster erlauben — der Grund, warum kein gutes Chart Namen hartkodiert.
  • .Chart — die Felder aus Chart.yaml, z. B. .Chart.Name, .Chart.Version, .Chart.AppVersion. Achtung: hier groß geschrieben, anders als in der YAML-Datei.
  • .Capabilities — was der Zielcluster kann, etwa .Capabilities.KubeVersion oder .Capabilities.APIVersions.Has "autoscaling/v2" für API-abhängige Templates.
  • .Files — Zugriff auf Nicht-Template-Dateien im Chart, z. B. um eine Konfigurationsdatei in eine ConfigMap einzubetten.

Die Values-Hierarchie: wer überschreibt wen

Beim Install oder Upgrade führt Helm alle Value-Quellen zu einem Objekt zusammen. Die Reihenfolge, von schwach nach stark:

  1. values.yaml aus dem Chart — die Defaults,
  2. Dateien per -f/--values (mehrere möglich, spätere gewinnen),
  3. einzelne --set-, --set-string- und --set-file-Flags.

Das Standardmuster in Projekten: Das Chart liefert sinnvolle Defaults, pro Umgebung gibt es eine schlanke Datei (values-prod.yaml) nur mit den Abweichungen, und --set bleibt Experimenten vorbehalten.

Die Merge-Regel mit dem größten Überraschungspotenzial: Maps werden rekursiv zusammengeführt, Listen komplett ersetzt. Wenn values.yaml drei env-Einträge als Liste definiert und deine values-prod.yaml einen vierten ergänzen will, musst du alle vier angeben — die Liste aus der Umgebungsdatei ersetzt die Default-Liste vollständig. Deshalb modellieren viele Charts solche Strukturen als Map statt als Liste.

Zu --set gehört eine ehrliche Warnung: Die Syntax (--set image.tag=1.27, --set 'hosts[0]=example.com') hat eigene Typ-Heuristiken — Werte, die wie Zahlen aussehen, werden Zahlen. Für einen Image-Tag wie 1.30 ist das fatal, weil daraus die Zahl 1.3 werden kann. --set-string erzwingt String-Typen; besser ist meist, den Wert in eine Datei zu schreiben.

Sprig-Funktionen und Pipelines: das Handwerkszeug

Funktionen verkettest du mit | zu Pipelines — der Output der linken Seite wird letztes Argument der rechten. Die Funktionen, die in fast jedem Chart vorkommen:

  • default: {{ .Values.image.tag | default .Chart.AppVersion }} — Fallback, wenn der Wert leer oder nicht gesetzt ist.
  • quote: {{ .Values.commitSha | quote }} — setzt Anführungszeichen und verhindert, dass YAML einen Wert als Zahl oder Boolean interpretiert.
  • required: {{ required "image.repository ist Pflicht" .Values.image.repository }} — bricht das Rendern mit deiner Fehlermeldung ab, statt ein leeres Feld zu erzeugen.
  • toYaml + indent/nindent: das Duo für strukturierte Werte. toYaml serialisiert eine Map oder Liste zurück zu YAML, nindent 12 setzt einen Zeilenumbruch und rückt jede Zeile 12 Spaces ein. So reichst du ganze Blöcke wie resources oder nodeSelector durch, ohne jedes Feld einzeln zu templaten.

Dazu Whitespace-Kontrolle: {{- schluckt Whitespace und Zeilenumbruch vor dem Ausdruck, -}} danach. Ohne sie hinterlässt jede Template-Anweisung eine Leerzeile im gerenderten YAML — meist nur hässlich, bei Einrückungen aber schnell ein Syntaxfehler.

Für Logik gibt es if/else, range (Schleife) und with (Kontextwechsel):

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
# ...
{{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
{{- end }}
{{- end }}

Innerhalb von range und with verschiebt sich der Kontext: . ist dann das aktuelle Listenelement bzw. das with-Objekt. Brauchst du von dort das Wurzelobjekt, nimmst du $$.Release.Name funktioniert in jeder Verschachtelungstiefe.

Praxis: Values-Hierarchie und Listen-Falle selbst sehen

Alles Folgende läuft ohne Cluster, rein mit helm template:

helm create demo

# Default aus values.yaml
helm template demo ./demo | grep -m1 replicas

# Umgebungsdatei schlägt Default
cat > values-prod.yaml <<'EOF'
replicaCount: 5
image:
  tag: "1.27"
EOF
helm template demo ./demo -f values-prod.yaml | grep -E 'replicas|image:'

# --set schlägt die Datei
helm template demo ./demo -f values-prod.yaml --set replicaCount=2 | grep -m1 replicas

# Effektive Werte nach dem Merge ansehen (sehr nützlich beim Debuggen)
helm template demo ./demo -f values-prod.yaml --set replicaCount=2 --show-only templates/deployment.yaml
helm install demo ./demo -f values-prod.yaml --dry-run | less

Teste anschließend die Listen-Falle: Trage in demo/values.yaml unter imagePullSecrets: zwei Einträge ein, überschreibe die Liste in values-prod.yaml mit einem anderen Eintrag und rendere — du wirst sehen, dass nur noch der eine Eintrag übrig ist, nicht drei.

Typische Stolperfallen

Nil-Pointer bei verschachtelten Values: {{ .Values.ingress.annotations.foo }} crasht das Rendern, wenn ingress gar nicht gesetzt ist („nil pointer evaluating interface”). Absichern mit if-Ketten, with oder default dict.

Listen werden ersetzt, nicht gemerged: Wer in einer Umgebungsdatei „nur einen Eintrag ergänzen” will, verliert alle Default-Einträge. Liste komplett angeben oder das Chart auf Maps umbauen.

Typ-Verlust durch YAML und --set: tag: 1.30 ohne Anführungszeichen ist eine Zahl, --set version=01 ebenfalls. In Templates quote nutzen, in Values-Dateien Strings quoten, bei Flags --set-string.

Vergessene Whitespace-Kontrolle: toYaml ohne nindent oder if-Blöcke ohne {{- erzeugen falsch eingerücktes YAML, das erst beim Rendern oder — schlimmer — erst beim Apply auffällt. Gerendertes YAML immer ansehen (Lektion 7 vertieft das).

Interview-Vorbereitung

Auf „Wie funktioniert das Templating in Helm?” antwortest du in drei Schichten: Quellen (Values aus Chart-Defaults, -f-Dateien und --set, spätere gewinnen; Maps mergen, Listen ersetzen), Engine (Go-Templates plus Sprig; Built-ins .Values, .Release, .Chart, .Capabilities) und Hygiene (required für Pflichtwerte, quote für Typen, toYaml | nindent für Blöcke). Ein konkretes Beispiel wie die Image-Tag-Zeile mit default .Chart.AppVersion macht die Antwort greifbar.

Follow-ups, mit denen du rechnen solltest:

  • „In welcher Reihenfolge überschreiben sich Values?” — Chart-Defaults < -f-Dateien (in Angabereihenfolge) < --set.
  • „Was passiert beim Merge mit Listen?” — sie werden vollständig ersetzt; nur Maps werden tief zusammengeführt.
  • „Wofür ist nindent da?” — YAML-Block mit Zeilenumbruch korrekt einrücken, typisch nach toYaml oder include.
  • „Wie kommst du in einer range-Schleife an .Release.Name?” — über $, das Wurzelobjekt bleibt dort erreichbar.

Zusammenfassung

Die Template-Engine verbindet drei Dinge: Built-in-Objekte als Datenquellen (.Values, .Release, .Chart, .Capabilities), eine klar definierte Values-Hierarchie (Defaults < Dateien < --set, Listen werden ersetzt) und Sprig-Funktionen als Werkzeugkasten (default, quote, required, toYaml | nindent). Kontrollstrukturen wie if, range und with bringen Logik ins YAML — mit $ als Anker zum Wurzelkontext.

Sobald mehrere Templates dieselben Label-Blöcke und Namens-Logik brauchen, willst du das nicht kopieren. Die nächste Lektion zeigt mit Named Templates und _helpers.tpl, wie Charts DRY bleiben.