⎈ Helm Lektion 6/12 ~8 Min. Fortgeschritten

Named Templates und _helpers.tpl

Wiederverwendbare Template-Bausteine.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du mit define eigene Named Templates schreiben und sie per include einbinden, erklärst sicher den Unterschied zwischen include und template, und weißt, warum Label-Helper und die app.kubernetes.io/*-Standard-Labels in praktisch jedem Chart stecken. Außerdem kennst du die tpl-Funktion, mit der du Template-Ausdrücke in Values rendern kannst.

Das Problem: Derselbe Block in fünf Templates

Ein realistisches Chart hat ein Deployment, einen Service, einen ServiceAccount, eine Ingress und einen HPA. Jede dieser Ressourcen braucht denselben Namen (abgeleitet aus Release- und Chart-Name) und denselben Label-Satz. Kopierst du die Blöcke, hast du fünf Stellen, die synchron bleiben müssen — und Labels, die auseinanderdriften, sind nicht kosmetisch: Wenn der selector des Service andere Labels trägt als die Pods, routet er ins Leere.

Go-Templates lösen das mit Named Templates: benannte, wiederverwendbare Template-Blöcke, definiert mit define, eingebunden mit include. Per Konvention liegen sie in templates/_helpers.tpl — der Unterstrich sorgt dafür, dass die Datei selbst kein Manifest erzeugt (Lektion 2).

define und include: der Mechanismus

Ein Named Template definierst du so:

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

Und bindest es im Deployment ein:

metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}

Zwei Details sind hier entscheidend. Erstens der Namespace der Template-Namen: Alle Named Templates aller geladenen Charts (inklusive Sub-Charts!) teilen sich einen globalen Namensraum. Deshalb die Konvention, den Chart-Namen voranzustellen — mychart.labels statt labels. Definieren zwei Charts denselben Namen, gewinnt eines stillschweigend, und du debuggst seltsam gerenderte Manifeste.

Zweitens das zweite Argument von include: der Kontext. Named Templates sehen nicht automatisch deine .Values — sie sehen genau das, was du übergibst. include "mychart.labels" . reicht das komplette Wurzelobjekt weiter; du kannst aber auch gezielt .Values.ingress übergeben oder mit dict ein eigenes Kontextobjekt bauen. Der häufigste Fehler in selbstgeschriebenen Helpern ist ein vergessener oder falscher Kontext — dann ist .Chart.Name im Helper plötzlich leer.

include vs. template: warum include fast immer gewinnt

Go-Templates bringen das Schlüsselwort template zum Einbinden mit — Helm-Charts benutzen trotzdem fast ausschließlich include. Der Grund: template ist eine Anweisung, die ihren Output direkt an Ort und Stelle schreibt. Sie ist keine Funktion, ihr Ergebnis kann nicht in eine Pipeline: {{ template "mychart.labels" . | nindent 4 }} ist ein Syntaxfehler.

include ist Helms Antwort darauf: eine echte Funktion, die das gerenderte Template als String zurückgibt. Den String kannst du weiterverarbeiten — und genau das brauchst du in YAML ständig, weil derselbe Label-Block an einer Stelle 4 und an anderer 8 Spaces Einrückung braucht: include ... | nindent 4. Merksatz für das Interview: template schreibt, include liefert zurück — und nur was zurückkommt, lässt sich einrücken.

Die Standard-Labels: app.kubernetes.io/* und der Selector

Das helm create-Gerüst erzeugt zwei Label-Helper, und die Trennung ist bewusst:

  • mychart.labels — der volle Satz für metadata.labels: app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/version, app.kubernetes.io/managed-by, helm.sh/chart. Diese von Kubernetes empfohlenen Labels sind die gemeinsame Sprache des Ökosystems — kubectl get all -l app.kubernetes.io/instance=my-release findet alles, was zu einem Release gehört, und Dashboards wie Monitoring-Tools gruppieren danach.
  • mychart.selectorLabels — eine minimale Teilmenge (nur name und instance) für spec.selector.matchLabels des Deployments und den Service-Selector.

Warum die Trennung? Der Selector eines Deployments ist immutable. Stünde helm.sh/chart: mychart-0.1.0 im Selector, würde jedes Chart-Versions-Bump den Selector ändern wollen — und das Upgrade schlüge mit „field is immutable” fehl. In den Selector gehören also nur Labels, die sich über die Lebenszeit des Release nie ändern. Wer diesen Zusammenhang im Interview erklären kann, hat verstanden, warum die Helper so geschnitten sind.

Die tpl-Funktion: Templates in Values rendern

Manchmal soll der Nutzer des Charts Template-Ausdrücke mitgeben — etwa eine Annotation, die den Release-Namen enthalten soll:

# values.yaml des Nutzers
podAnnotations:
  backup.example.com/target: "{{ .Release.Name }}-data"

Values sind aber reine Daten; der String käme wörtlich im Manifest an. Die Funktion tpl rendert einen String als Template, mit dem Kontext, den du übergibst:

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

Das Muster findest du in vielen verbreiteten Charts überall dort, wo Values dynamisch sein sollen. Der Trade-off: tpl rendert bei jedem Aufruf neu und macht Values schwerer nachvollziehbar — was im Manifest landet, steht nicht mehr wörtlich in der Datei. Setze es gezielt ein, nicht flächendeckend.

Praxis: einen eigenen Helper bauen

helm create demo

Lies zuerst demo/templates/_helpers.tpldemo.fullname mit seiner Truncation auf 63 Zeichen (DNS-Limit für Kubernetes-Namen) und das Helper-Paar demo.labels/demo.selectorLabels. Ergänze dann am Dateiende einen eigenen Helper:

{{- define "demo.image" -}}
{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
{{- end }}

Nutze ihn in templates/deployment.yaml:

          image: {{ include "demo.image" . | quote }}

Und prüfe das Ergebnis:

helm template demo ./demo --show-only templates/deployment.yaml
helm template demo ./demo --set image.tag=1.27 --show-only templates/deployment.yaml

# Kontext-Fehler einmal absichtlich provozieren:
# im include das "." durch .Values ersetzen und erneut rendern

Beim provozierten Fehler siehst du, wie sich ein falscher Kontext äußert: kein lauter Crash, sondern leere Stellen oder eine Fehlermeldung wie „nil pointer evaluating” — je nachdem, worauf der Helper zugreift.

Typische Stolperfallen

Kollidierende Template-Namen: Der Namensraum ist global über Chart und alle Sub-Charts. Ein Helper namens labels ohne Chart-Präfix kann von einer Dependency überschrieben werden — immer <chartname>.<helpername>.

Falscher oder fehlender Kontext: include "demo.labels" ohne . übergibt nichts; der Helper rendert leer oder bricht ab. Bei eigenen Kontexten mit dict musst du im Helper exakt die übergebenen Schlüssel verwenden.

Chart-Version im Selector: mychart.labels (mit helm.sh/chart) statt mychart.selectorLabels in matchLabels verwendet — das erste Chart-Update scheitert am immutablen Selector-Feld.

template statt include benutzt: Funktioniert, bis du einrücken musst — dann steht Output an falscher Position im YAML und der Fehler zeigt sich erst beim Rendern oder Apply.

Interview-Vorbereitung

Die Standardfrage ist „Was steht in _helpers.tpl und wozu?”. Starke Antwort: Named Templates als DRY-Mechanismus (define + include), mit den zwei Standardbeispielen Namens-Helper (fullname, 63-Zeichen-Limit) und Label-Helper — und dabei die Trennung labels vs. selectorLabels mit dem immutablen Selector begründen. Damit deckst du Mechanik und Betriebsrelevanz in einer Antwort ab.

Follow-ups:

  • „Unterschied include und template?” — include ist eine Funktion mit String-Rückgabe (pipelinefähig, z. B. | nindent), template eine Anweisung ohne Rückgabewert.
  • „Warum app.kubernetes.io/*-Labels?” — empfohlene Standard-Labels, gemeinsame Konvention für Tooling, Selektion und Monitoring über alle Charts hinweg.
  • „Was macht tpl?” — rendert einen String aus den Values als Template; ermöglicht dynamische Values wie {{ .Release.Name }} in Annotations.
  • „Warum sehen Helper meine Values nicht automatisch?” — sie bekommen nur den explizit übergebenen Kontext; üblich ist ., das Wurzelobjekt.

Zusammenfassung

Named Templates sind die Funktionen der Template-Welt: definiert mit define in _helpers.tpl, eingebunden mit include plus explizitem Kontext, eingerückt mit nindent. Der Namensraum ist global — daher das Chart-Präfix. Die Standard-Helper liefern Namen (mit 63-Zeichen-Truncation) und die app.kubernetes.io/*-Labels, sauber getrennt in volle Labels und immutable Selector-Labels. tpl rendert Template-Ausdrücke aus Values, sparsam eingesetzt.

Je mehr Logik in Templates und Helpern steckt, desto wichtiger wird die Frage: Wie sehe ich, was wirklich gerendert wird — bevor es im Cluster landet? Genau darum geht es in der nächsten Lektion.