24.5 Flow Control: if, range, with
Right, let’s talk about making your Helm charts less like a static to-do list and more like a dynamic, context-aware application. This is where the real power—and the real headaches—begin: flow control with if, range, and with.
These are your fundamental building blocks for logic within your chart templates. They’re not some fancy add-on; they’re the core tools you use to say, “If this thing exists, do this,” or “For each of these things, make a chunk of YAML.” They’re powered by Go’s text/template library, which means they’re powerful but occasionally feel like they were designed by someone who thinks parentheses are for the weak. We’ll get to that.
The if Statement: Your Conditional Friend
The if statement is how you prevent Helm from barfing when it tries to access a value that doesn’t exist. It’s your number one defense against nil pointer errors. The basic structure is dead simple:
{{- if CONDITION }}
# YAML that gets rendered if the condition is true
{{- end }}
Now, the CONDITION is where the magic and the occasional frustration happens. Helm uses Go’s notion of “truthiness.” The empty values—false, 0, any nil value, an empty string "", an empty map {}, or an empty list []—are all considered false. Anything else is true. A non-empty string, even "false", is true. Remember that. It will bite you.
Let’s look at a real-world example. You want to add an annotation only if a custom tag is set in values.yaml.
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-myapp
{{- if .Values.customTag }}
annotations:
app.github.com/tag: {{ .Values.customTag }}
{{- end }}
spec:
ports:
- port: 80
The beauty here is that if customTag isn’t defined or is an empty string, the entire annotations block vanishes from the rendered template. No trace. Clean.
You can also use else if and else for more complex logic.
{{- if eq .Values.service.type "LoadBalancer" }}
type: LoadBalancer
{{- else if eq .Values.service.type "NodePort" }}
type: NodePort
{{- else }}
type: ClusterIP
{{- end }}
Pro Tip: Always, and I mean always, use the {{- (with the dash) to strip whitespace. Without it, you’ll get blank lines in your rendered YAML, which are mostly harmless but make you look like an amateur. The dash eats the whitespace and newline before ({{-) or after (-}}) the directive. It’s the difference between tidy and tragically sloppy.
The with Statement: Scoping Your World
with is like a focused if. It says, “If this thing exists and isn’t empty, then rebrand the entire universe inside this block so that . refers to that thing.” It changes the current scope.
This is incredibly useful for diving into nested structures without having to write the same long, windy path over and over again. Let’s say you have a deeply nested value:
# values.yaml
resources:
requests:
memory: "256Mi"
cpu: "250m"
You could write your template like this, but it’s verbose and prone to typos:
spec:
containers:
- name: myapp
resources:
requests:
memory: {{ .Values.resources.requests.memory }}
cpu: {{ .Values.resources.requests.cpu }}
{{- if .Values.resources.requests.ephemeral-storage }}
ephemeral-storage: {{ .Values.resources.requests.ephemeral-storage }}
{{- end }}
The with statement cleans this up immensely. It creates a new scope where . is now the thing you passed to it.
spec:
containers:
- name: myapp
resources:
requests:
{{- with .Values.resources.requests }}
memory: {{ .memory }}
cpu: {{ .cpu }}
{{- if .ephemeral-storage }}
ephemeral-storage: {{ .ephemeral-storage }}
{{- end }}
{{- end }}
See how inside the with block, we can just reference .memory instead of the full path? Elegant. Crucial Caveat: The moment you step inside that with block, you lose access to the parent scope. You can’t suddenly ask for .Release.Name in there because . is now requests. It’s a trade-off. For this reason, I often assign the top-level context to a variable before entering a with block ({{- $top := . -}}) just in case I need to escape back to it.
The range Operator: Looping Like a Pro
When you need to create a list of things—a bunch of environment variables, multiple volumes, a set of container ports—you use range. It iterates over a map or a list (an array, in YAML terms).
Iterating over a list is straightforward. Let’s say you have a list of extra arguments:
# values.yaml
extraArgs:
- "--debug"
- "--log-level=verbose"
Your template would loop through them:
spec:
containers:
- name: myapp
args:
- "server"
{{- range .Values.extraArgs }}
- {{ . }}
{{- end }}
Inside the range block, . becomes the current item in the list. So for the first iteration, . is "--debug".
Iterating over a map is where you see the designers’ questionable choice. The syntax is… unique. Let’s say you want to create environment variables from a map:
# values.yaml
env:
LOG_LEVEL: "DEBUG"
DATABASE_URL: "postgresql://localhost:5432"
You can’t just range over this and get key-value pairs directly. You have to use a special, almost-secret syntax because Go templates are like that. You use $key and $value.
spec:
containers:
- name: myapp
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
This is the part everyone has to look up. It feels arcane because it is. But it works brilliantly. The range $key, $value construct is your only way to unpack a map. Remember it. Write it on a sticky note.
The “Empty List” Pitfall: This is a big one. If you range over a list or map that is nil or empty, it simply does nothing. It doesn’t throw an error. This is usually what you want. However, it means if your range is meant to produce a required field and the list is empty, you’ll generate invalid YAML. Always consider the case where the list you’re ranging over might be empty and structure your templates accordingly. Sometimes, you need a default empty list ([]) in your values.yaml to ensure the structure always exists for the template to range over.
Master these three tools, and you’ve just leveled up from writing charts to engineering them. They’re the difference between a chart that works for one perfect set of values and a chart that works robustly for a thousand different configurations.