Helm
Kubernetes
Environment Variables
Configuration Management
DevOps

Helm chart passing multiple environment values for single key

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When deploying applications to Kubernetes with Helm, you often need to pass environment variables to containers. Helm templates use Go templating to dynamically generate the env section of pod specs from values.yaml. This article covers how to define environment variables in Helm values files, iterate over them in templates, and handle different environments (dev, staging, production).

Basic Environment Variables in Helm

values.yaml

yaml
1env:
2  - name: DATABASE_HOST
3    value: "db.example.com"
4  - name: DATABASE_PORT
5    value: "5432"
6  - name: LOG_LEVEL
7    value: "info"

deployment.yaml template

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4  name: {{ .Chart.Name }}
5spec:
6  template:
7    spec:
8      containers:
9        - name: {{ .Chart.Name }}
10          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
11          env:
12            {{- toYaml .Values.env | nindent 12 }}

The toYaml function converts the list to YAML, and nindent 12 indents it correctly within the container spec.

Using a Map Instead of a List

A map (dictionary) is often cleaner and easier to merge across environments:

values.yaml

yaml
1envVars:
2  DATABASE_HOST: "db.example.com"
3  DATABASE_PORT: "5432"
4  LOG_LEVEL: "info"
5  APP_NAME: "my-service"

deployment.yaml template

yaml
1env:
2  {{- range $key, $value := .Values.envVars }}
3  - name: {{ $key }}
4    value: {{ $value | quote }}
5  {{- end }}

The range iterates over the map and creates an env entry for each key-value pair. Using quote ensures values are properly quoted in YAML.

Mixing Static and Dynamic Values

Combine hardcoded env vars with values from ConfigMaps and Secrets:

values.yaml

yaml
1envVars:
2  APP_NAME: "my-service"
3  LOG_LEVEL: "info"
4
5envFromSecret:
6  - name: DATABASE_PASSWORD
7    secretName: db-credentials
8    secretKey: password
9  - name: API_KEY
10    secretName: api-secrets
11    secretKey: key
12
13envFromConfigMap:
14  - name: DATABASE_HOST
15    configMapName: app-config
16    configMapKey: db_host

deployment.yaml template

yaml
1env:
2  {{- range $key, $value := .Values.envVars }}
3  - name: {{ $key }}
4    value: {{ $value | quote }}
5  {{- end }}
6  {{- range .Values.envFromSecret }}
7  - name: {{ .name }}
8    valueFrom:
9      secretKeyRef:
10        name: {{ .secretName }}
11        key: {{ .secretKey }}
12  {{- end }}
13  {{- range .Values.envFromConfigMap }}
14  - name: {{ .name }}
15    valueFrom:
16      configMapKeyRef:
17        name: {{ .configMapName }}
18        key: {{ .configMapKey }}
19  {{- end }}

Per-Environment Configuration

Use separate values files for each environment:

values-dev.yaml

yaml
1envVars:
2  LOG_LEVEL: "debug"
3  DATABASE_HOST: "dev-db.internal"
4  ENABLE_PROFILING: "true"

values-prod.yaml

yaml
1envVars:
2  LOG_LEVEL: "warn"
3  DATABASE_HOST: "prod-db.internal"
4  ENABLE_PROFILING: "false"

Deploy with the appropriate values file:

bash
1# Development
2helm install my-app ./chart -f values-dev.yaml
3
4# Production
5helm install my-app ./chart -f values-prod.yaml
6
7# Override specific values
8helm install my-app ./chart -f values-prod.yaml --set envVars.LOG_LEVEL=debug

Using envFrom to Load All Keys from ConfigMap/Secret

yaml
1# values.yaml
2envFrom:
3  - configMapRef:
4      name: app-config
5  - secretRef:
6      name: app-secrets
yaml
1# deployment.yaml
2spec:
3  containers:
4    - name: {{ .Chart.Name }}
5      envFrom:
6        {{- toYaml .Values.envFrom | nindent 8 }}

This loads all keys from the ConfigMap or Secret as environment variables without listing each one individually.

Conditional Environment Variables

yaml
1# values.yaml
2features:
3  enableMetrics: true
4  enableTracing: false
yaml
1# deployment.yaml
2env:
3  {{- range $key, $value := .Values.envVars }}
4  - name: {{ $key }}
5    value: {{ $value | quote }}
6  {{- end }}
7  {{- if .Values.features.enableMetrics }}
8  - name: METRICS_ENABLED
9    value: "true"
10  - name: METRICS_PORT
11    value: "9090"
12  {{- end }}
13  {{- if .Values.features.enableTracing }}
14  - name: TRACING_ENABLED
15    value: "true"
16  {{- end }}

Merging Values with --set

Override or add individual env vars at deploy time:

bash
1# Set a single env var
2helm install my-app ./chart --set 'envVars.NEW_VAR=hello'
3
4# Override an existing value
5helm install my-app ./chart --set 'envVars.LOG_LEVEL=debug'
6
7# Set multiple values
8helm install my-app ./chart \
9  --set 'envVars.VAR1=a' \
10  --set 'envVars.VAR2=b'

Common Pitfalls

  • Numeric values need quoting: YAML interprets 5432 as an integer, but Kubernetes env var values must be strings. Use quote in templates: value: {{ $value | quote }} to ensure values like port numbers are rendered as "5432".
  • Indentation errors: toYaml output must be indented correctly in the deployment template. Use nindent N (not indent N) to handle both the newline and indentation. Wrong indentation causes cryptic Kubernetes deployment errors.
  • Map key ordering: Go templates iterate over maps in sorted key order. If you need a specific order, use a list instead of a map.
  • Overriding list items with --set: --set env[0].name=VAR syntax is fragile with lists. Prefer using a map structure for env vars so you can override with --set envVars.KEY=value.
  • Secret values in values.yaml: Never put actual secrets (passwords, API keys) in values.yaml. Use Kubernetes Secrets with valueFrom.secretKeyRef or external secret managers (Vault, AWS Secrets Manager).

Summary

  • Define env vars as a map in values.yaml for easy merging and overriding
  • Use range to iterate over the map and generate env entries in the deployment template
  • Use valueFrom with secretKeyRef and configMapKeyRef for sensitive or external values
  • Create per-environment values files (values-dev.yaml, values-prod.yaml) and deploy with -f
  • Always quote values in templates to prevent YAML type coercion issues

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.