Named Templates & Helpers
Named Templates & Helpers
By lesson 4 you know that Helm templates are Go text/template files wired to a .Values tree. But a real-world chart for a production application has ten or more templates: Deployment, Service, Ingress, HPA, PDB, ServiceAccount, RBAC, ConfigMap, NetworkPolicy, and more. Every one of them needs the same Kubernetes label set, the same selector labels, the same name helper that trims to 63 characters, and the same chart/app version annotations. Without a shared location for this logic you copy-paste the same twelve lines into every file, and when the label standard changes you have ten files to update — and you will miss one.
The solution is _helpers.tpl: a file of named templates (also called partials) that centralises reusable logic. This lesson covers the mechanics thoroughly, because getting this layer right is the difference between a maintainable chart library and a mess of duplicated YAML.
How Named Templates Work: define and include
Helm's Go template engine exposes two directives for sharing logic across files:
{{ define "name" }}…{{ end }}— declares a named template block. Any file intemplates/can define one, but convention dictates putting all of them intemplates/_helpers.tpl. Files whose names begin with an underscore are never rendered as manifests — they exist purely to contribute definitions to the shared namespace.{{ include "name" . }}— renders a named template in the current scope (the dot.) and returns the result as a string. This is the right primitive for injecting partial output into a parent document.{{ template "name" . }}— identical in semantics but does not return the rendered string; it writes directly to the output stream. This makes it impossible to pipe throughnindentortrim, so in practice you should always useinclude.
template vs include: A common chart bug is using {{ template "myapp.labels" . }} directly in a metadata: block instead of {{ include "myapp.labels" . | nindent 4 }}. The template directive cannot be piped, so indentation is hardcoded in the partial itself and breaks the moment you use it at a different nesting depth. Always prefer include.
Anatomy of _helpers.tpl
Every chart generated by helm create ships a starter _helpers.tpl. Understanding each named template in it is prerequisite to writing your own. Below is the canonical production version for a chart named myapp:
Notice several critical patterns baked into this template:
- The
{{- … -}}dash trimming removes all whitespace before and after the block, keeping rendered YAML clean. trunc 63 | trimSuffix "-"prevents Kubernetes DNS label validation errors when release names are long (e.g., in ArgoCD the release name often includes the environment and Git branch).selectorLabelsis a strict subset oflabels. Serviceselector:and DeploymentmatchLabels:useselectorLabels;metadata.labelsuses the fulllabelstemplate. This separation is essential.
Using Helpers in Templates
Here is how the Deployment template consumes the helpers, showing the exact indentation rules:
The nindent N function prepends a newline then indents the entire multi-line string by N spaces. This is why include is mandatory: nindent is a string pipeline function — it cannot accept the void output of template.
checksum/config annotation trick: Adding checksum/config: {{ include … | sha256sum }} to the pod template causes Helm to roll the Deployment automatically whenever the referenced ConfigMap changes. Without this, an helm upgrade that only modifies a ConfigMap produces zero pod restarts — your app keeps running with stale config. This annotation is a production standard at every major tech company.
Writing Custom Helpers
Beyond the standard label helpers, you will frequently write domain-specific partials. Common examples from production charts:
The Kubernetes Label Standard
The labels produced by the helper templates follow the Kubernetes Recommended Labels spec (app.kubernetes.io/*). These are not arbitrary — tooling in the ecosystem (Prometheus ServiceMonitors, Datadog autodiscovery, ArgoCD, Lens) queries resources by these labels. Getting them right unlocks observability for free.
The full set of recommended labels to emit on every resource:
app.kubernetes.io/name— the application name (not the release name). Used by Prometheus to auto-discover ServiceMonitors.app.kubernetes.io/instance— the release name (unique per install). Lets you run multiple instances of the same app in one namespace.app.kubernetes.io/version— the application version (usually.Chart.AppVersion). Feeds Datadog's version tracking.app.kubernetes.io/managed-by— alwaysHelm. Used byhelm listto discover releases.helm.sh/chart— chart name + version. Used by ArgoCD to detect chart drift.
team: platform, cost-center: infrastructure, env: prod. These are injected via a myapp.globalLabels helper that reads from .Values.global. Every resource in every chart in the company emits these labels, so cloud cost tooling (Kubecost, Infracost) can break down spend by team and environment automatically — no manual tagging.
Validating Rendered Output
You should lint your helpers before ever applying to a cluster. The three commands every chart author must know:
Run helm template in your CI pipeline on every pull request. A broken template renders broken YAML before it ever reaches the cluster, and the pipeline catches it before a reviewer even looks at the diff.
_helpers.tpl is not a convention — it is the architectural foundation of a maintainable Helm chart. Define your label standard once, enforce the include … | nindent N pattern everywhere, add the checksum/config annotation on pod templates, and your chart becomes a first-class Kubernetes citizen that plays well with the entire observability and GitOps tooling ecosystem.