Helm's templating language is Go's text/template augmented with the Sprig function library. This lesson walks through the syntax you will encounter in real charts and the workflow for editing safely.
Actions, Pipelines, and Whitespace
Anything inside {{ ... }} is an action — an expression to evaluate. Outside the braces is plain text emitted verbatim.
image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
The | is a pipeline: pass the value on the left to the function on the right. Above: if image.tag is empty, use Chart.AppVersion instead.
Templates emit whitespace literally, which produces invalid YAML if you are not careful. The dash variants trim:
{{-trims preceding whitespace (including newline)-}}trims following whitespace
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
{{- end }}
Common Functions You Will See Daily
| Function | Use |
|---|---|
default "x" .Values.foo | Fallback if foo is empty |
required "msg" .Values.foo | Fail render if foo missing |
quote | Wrap in double quotes (force string) |
toYaml / toJson | Serialise a structure |
nindent N | Add newline + N spaces; ideal after toYaml |
tpl "..." . | Render a string that itself contains template syntax |
lookup | Query the cluster for existing resources at render time |
printf | Format strings (sprintf semantics) |
trunc N | trimSuffix "-" | Bound names to Kubernetes 63-char DNS limit |
sha256sum | Hash content (used in pod-annotation rollouts) |
Conditionals and Loops
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "my-app.fullname" . }}
spec:
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- range .Values.autoscaling.metrics }}
- type: {{ .type }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
range sets . to each element. Inside the loop, the outer scope is reached via $: {{ $.Release.Name }}.
Named Templates and _helpers.tpl
The _helpers.tpl file conventionally holds reusable named templates. Define with define, call with include:
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "my-app.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end -}}
Used in deployment.yaml:
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
Use include, not template. template is an action (statement) and cannot be piped to nindent or indent; include returns a string and composes cleanly.
The Recommended Label Set
Kubernetes defines recommended labels that helm create wires up by default. Two helpers usually exist:
my-app.labels— the full set, on every resource'smetadata.labelsmy-app.selectorLabels— the subset used inspec.selector(just name + instance). These must be immutable;app.kubernetes.io/versionchanges between releases and so cannot be a selector.
Annotating Pods to Trigger Rollouts on Config Change
A subtle but important pattern. If your Deployment reads from a ConfigMap and you only change the ConfigMap, the pods do not roll. Embed a hash of the config into a pod annotation so the pod template changes whenever the config does:
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
Debugging
Never debug by installing. Render locally:
helm template my-release ./my-app -f prod-values.yaml
helm template my-release ./my-app --debug --dry-run
helm install my-release ./my-app --dry-run --debug
helm lint ./my-app
helm lint ./my-app --strict # treat warnings as errors
helm template emits to stdout — pipe through less, kubectl apply --dry-run=server -f - for server-side validation, or kubeconform for CRD-aware schema checks.
Common Mistakes
- Whitespace causing invalid YAML. Use
helm templateliberally. - Forgetting
quoteon numeric strings like version"1.10"(YAML parses it as 1.1). - Iterating a nil value. Guard with
{{- if .Values.something }}. - Modifying selectors between releases. Deployment selectors are immutable; releases fail to upgrade.
- Using
templateinsidenindent. Useinclude.