Lesson 13 Helpers & Named Templates
Theme:

Helpers & Named Templates

Eliminate duplication and enforce consistency across your Helm charts using _helpers.tpl, define/include, and reusable template snippets.

Introduction

As Helm charts grow beyond a single Deployment and Service, you quickly notice the same YAML fragments appearing in multiple template files: resource names, label blocks, annotation sets, image pull secret lists. Copy-pasting these fragments is fragile — a label change in one file but not another silently breaks selector matching, and a typo in a resource name leads to hours of debugging.

Helm solves this with named templates (also called "partials" or "helpers"). You define a reusable snippet once, give it a name, and invoke it wherever needed. The convention file for these snippets is templates/_helpers.tpl, though any file prefixed with an underscore works. This lesson covers the mechanics of defining, invoking, and composing named templates, then walks through every helper that helm create generates by default.

The _helpers.tpl Convention

Helm's template engine processes every file inside the templates/ directory. Files whose names begin with an underscore (_) are special: they are parsed for template definitions but never rendered as standalone Kubernetes manifests. This makes them the perfect home for reusable snippets.

By convention, charts place all named templates in a single file called _helpers.tpl. You are free to split helpers across multiple underscore-prefixed files (e.g., _labels.tpl, _probes.tpl), but the single-file convention is overwhelmingly standard and what helm create produces.

Why the underscore? Without the underscore prefix, Helm would attempt to render the file as a Kubernetes resource and send it to the API server. Since _helpers.tpl contains only define blocks and no actual resource YAML, the API server would reject it. The underscore tells Helm: "parse this file for definitions, but do not output it."

Defining Named Templates

A named template is created with the define action and closed with end:

{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

Key points about the syntax:

include vs template

Go's standard library provides the template action to invoke a named template. Helm adds a custom include function. The difference is critical:

Featuretemplateinclude
OriginGo standard libraryHelm-specific function
Return valueWrites directly to output (returns nothing)Returns a string
Pipeline supportNo — cannot pipe resultYes — pipe into nindent, toYaml, etc.
RecommendedRarely used in Helm chartsAlways preferred

Because template writes directly to the output stream, you cannot post-process its result. This means the following does not work:

{{/* BROKEN - template cannot be piped */}}
labels:
  {{ template "mychart.labels" . | nindent 4 }}

The pipeline (| nindent 4) applies to the return value, but template returns nothing — the labels have already been written to output without indentation. The correct approach:

{{/* CORRECT - include returns a string */}}
labels:
  {{- include "mychart.labels" . | nindent 4 }}
Rule of thumb: Always use include in Helm charts. Reserve template for the rare case where you are writing raw Go templates outside of Helm.

The Standard Helpers from helm create

When you run helm create mychart, Helm scaffolds a _helpers.tpl with six named templates. Understanding these is essential because nearly every production chart follows this pattern.

mychart.name

Returns the chart name (from Chart.yaml), allowing an override via .Values.nameOverride. Truncated to 63 characters to comply with Kubernetes DNS label limits.

{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

mychart.fullname

Produces a unique resource name by combining the release name and the chart name. Supports fullnameOverride for complete control, and nameOverride for the chart-name portion only. Also truncated to 63 characters.

mychart.chart

Returns a name-version string (e.g., mychart-0.1.0) used for the helm.sh/chart label. Version dots and plus signs are replaced to keep the label value DNS-safe.

{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

mychart.labels

Produces the full set of recommended Kubernetes labels. It calls mychart.selectorLabels internally and adds informational labels:

{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

mychart.selectorLabels

The minimal label set used in Deployment selectors and Service selectors. These labels must remain immutable after the first install — Kubernetes forbids changing a Deployment's spec.selector.

{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

mychart.serviceAccountName

Conditionally returns either a user-specified name or the chart's fullname, depending on whether ServiceAccount creation is enabled:

{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Writing Custom Helpers

The standard helpers cover naming and labeling, but real charts need more. Here are two commonly needed custom helpers.

Image Pull Secret Helper

Many organizations use private container registries. Rather than repeating the imagePullSecrets block in every Deployment and Job, extract it into a helper:

{{- define "mychart.imagePullSecrets" -}}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- range . }}
  - name: {{ . }}
{{- end }}
{{- end }}
{{- end }}

Usage: {{- include "mychart.imagePullSecrets" . | nindent 6 }}

Annotation Merge Helper

When you need to combine chart-level annotations with resource-specific annotations:

{{- define "mychart.mergeAnnotations" -}}
{{- $common := .common | default dict -}}
{{- $extra := .extra | default dict -}}
{{- toYaml (merge $extra $common) -}}
{{- end }}

Invoke it by passing a custom dict:

annotations:
  {{- include "mychart.mergeAnnotations" (dict "common" .Values.commonAnnotations "extra" .Values.serviceAnnotations) | nindent 4 }}

Scope and the Dot

Every Go template executes within a scope — the value of . (dot). When Helm renders a template file, dot starts as the root context containing .Values, .Release, .Chart, and .Capabilities. When you call include, the second argument becomes the new dot for the called template.

This matters inside control structures that rebind dot:

{{- range .Values.containers }}
  {{/* . is now the current container, not root */}}
  name: {{ .name }}
  image: {{ .image }}
  labels:
    {{- include "mychart.labels" $ | nindent 8 }}
    {{/* $ always refers to the root scope */}}
{{- end }}

The $ variable is set once at the start of template execution and never changes, making it your escape hatch when dot has been rebound by range, with, or other actions.

Tip: If you find yourself frequently needing $ inside a helper, consider whether the helper should accept a more specific scope (like a dict with only the values it needs) rather than the entire root context.

DRY Patterns: Extracting Repeated YAML

Beyond labels and names, look for any YAML block that appears in more than one template file. Common candidates include:

Extract each into a define block, parameterize the variable parts through scope, and invoke with include plus nindent.

Overriding Helpers in Parent Charts

Helm processes templates in a specific order: subcharts are loaded before the parent chart. When two define blocks share the same name, the last definition wins. This means a parent chart can override any helper defined by a subchart.

For example, if subchart backend defines "backend.fullname", the parent chart can redefine it:

{{/* Parent chart's _helpers.tpl */}}
{{- define "backend.fullname" -}}
{{- printf "%s-api" .Release.Name -}}
{{- end }}

This is powerful but requires care. Always document which helpers are intended as extension points. The naming convention (chartname.helpername) makes it explicit which chart originally owns each helper.

Caution: Overriding helpers can produce subtle bugs if the parent does not preserve the contract (e.g., truncation to 63 characters, or the expected label keys). Only override when you have a clear reason and understand the downstream effects.

Partials for Complex Resources

For charts that generate many similar resources (e.g., multiple microservices in a mono-chart), you can use helpers as partials that produce entire resource sections. Combined with range over a list in values, this pattern can generate multiple Deployments from a single template file:

{{- define "mychart.deploymentSpec" -}}
spec:
  replicas: {{ .replicas | default 1 }}
  selector:
    matchLabels:
      app: {{ .name }}
  template:
    metadata:
      labels:
        app: {{ .name }}
    spec:
      containers:
        - name: {{ .name }}
          image: "{{ .image }}:{{ .tag | default "latest" }}"
          ports:
            - containerPort: {{ .port | default 8080 }}
{{- end }}

This technique is an advanced pattern. For most charts, keeping helpers focused on small, composable fragments (labels, probes, annotations) produces more maintainable code than generating entire resources from helpers.

Summary

Named templates are the backbone of maintainable Helm charts. The key takeaways: