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.

Want structured learning?

Take the full Helm course →