Helm's superpower is its template engine. Every file inside templates/ is processed by Go's text/template package, augmented with the Sprig function library and Helm-specific builtins. Understanding this engine at a deep level separates engineers who write brittle charts from those who write reusable, production-grade ones. This lesson covers the full execution model: how values flow in, how functions and pipelines transform them, and how conditionals and loops let a single template adapt to every environment.
The Template Execution Context
Every Helm template executes within a root object called . (dot). This object is a merged map of several namespaces that Helm populates before rendering begins:
.Values — the merged result of values.yaml plus every --values file plus every --set flag, in priority order (later wins).
.Chart — metadata from Chart.yaml: .Chart.Name, .Chart.Version, .Chart.AppVersion.
.Files — access to non-template files bundled in the chart (config files, certs).
Helm merges all inputs into a single root context (dot) and passes it through the Go template engine to produce rendered Kubernetes manifests.
Scope shift warning: When you enter a range loop or a with block, . is rebound to the current element or value. To reach the outer release name inside a loop, use $.Release.Name — the $ prefix always refers to the top-level root object, regardless of scope.
Values — the Public API of a Chart
values.yaml is not just a defaults file — it is the documented interface to your chart. Every key in it is a knob that operators can override. Design it the way you design a function signature: clear names, sensible defaults, grouped by concern.
Referencing values in a template is straightforward: {{ .Values.replicaCount }}. For nested keys: {{ .Values.image.repository }}. Helm renders the entire template before applying it, so a typo in a value reference produces an error at render time — not silently at runtime.
Functions and Pipelines
Helm ships with over 70 functions from Sprig plus its own additions. The key insight is that they compose via pipelines — the Unix pipe model applied to template data. The output of one function becomes the last argument of the next.
# In a template file — common function patterns
# quote: wraps the value in double-quotes (required for env var string values)
env:
- name: APP_ENV
value: {{ .Values.appEnv | quote }}
# default: supply a fallback when a value is empty
image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
# upper / lower / title — string transforms
- name: LOG_LEVEL
value: {{ .Values.logLevel | upper | quote }}
# toYaml + nindent — the most important pipeline in Helm
# Dump a complex value (map or list) as indented YAML in-place
resources:
{{ toYaml .Values.resources | nindent 10 }}
# tpl — evaluate a string value as a template (useful for constructing URLs)
annotations:
"external-dns.alpha.kubernetes.io/hostname": {{ tpl .Values.ingress.host . | quote }}
# include + nindent (for named templates — covered in lesson 5)
metadata:
labels:
{{ include "myapp.labels" . | nindent 4 }}
# sha256sum — inject a checksum annotation so pods roll on configmap changes
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
Production pattern — forced pod rollout on ConfigMap change: Kubernetes does not restart pods when a ConfigMap changes. The sha256sum annotation trick encodes the rendered ConfigMap content into the Deployment's pod spec annotation. Any ConfigMap change changes the hash, which changes the pod spec, which Kubernetes treats as a rolling update. This is a near-universal practice in mature Helm charts.
Common pitfall — forgetting nindent vs indent:indent N adds N spaces to every line of its input but does NOT add a leading newline. nindent N adds a newline first, then indents. In YAML, missing or extra whitespace silently corrupts structure. Always use nindent after a key that expects a block, and use toYaml | nindent X rather than hand-formatting nested values.
Conditionals
Helm conditionals use Go template's if / else if / else / end syntax. The condition is falsy for: false, 0, empty string, nil, empty list, and empty map. Everything else is truthy.
# templates/ingress.yaml — conditional rendering of the entire resource
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "myapp.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
{{- end }}
The - dash inside delimiters ({{- and -}}) trims whitespace and newlines on the respective side. This is critical: without it, conditional blocks leave blank lines in the rendered YAML that can trip up strict parsers or produce confusing diffs in your GitOps tooling.
The with block is a focused form of if — it both guards against empty values and rebinds . to the value, removing repetition:
# templates/deployment.yaml — with block for optional annotations
metadata:
name: {{ include "myapp.fullname" . }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
# Equivalent but more verbose:
# {{- if .Values.podAnnotations }}
# annotations:
# {{- toYaml .Values.podAnnotations | nindent 4 }}
# {{- end }}
# Capabilities-based conditional — emit HPA only if autoscaling API is available
{{- if and .Values.autoscaling.enabled (.Capabilities.APIVersions.Has "autoscaling/v2") }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
...
{{- end }}
Loops with range
The range action iterates over lists and maps. It is the mechanism behind rendering environment variables, volume mounts, init containers, and any other list-typed field in Kubernetes manifests.
# templates/deployment.yaml — ranging over a list and a map
# Iterate a list of extra env vars (each item is a map with name/value)
# values.yaml: extraEnv: [{name: "FEATURE_X", value: "on"}, {name: "REGION", value: "us-east-1"}]
env:
- name: PORT
value: {{ .Values.service.port | quote }}
{{- range .Values.extraEnv }}
- name: {{ .name | quote }}
value: {{ .value | quote }}
{{- end }}
# Iterate a map — range gives you ($key, $val) or just $val
# values.yaml: labels: {team: platform, env: prod}
{{- range $key, $val := .Values.extraLabels }}
{{ $key }}: {{ $val | quote }}
{{- end }}
# Loop with index — useful when order matters (e.g., init containers)
{{- range $i, $container := .Values.initContainers }}
- name: init-{{ $i }}
image: {{ $container.image | quote }}
command: {{ toYaml $container.command | nindent 6 }}
{{- end }}
The range action iterates over a list in values.yaml and renders the template block once per item, building the final YAML list.
Putting It Together — a Production Deployment Template
The following template is representative of what you find in a mature, production-grade chart. It combines all the concepts from this lesson: value references, pipelines with toYaml | nindent, feature-flagged blocks, capability checks, and a range loop for environment variables.
Debugging templates: Use helm template myapp ./mychart --values prod.yaml to render templates locally without touching a cluster. Pipe through | grep -A5 "kind: Deployment" to focus on one resource. For deeper inspection, helm install myapp ./mychart --dry-run --debug hits the cluster API for validation but does not apply anything — it also prints the computed values so you can verify your override chain is correct.
Mastery of Helm's template engine — values flow, function pipelines, whitespace-trimming dashes, with/range/if, and the root vs. scoped dot — is what separates a chart that works in one environment from one that reliably powers a multi-tenant, multi-environment platform. Lesson 5 extends this with named templates and helpers to eliminate duplication across files.