Helm values and templating — Go template patterns in production
What you'll learn
- Design values.yaml structures that are environment-portable
- Apply Go template patterns (conditionals, loops, functions)
- Choose between --set and -f for value overrides
- Apply the operational discipline of treating values files as production configuration
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Helm values are the chart’s configuration. This lesson walks the values.yaml structure, Go template patterns, values merging, the —set vs -f trade-off, value validation, and the operational discipline.
The values structure
# values.yaml
replicaCount: 3
image:
repository: myapp
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
The values structure:
- Nested maps for related values (
image.repository,image.tag). - Lists for arrays (
ingress.hosts). - Booleans for feature toggles (
ingress.enabled,autoscaling.enabled). - Defaults for sensible production values.
The discipline is to structure values logically; the
template uses dot notation ({{ .Values.image.tag }})
to access them.
Go template patterns
# Conditional
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mychart.fullname" . }}
spec:
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mychart.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
Common patterns:
- Conditional.
{{- if .Values.X }}…{{- end }}renders the block only if the condition is true. - Loop.
{{- range .Values.X }}…{{- end }}iterates over a list. - Variable capture.
{{ $ := . }}captures the root context; use$to reference it inside a range. - Function call.
{{ .host | quote }}pipes the value to thequotefunction. - YAML rendering.
{{- toYaml .Values.resources | nindent 12 }}converts a value to YAML and indents it.
Values merging
flowchart LR
A[values.yaml defaults] --> D[Final values]
B[-f prod-values.yaml] --> D
C[--set replicaCount=5] --> D
The merge order (later overrides earlier):
- Chart’s values.yaml (defaults).
- Files passed with
-f(in order). - Values passed with
--set(highest priority).
This allows a base values file plus environment- specific overrides:
helm install myrelease mychart/ \
-f values.yaml \
-f values-prod.yaml \
--set image.tag=2.0.0
The --set image.tag=2.0.0 overrides anything in
the files.
—set vs -f
flowchart LR
A[--set replicaCount=5] --> B[Hard to review]
A --> C[Not in version control]
A --> D[Best for ad-hoc overrides]
E[-f values-prod.yaml] --> F[Easy to review]
E --> G[In version control]
E --> H[Best for production]
The trade-off:
- —set. Quick; convenient for ad-hoc overrides and tests. Hard to review; not in version control.
- -f. Reviewable; version-controlled. Best for production.
Value validation (values.schema.json)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["image", "service"],
"properties": {
"image": {
"type": "object",
"required": ["repository", "tag"],
"properties": {
"repository": {"type": "string"},
"tag": {"type": "string"}
}
},
"replicaCount": {"type": "integer", "minimum": 1},
"service": {
"type": "object",
"properties": {
"port": {"type": "integer", "minimum": 1, "maximum": 65535}
}
}
}
}
A JSON schema in values.schema.json validates the values during install/upgrade. Required fields, type checks, and range constraints prevent common errors.
helm install myrelease mychart/ -f values-prod.yaml
# Error: values don't satisfy schema
Quiz
Knowledge check · 4 questions
Q1. Given `-f base.yaml -f prod.yaml` with the same key in both, which value applies?
Q2. A list value in a later values file is appended to the list from an earlier file.
Q3. Account for a production memory limit that appears in no values file, and bring it under version control.
`payments` in `prod-app` runs with `resources.limits.memory: 8Gi`, but every values file in the repository says `2Gi` and nobody can find the change. `helm get values payments -n prod-app` lists `resources.limits.memory: 8Gi` among the user-supplied values, and the CI job that last ran `helm upgrade` passes `--set resources.limits.memory=$MEM_LIMIT` from a pipeline variable.
Q4. Which command shows the values a release is actually running with including the chart's own defaults, and what does the same command show without that flag?
Passing score: 75%. Answers are checked in this browser.
The operational discipline
Helm values in production rest on five non-negotiable elements:
- Values files in version control. Production values are in Git (or another VCS).
- Pin chart and app versions. Never use
latest. - Validate with schema. values.schema.json catches errors before apply.
- Use -f for production. Reserve —set for development.
- Document the values. Comment the values file; explain non-obvious values.
Helm values are production configuration. Treat them with the same rigour as Kubernetes manifests.