Helm Values and Templates - Advanced Techniques
Mastering Helm's templating system transforms static Kubernetes manifests into dynamic, configurable deployments. The combination of structured values and powerful templates enables teams to maintain a single chart that adapts to different environments, configurations, and deployment scenarios. Understanding advanced templating patterns and value management strategies is essential for building maintainable, production-ready Helm charts.
Template Syntax Fundamentals
Helm uses Go's templating language with additional functions for Kubernetes-specific operations:
graph TB
A[Template Syntax] --> B[Actions]
A --> C[Pipelines]
A --> D[Functions]
A --> E[Control Structures]
B --> B1[{{ .Values.key }}]
B --> B2[{{- whitespace control -}}]
C --> C1[{{ .Values.key | quote }}]
C --> C2[{{ .Values.key | default "value" }}]
D --> D1[Built-in: quote, upper, lower]
D --> D2[Helm: include, toYaml, toJson]
D --> D3[Sprig: trunc, replace, regex]
E --> E1[{{- if }}]
E --> E2[{{- range }}]
E --> E3[{{- with }}]
Basic Actions:
# Simple variable substitution
name: {{ .Values.app.name }}
# With whitespace control (removes newlines)
labels:
{{- include "myapp.labels" . | nindent 2 }}
# Comments (not rendered)
{{- /* This is a comment */ -}}
Whitespace Control:
{{-removes whitespace before the action-}}removes whitespace after the action- Essential for clean YAML output
Working with Values
Values flow from multiple sources with increasing priority:
flowchart LR
A[values.yaml
Default values] --> B[Parent Chart Values]
B --> C[-f custom.yaml
Value files]
C --> D[--set flag
Command line]
D --> E[Final Values]
style E fill:#90EE90
Accessing Values:
# Direct access
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
# Nested access
database:
host: {{ .Values.db.connection.host }}
port: {{ .Values.db.connection.port }}
# Access chart metadata
version: {{ .Chart.Version }}
appVersion: {{ .Chart.AppVersion }}
# Access release information
releaseName: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
Built-in Objects:
# .Values - Values from values.yaml and overrides
{{ .Values.replicaCount }}
# .Chart - Contents of Chart.yaml
{{ .Chart.Name }}-{{ .Chart.Version }}
# .Release - Information about the release
{{ .Release.Name }}
{{ .Release.Namespace }}
{{ .Release.Service }} # Always "Helm"
# .Capabilities - Cluster capabilities
{{ .Capabilities.KubeVersion }}
{{ .Capabilities.APIVersions }}
# .Template - Current template information
{{ .Template.Name }}
{{ .Template.BasePath }}
# .Files - Access to non-special files in the chart
{{ .Files.Get "config.txt" }}
Conditional Logic
Control what gets rendered based on values:
Simple Conditionals:
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
spec:
# Ingress configuration
{{- end }}
If-Else:
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" . }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "LoadBalancer" }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- else if eq .Values.service.type "NodePort" }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
Complex Conditions:
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "myapp.fullname" . }}-data
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}
Conditional Functions:
eq- Equalne- Not equallt- Less thangt- Greater thanand- Logical ANDor- Logical ORnot- Logical NOT
Loops and Iteration
Iterate over lists and maps:
Range over Lists:
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
# With values.yaml:
# env:
# - name: LOG_LEVEL
# value: info
# - name: PORT
# value: "8080"
Range over Maps:
data:
{{- range $key, $value := .Values.configData }}
{{ $key }}: {{ $value | quote }}
{{- end }}
# With values.yaml:
# configData:
# database.host: "postgres"
# database.port: "5432"
# cache.ttl: "3600"
Range with Index:
hosts:
{{- range $index, $host := .Values.ingress.hosts }}
- host: {{ $host }}
http:
paths:
- path: /
backend:
serviceName: {{ include "myapp.fullname" $ }}-{{ $index }}
servicePort: 80
{{- end }}
Note: Use $ to reference the root context within loops.
Variables
Create and use variables within templates:
{{- $fullName := include "myapp.fullname" . -}}
{{- $chartName := .Chart.Name -}}
{{- $labels := include "myapp.labels" . -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ $fullName }}-config
labels:
{{- $labels | nindent 4 }}
data:
app: {{ $chartName }}
instance: {{ $fullName }}
Variables in Loops:
{{- $root := . -}}
{{- range .Values.services }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" $root }}-{{ .name }}
labels:
service: {{ .name }}
spec:
type: {{ .type }}
ports:
- port: {{ .port }}
selector:
app: {{ include "myapp.name" $root }}
{{- end }}
Template Functions
Helm includes powerful functions from Sprig library:
String Functions:
# Quote values
value: {{ .Values.setting | quote }}
# Uppercase/lowercase
name: {{ .Values.name | upper }}
env: {{ .Values.environment | lower }}
# Truncate and trim
shortName: {{ .Values.fullName | trunc 20 | trimSuffix "-" }}
# Replace strings
sanitized: {{ .Values.input | replace " " "-" | lower }}
# Concatenate
fullPath: {{ printf "%s/%s" .Values.basePath .Values.fileName }}
Default Values:
# Provide default if value is empty
image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
# Default for nested values
replicas: {{ .Values.replicaCount | default 3 }}
# Complex defaults
port: {{ .Values.service.port | default (ternary 443 80 .Values.tls.enabled) }}
List Functions:
# First element
primaryHost: {{ .Values.ingress.hosts | first }}
# Last element
backupHost: {{ .Values.ingress.hosts | last }}
# Has element
{{- if has "production" .Values.environments }}
# Production-specific config
{{- end }}
# Append to list
{{- $ports := list 80 443 }}
{{- $ports = append $ports 8080 }}
ports: {{ $ports }}
YAML and JSON Functions:
# Convert to YAML (most common)
resources:
{{- toYaml .Values.resources | nindent 2 }}
# Convert to JSON
config: {{ .Values.appConfig | toJson }}
# Convert from JSON
{{- $parsed := .Values.jsonString | fromJson }}
value: {{ $parsed.key }}
Indentation Functions:
# Indent by N spaces
spec:
{{ .Values.podSpec | indent 2 }}
# Newline + indent (most common)
labels:
{{- include "myapp.labels" . | nindent 2 }}
# Indent without first line
annotations:
{{- .Values.annotations | nindent 2 }}
Advanced Templating Patterns
Generating Multiple Resources from Values
# values.yaml
databases:
- name: users
size: 10Gi
storageClass: fast
- name: sessions
size: 5Gi
storageClass: standard
- name: analytics
size: 50Gi
storageClass: bulk
# templates/databases.yaml
{{- range .Values.databases }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "myapp.fullname" $ }}-{{ .name }}-pvc
labels:
{{- include "myapp.labels" $ | nindent 4 }}
database: {{ .name }}
spec:
accessModes:
- ReadWriteOnce
storageClassName: {{ .storageClass }}
resources:
requests:
storage: {{ .size }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" $ }}-{{ .name }}
labels:
{{- include "myapp.labels" $ | nindent 4 }}
database: {{ .name }}
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: postgres
selector:
database: {{ .name }}
{{- end }}
Dynamic Environment Variables
# values.yaml
envVars:
simple:
LOG_LEVEL: info
PORT: "8080"
fromSecret:
DATABASE_PASSWORD:
secretName: db-credentials
key: password
API_KEY:
secretName: api-credentials
key: key
fromConfigMap:
CONFIG_FILE:
configMapName: app-config
key: config.json
# templates/deployment.yaml
env:
{{- range $key, $value := .Values.envVars.simple }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- range $key, $value := .Values.envVars.fromSecret }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ $value.secretName }}
key: {{ $value.key }}
{{- end }}
{{- range $key, $value := .Values.envVars.fromConfigMap }}
- name: {{ $key }}
valueFrom:
configMapKeyRef:
name: {{ $value.configMapName }}
key: {{ $value.key }}
{{- end }}
Conditional Resource Limits
# templates/deployment.yaml
resources:
{{- if .Values.resources }}
{{- if or .Values.resources.limits .Values.resources.requests }}
{{- if .Values.resources.limits }}
limits:
{{- if .Values.resources.limits.cpu }}
cpu: {{ .Values.resources.limits.cpu }}
{{- end }}
{{- if .Values.resources.limits.memory }}
memory: {{ .Values.resources.limits.memory }}
{{- end }}
{{- end }}
{{- if .Values.resources.requests }}
requests:
{{- if .Values.resources.requests.cpu }}
cpu: {{ .Values.resources.requests.cpu }}
{{- end }}
{{- if .Values.resources.requests.memory }}
memory: {{ .Values.resources.requests.memory }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
# Cleaner approach using toYaml
resources:
{{- toYaml .Values.resources | nindent 2 }}
Environment-Specific Configuration
# values.yaml
global:
domain: example.com
environments:
development:
replicas: 1
resources:
limits:
cpu: 500m
memory: 256Mi
staging:
replicas: 2
resources:
limits:
cpu: 1000m
memory: 512Mi
production:
replicas: 5
resources:
limits:
cpu: 2000m
memory: 1Gi
# Environment is selected via values
environment: production
# templates/deployment.yaml
{{- $env := index .Values.environments .Values.environment }}
spec:
replicas: {{ $env.replicas }}
template:
spec:
containers:
- name: app
resources:
{{- toYaml $env.resources | nindent 10 }}
Named Templates and Includes
Create reusable template functions:
templates/_helpers.tpl:
{{/*
Generate labels
*/}}
{{- define "myapp.labels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "myapp.chart" . }}
{{- if .Values.customLabels }}
{{ toYaml .Values.customLabels }}
{{- end }}
{{- end }}
{{/*
Generate selector labels (subset of all labels)
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Generate full name with suffix
*/}}
{{- define "myapp.fullnameWithSuffix" -}}
{{- $name := include "myapp.fullname" . -}}
{{- $suffix := . | index 1 -}}
{{- printf "%s-%s" $name $suffix | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Generate container image string
*/}}
{{- define "myapp.image" -}}
{{- $registry := .Values.image.registry | default "docker.io" -}}
{{- $repository := .Values.image.repository -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s/%s:%s" $registry $repository $tag }}
{{- end }}
{{/*
Generate container pull policy
*/}}
{{- define "myapp.pullPolicy" -}}
{{- if .Values.image.pullPolicy -}}
{{ .Values.image.pullPolicy }}
{{- else if eq .Values.image.tag "latest" -}}
Always
{{- else -}}
IfNotPresent
{{- end -}}
{{- end }}
Using Named Templates:
# Using include (renders template and allows piping)
metadata:
labels:
{{- include "myapp.labels" . | nindent 4 }}
# Using template (renders template directly, no piping)
metadata:
labels:
{{ template "myapp.labels" . }}
# Passing additional context
name: {{ include "myapp.fullnameWithSuffix" (list . "worker") }}
Value Validation
Validate required values and fail fast:
{{- if not .Values.image.repository }}
{{- fail "image.repository is required" }}
{{- end }}
{{- if and .Values.persistence.enabled (not .Values.persistence.size) }}
{{- fail "persistence.size must be set when persistence is enabled" }}
{{- end }}
{{- if not (or (eq .Values.service.type "ClusterIP") (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) }}
{{- fail (printf "Invalid service.type: %s. Must be ClusterIP, NodePort, or LoadBalancer" .Values.service.type) }}
{{- end }}
# Kubernetes version validation
{{- if semverCompare "<1.24.0" .Capabilities.KubeVersion.Version }}
{{- fail "This chart requires Kubernetes 1.24 or higher" }}
{{- end }}
Multi-Value File Strategy
Organize values for different scenarios:
chart/
├── values.yaml # Base defaults
├── values-production.yaml # Production overrides
├── values-staging.yaml # Staging overrides
├── values-development.yaml # Development overrides
└── environments/
├── prod-us-west.yaml # Specific deployments
├── prod-eu-central.yaml
└── staging-dev-team.yaml
values.yaml (Base):
replicaCount: 2
image:
repository: company/myapp
tag: ""
pullPolicy: IfNotPresent
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
values-production.yaml:
replicaCount: 5
image:
pullPolicy: IfNotPresent
resources:
limits:
cpu: 2000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.company.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.company.com
monitoring:
enabled: true
serviceMonitor:
enabled: true
environments/prod-us-west.yaml:
region: us-west-2
nodeSelector:
topology.kubernetes.io/region: us-west-2
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: myapp
topologyKey: kubernetes.io/hostname
ingress:
hosts:
- host: api-us.company.com
Deploying with Multiple Value Files:
# Development
helm install myapp ./chart -f values-development.yaml
# Staging
helm install myapp ./chart -f values-staging.yaml
# Production with region-specific config
helm install myapp ./chart \
-f values-production.yaml \
-f environments/prod-us-west.yaml \
--namespace production
# Override specific values via command line
helm install myapp ./chart \
-f values-production.yaml \
--set image.tag=v2.1.0 \
--set replicaCount=10
Templating Best Practices
Use toYaml for Complex Structures:
# Bad - error-prone manual templating
resources:
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
# Good - clean and maintainable
resources:
{{- toYaml .Values.resources | nindent 2 }}
Fail Fast with Validation:
{{- if not .Values.required.databaseHost }}
{{- fail "Database host must be specified" }}
{{- end }}
Use Descriptive Helper Names:
# Bad
{{- define "h" -}}
...
{{- end }}
# Good
{{- define "myapp.databaseConnectionString" -}}
...
{{- end }}
Leverage Context Variables in Loops:
{{- $root := . -}}
{{- range .Values.services }}
name: {{ include "myapp.fullname" $root }}-{{ .name }}
{{- end }}
Comment Complex Logic:
{{- /*
Calculate replica count based on environment and autoscaling configuration.
If autoscaling is enabled, use minReplicas. Otherwise, use configured replicaCount.
*/ -}}
{{- $replicas := .Values.replicaCount -}}
{{- if .Values.autoscaling.enabled -}}
{{- $replicas = .Values.autoscaling.minReplicas -}}
{{- end -}}
replicas: {{ $replicas }}
Common Pitfalls
Improper Indentation: YAML is whitespace-sensitive. Always use nindent with the correct number:
# Wrong - will break YAML
labels:
{{ include "myapp.labels" . }}
# Correct
labels:
{{- include "myapp.labels" . | nindent 2 }}
Missing Whitespace Control: Forgetting - creates empty lines:
# Creates empty line
{{- if .Values.enabled }}
enabled: true
{{ end }}
# Clean output
{{- if .Values.enabled }}
enabled: true
{{- end }}
Using Template Instead of Include: Use include when piping:
# Wrong - cannot pipe template
labels:
{{ template "myapp.labels" . | nindent 2 }}
# Correct
labels:
{{- include "myapp.labels" . | nindent 2 }}
Losing Context in Loops: Remember to use $ or pass context:
# Wrong - .Release is undefined in loop
{{- range .Values.services }}
name: {{ .Release.Name }}-{{ .name }} # Error!
{{- end }}
# Correct
{{- $root := . -}}
{{- range .Values.services }}
name: {{ $root.Release.Name }}-{{ .name }}
{{- end }}
Not Quoting String Values: Some values must be quoted:
# Wrong - numeric strings may be misinterpreted
port: {{ .Values.port }} # "8080" becomes 8080
# Correct
port: {{ .Values.port | quote }} # "8080" stays "8080"
Key Takeaways
- Helm templates use Go templating syntax with actions
{{ }}and whitespace control{{- -}} - Values cascade from values.yaml to parent charts to value files to --set flags with increasing priority
- Built-in objects provide access to .Values, .Chart, .Release, .Capabilities, and .Files
- Conditional logic with
if,else,and,or, andnotcontrols what resources get rendered - Loop over lists and maps with
rangeto generate multiple resources from values - Template functions from Sprig library provide string manipulation, defaults, list operations, and more
- Named templates in _helpers.tpl promote code reuse and consistency across manifests
- Use
includeinstead oftemplatewhen piping output to functions likenindent - The
toYamlfunction cleanly renders complex nested structures maintaining proper formatting - Validate required values with conditional logic and
failfunction to catch configuration errors early - Organize multiple value files by environment (dev, staging, prod) and region for deployment flexibility
- Use
$or context variables to maintain access to root context within loops - The
nindentfunction handles YAML indentation correctly when including multi-line content - Quote string values that could be misinterpreted as numbers or booleans
- Comment complex templating logic to help maintainers understand intent and behavior