Skip to content
9 min read·Lesson 3 of 8

Templates, Values, and the Helm Engine

How Helm renders templates: Go template syntax, sprig functions, conditionals, loops, named templates, and debugging with helm template.

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

FunctionUse
default "x" .Values.fooFallback if foo is empty
required "msg" .Values.fooFail render if foo missing
quoteWrap in double quotes (force string)
toYaml / toJsonSerialise a structure
nindent NAdd newline + N spaces; ideal after toYaml
tpl "..." .Render a string that itself contains template syntax
lookupQuery the cluster for existing resources at render time
printfFormat strings (sprintf semantics)
trunc N | trimSuffix "-"Bound names to Kubernetes 63-char DNS limit
sha256sumHash 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's metadata.labels
  • my-app.selectorLabels — the subset used in spec.selector (just name + instance). These must be immutable; app.kubernetes.io/version changes 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 template liberally.
  • Forgetting quote on 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 template inside nindent. Use include.

Key Takeaways

  • Helm uses Go templates plus the Sprig function library — hundreds of helpers available.
  • {{ }} is execution; {{- and -}} trim whitespace; pipelines | chain functions.
  • include is preferred over template — it returns a string usable in pipelines.
  • Use helm template and helm lint to debug before installing into a cluster.
  • _helpers.tpl is the conventional home for fullname, labels, and selector helpers.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →