Helm’s if/else constructs are more powerful than a simple true/false check; they operate on the truthiness of values, which can be a tricky concept to grasp initially.
Let’s see how this plays out with a simple deployment. Imagine we have a values.yaml file:
replicaCount: 1
image:
repository: nginx
tag: latest
ingress:
enabled: true
host: myapp.example.com
And a deployment template like this:
{{ define "mychart.deployment" }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "mychart.selectorLabels" . }}
template:
metadata:
labels:
app: {{ include "mychart.selectorLabels" . }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
ports:
- containerPort: 80
name: http
---
{{ if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}-ingress
labels:
app: {{ include "mychart.selectorLabels" . }}
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "mychart.fullname" . }}
port:
number: 80
{{ end }}
{{ end }}
When we helm template . with this values.yaml, we get:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myrelease-myapp
spec:
replicas: 1
selector:
matchLabels:
app: release-app
template:
metadata:
labels:
app: release-app
spec:
containers:
- name: mychart
image: "nginx:latest"
ports:
- containerPort: 80
name: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myrelease-myapp-ingress
labels:
app: release-app
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myrelease-myapp
port:
number: 80
Notice how the entire Ingress resource is generated because ingress.enabled is true. If we change ingress.enabled to false in values.yaml and run helm template . again, the Ingress block simply disappears. The if block is a powerful way to conditionally include entire resources or parts of them.
The core problem if/else solves in Helm is the need to generate different Kubernetes manifests based on user-provided configuration. Helm charts are meant to be generic and reusable, and users will have varying infrastructure needs. For instance, a user might not want an Ingress controller for a development environment, or they might need to specify different resource limits for production. if/else allows the chart author to anticipate these variations and conditionally render only the necessary parts of the templates.
Internally, Go’s text/template (which Helm uses) evaluates the condition within the {{ if ... }} block. It checks for "truthiness." In Go templates, a value is considered "false" if it’s:
false(the boolean literal)0(the integer zero)""(an empty string)nil(a null value)- An empty slice or map
Any other value, including non-zero numbers, non-empty strings, and non-empty collections, is considered "true." This is crucial: {{ if .Values.someConfig }} will render if someConfig is set to 1, "hello", or [], but not if it’s 0, "", or nil.
Let’s illustrate with a more complex example involving else and checking for empty values. Suppose we want to set environment variables, but only if they are provided and not empty.
values.yaml:
env:
MY_VAR: "production_value"
ANOTHER_VAR: ""
YET_ANOTHER: "something"
deployment.yaml template:
{{ define "mychart.deployment" }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "mychart.selectorLabels" . }}
template:
metadata:
labels:
app: {{ include "mychart.selectorLabels" . }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
env:
{{ range $key, $value := .Values.env }}
{{- if $value }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- else }}
# This env var was provided but is empty, so we skip it.
# If you wanted to explicitly set an empty value, you'd need a different check.
{{- end }}
{{ end }}
{{ end }}
Running helm template . with the above values.yaml yields:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myrelease-myapp
spec:
replicas: 1
selector:
matchLabels:
app: release-app
template:
metadata:
labels:
app: release-app
spec:
containers:
- name: mychart
image: "nginx:latest"
env:
- name: MY_VAR
value: "production_value"
- name: YET_ANOTHER
value: "something"
Notice that ANOTHER_VAR is completely omitted because its value was an empty string "", which evaluates to false. If we did want to explicitly set an environment variable to an empty string, we’d need a slightly different approach, perhaps checking if the key exists and then explicitly setting value: "" if we wanted that behavior.
The else part of the if/else structure is triggered when the condition evaluates to false. This is useful for providing default behaviors or fallback configurations. For example, if you want to enable a feature by default but allow users to disable it:
values.yaml:
featureX:
enabled: false
deployment.yaml template:
{{ define "mychart.deployment" }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
# ... other deployment spec ...
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
{{ if .Values.featureX.enabled }}
args: ["--enable-feature-x"]
{{ else }}
args: ["--disable-feature-x"]
{{ end }}
{{ end }}
If featureX.enabled is true, the container gets --enable-feature-x. If it’s false, it gets --disable-feature-x. This provides a clear default (disabling) and an explicit override.
A common pitfall is assuming that checking for the existence of a key is the same as checking its value. For example, {{ if .Values.myKey }} will evaluate to false if .Values.myKey is 0 or "", even if the key myKey exists in the values.yaml. If you need to distinguish between a key not being present and a key being present with a falsy value, you often need to use hasKey or check against nil explicitly. However, for most common use cases where you’re toggling features or setting values, the standard truthiness check is what you want.
The most surprising thing about Helm’s if/else is how it interacts with range. When you range over a map or slice and use an if inside, the if condition is evaluated for each element. This means you can filter collections dynamically. For instance, if you have a list of ports and want to only include ones explicitly enabled:
values.yaml:
ports:
- containerPort: 80
name: http
enabled: true
- containerPort: 443
name: https
enabled: false
- containerPort: 9090
name: metrics
enabled: true
Template snippet:
ports:
{{ range .Values.ports }}
{{- if .enabled }}
- containerPort: {{ .containerPort }}
name: {{ .name }}
{{- end }}
{{ end }}
This will render only the http and metrics ports, skipping https because its enabled flag is false. The {{- end }} is important here; it consumes the trailing newline and any whitespace on that line, preventing extra blank lines in the generated YAML when a condition is not met.
The next concept to explore is how to define default values for conditional checks, particularly when a value might not exist at all.