Helm’s range function is your go-to for looping through lists and maps within your templates, but it’s not just about spitting out identical YAML blocks. The real power lies in how you can dynamically transform and filter data as you iterate, making your Helm charts incredibly flexible.

Let’s see range in action. Imagine you have a list of application versions and you want to create a Kubernetes Deployment for each, but with a specific suffix for each environment.

# values.yaml
versions:
  - app: frontend
    version: "1.2.0"
  - app: backend
    version: "2.1.1"

environments:
  - name: staging
    suffix: -stg
  - name: production
    suffix: -prod

Now, in your deployment.yaml template, we’ll use nested range to achieve this:

# templates/deployment.yaml

{{- range .Values.environments }}

---
apiVersion: apps/v1
kind: Deployment
metadata:

  name: {{ .name }}-app

spec:
  replicas: 1
  selector:
    matchLabels:

      app: {{ .name }}-app

  template:
    metadata:
      labels:

        app: {{ .name }}-app

    spec:
      containers:

        {{- range $.Values.versions }}


        - name: {{ $.name }}-{{ .app }}


          image: "{{ .app }}:{{ .version }}{{ $.suffix }}"

          ports:
            - containerPort: 8080

        {{- end }}


{{- end }}

When you helm template . with this setup, you’ll get:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: staging-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: staging-app
  template:
    metadata:
      labels:
        app: staging-app
    spec:
      containers:
        - name: staging-frontend
          image: "frontend:1.2.0-stg"
          ports:
            - containerPort: 8080
        - name: staging-backend
          image: "backend:2.1.1-stg"
          ports:
            - containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: production-app
  template:
    metadata:
      labels:
        app: production-app
    spec:
      containers:
        - name: production-frontend
          image: "frontend:1.2.0-prod"
          ports:
            - containerPort: 8080
        - name: production-backend
          image: "backend:2.1.1-prod"
          ports:
            - containerPort: 8080

Notice how the outer range iterates through environments, and for each environment, the inner range iterates through versions. The $. prefix is crucial here: $ always refers to the root of your values, so $.Values.versions accesses the versions list from the root context, while .name and .app refer to the current item in their respective range loops. This allows you to combine data from different levels of your values.yaml dynamically.

The mental model for range is that it creates a new scope for each iteration. Inside that scope, . refers to the current item. To access variables from an outer scope (including the root values), you use the $ prefix to explicitly jump out of the current scope. This is why $.Values.versions works when you’re already inside a .Values.environments loop.

When you’re ranging over a map, the syntax is similar, but the loop variable gets two values: the key and the value. For example, if you had ports: {http: 80, https: 443}, you’d write {{- range $name, $port := .Values.ports }}. Inside the loop, $name would be "http" and $port would be 80 in the first iteration, then "https" and 443 in the second. This is incredibly useful for generating configuration like environment variables or port mappings from a map.

A common pitfall is forgetting the $ when you need to access a parent scope’s value. This often leads to errors like "cannot access field 'versions' of type string" because Helm incorrectly assumes you’re trying to access a field on the current loop item (which might be a simple string or number) instead of the root values. Always be mindful of your current scope and use $ liberally when you need to step outside it.

Once you’ve mastered range for iterating and combining data, you’ll naturally want to explore how to conditionally include or exclude items within those loops using if statements, which is the next logical step in building truly dynamic Helm charts.

Want structured learning?

Take the full Helm course →