In the last chapter we learned to read and shape values. Now we add logic: generating resources conditionally, looping to avoid repetition, and factoring out reusable snippets. These are the tools that let one small chart deploy a whole application.
Conditionals: if / else
Render a block only when a condition is true:
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
{{- end }}
You can add else and else if too:
type: {{ if .Values.service.external }}LoadBalancer{{ else }}ClusterIP{{ end }}
Helm treats these as false: false, 0, an empty string, an empty list, an empty
map, and nil. Everything else is true.
Loops: range
range iterates over a list or a map — perfect for generating N similar resources. Over
a list:
env:
{{- range .Values.extraEnv }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
Inside the loop, . (the dot) is rebound to the current item. Over a map you get
both key and value:
{{- range $key, $value := .Values.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
The dot and the $ root
This trips up every beginner, so it’s worth stating clearly: inside a range (or
with), the dot . changes to point at the current item. If you still need the
top-level context — .Values, .Release, .Chart — use the root object $,
which always points at the very top:
{{- range $name, $svc := .Values.services }}
image: "{{ $.Values.image.registry }}/{{ $svc.repository }}"
{{- end }}
Here $svc is the current service, but $.Values.image reaches back to the global
image settings. We’ll rely on exactly this pattern when we template the Book Info app.
Scope: with
with narrows the dot to a sub-object, saving repetition:
{{- with .Values.resources }}
resources:
requests:
cpu: {{ .requests.cpu }}
memory: {{ .requests.memory }}
{{- end }}
Inside the with, . is .Values.resources. Bonus: the block is skipped entirely if
resources is empty — a neat way to make optional sections disappear.
Named templates: define and include
Repetition across files (labels, selectors, names) is best factored into named
templates in _helpers.tpl. Define one with define:
{{- define "book-info.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
Then pull it in wherever you need it with include, piping through nindent to get the
indentation right:
metadata:
labels:
{{- include "book-info.labels" . | nindent 4 }}
Why
includeand nottemplate? Helm has atemplateaction too, but it can’t be piped into functions likenindent.includereturns the rendered text as a string, so it composes with pipelines. Always preferinclude.
Notice we pass . as the second argument — that hands the current context to the named
template so it can read .Chart, .Release, and so on. Inside a range, you’d pass
$ instead to give it the root context.
fail and required — guard rails
You can stop a render with a clear error when a required value is missing:
image: {{ required "image.repository is required!" .Values.image.repository }}
This is far friendlier than shipping a broken manifest to the cluster.
A complete helper file
Putting it together, a typical _helpers.tpl provides a name helper and a labels
helper that every resource reuses:
{{- define "book-info.name" -}}
{{ .Chart.Name }}
{{- end -}}
{{- define "book-info.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 }}
{{- end -}}
The example webapp chart (with _helpers.tpl and flow control) is in
04-flow-control-and-helpers.
We now have every building block we need. In the next chapter we put it all to work and turn the Book Info microservices into a real, reusable chart.