Intermediate
Lesson 5 of 14
Values & Templating
Master how Helm merges values and renders Go templates — the foundation of chart customization.
🧒 Simple Explanation (ELI5)
Imagine a "Mad Libs" game. The template has sentences with blanks: "Deploy _____ replicas of _____ image." The values.yaml fills in those blanks: "3" and "nginx:1.25". Different players (environments) can fill in different answers using the same template.
🔧 How Values Work
Value Sources & Priority
Values come from multiple sources. Later sources override earlier ones:
values.yaml in the chart (lowest priority — defaults)
-f custom-values.yaml (overrides defaults)
- Additional
-f files (each overrides the previous)
--set key=value (highest priority)
# Priority demonstration
helm install app ./mychart \
-f base-values.yaml \ # overrides chart defaults
-f prod-values.yaml \ # overrides base-values
--set image.tag=v3.0 # overrides everything
Accessing Values in Templates
# values.yaml
image:
repository: nginx
tag: "1.25"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
config:
logLevel: info
features:
- auth
- cache
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
spec:
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: LOG_LEVEL
value: {{ .Values.config.logLevel | quote }}
--set Syntax for Nested & Complex Values
# Simple key
--set replicaCount=5
# Nested key (dot notation)
--set image.tag=v2.0
# String with special chars (use quotes)
--set env.DATABASE_URL="postgres://user:pass@host/db"
# Array/list (use curly braces)
--set "config.features={auth,cache,logging}"
# Set to null (remove key)
--set ingress.annotations=null
# JSON value
--set-json 'resources={"requests":{"cpu":"100m","memory":"64Mi"}}'
# COMMON GOTCHAS with --set:
# Comma in value? Escape it:
--set nodeSelector."kubernetes\.io/os"=linux
# Array item by index:
--set ingress.hosts[0].host=example.com
--set ingress.hosts[0].paths[0].path=/
# Set from file content:
--set-file tls.crt=./cert.pem
--set-file tls.key=./key.pem
❗
K8s Connection: Values Map to Kubernetes Resource Fields
Every value in values.yaml ultimately becomes a field in a Kubernetes resource. Understanding this mapping helps debug issues:
replicaCount → Deployment .spec.replicas
image.repository + image.tag → Container .spec.containers[].image
resources.requests.cpu → Container .spec.containers[].resources.requests.cpu
service.type → Service .spec.type (ClusterIP, NodePort, LoadBalancer)
ingress.enabled → Conditionally creates entire K8s Ingress resource
📊 Visual: Value Merging
Value Merge Order
values.yaml
(chart defaults)
← overrides ←
-f base.yaml
← overrides ←
-f prod.yaml
← overrides ←
--set key=val
(highest priority)
🔧 Go Template Basics
Actions (Delimiters)
# {{ }} — template action delimiters
# {{- }} — trim whitespace before
# {{ -}} — trim whitespace after
# {{- -}} — trim both sides
# Example: without trim
metadata:
name: {{ .Release.Name }}
# Output: " name: my-release" (extra whitespace before)
# With trim:
metadata:
name: {{- .Release.Name }}
# Output: " name:my-release" (too aggressive)
# Correct approach:
metadata:
name: {{ .Release.Name }} {{/* let YAML handle spacing */}}
Variables
# Assign a variable
{{- $fullName := include "mychart.fullname" . -}}
# Use it later
metadata:
name: {{ $fullName }}
labels:
app: {{ $fullName }}
Comments
{{/* This is a template comment — won't appear in output */}}
# This is a YAML comment — WILL appear in rendered output
⌨️ Hands-on
# Create a test chart
helm create valueslab
cd valueslab
# See default values
cat values.yaml
# Render with defaults
helm template test . | head -60
# Override values
helm template test . --set replicaCount=5 --set image.tag=latest
# Use a custom values file
cat > values-dev.yaml <<EOF
replicaCount: 1
image:
tag: "dev-latest"
service:
type: NodePort
port: 8080
EOF
helm template test . -f values-dev.yaml
# Compare outputs
diff <(helm template test .) <(helm template test . -f values-dev.yaml)
🐛 Debugging Scenarios
Scenario 1: Values not applied — "It's still using the old value"
# Check what values are actually in use
helm get values my-release --all
# Common causes:
# 1. Typo in key name (image.Tag vs image.tag)
# 2. Forgot to pass -f values file on upgrade
# 3. --set syntax wrong for nested keys
# Debug: Render locally and inspect
helm template test ./mychart -f my-values.yaml --set image.tag=v2 | grep "image:"
Scenario 2: Template produces invalid YAML
# Error: "YAML parse error"
# Debug: render and validate
helm template test ./mychart > rendered.yaml
# Open rendered.yaml and check indentation
# Usually caused by incorrect nindent value
# Wrong:
# labels:
# {{ include "mychart.labels" . }} ← no nindent!
# Right:
# labels:
# {{- include "mychart.labels" . | nindent 4 }}
🎯 Interview Questions
Beginner
The default configuration file for a Helm chart. It defines all configurable parameters (image, replicas, ports, etc.) with sensible defaults. Templates reference these values via {{ .Values.key }}. Users override defaults with -f custom.yaml or --set.
Two ways: 1) --set key=value for inline overrides: helm install app ./chart --set replicaCount=5. 2) -f values-file.yaml for file-based overrides: helm install app ./chart -f prod.yaml. You can combine both; --set has highest priority.
Helm uses Go's text/template package with Sprig library extensions. Actions are wrapped in {{ }}. Values are accessed via .Values, .Chart, .Release, etc. It supports conditionals, loops, variables, functions, and pipelines.
It resolves to the release name provided during helm install <name>. For example, helm install myapp ./chart makes .Release.Name = "myapp". Used in resource names to ensure uniqueness when the same chart is installed multiple times.
-f loads an entire values file (YAML). Good for many overrides and version-controlled configs. --set sets individual key-value pairs inline. Good for quick overrides. --set has higher priority than -f. Best practice: use -f for environment configs, --set for CI/CD-injected values (image tag, build number).
Intermediate
From lowest to highest priority: 1) Chart's values.yaml (defaults). 2) Parent chart's values for subcharts. 3) First -f file. 4) Subsequent -f files (each overrides previous). 5) --set / --set-string / --set-json (highest). Deep merge for objects; replace for scalars and arrays.
quote wraps a value in double quotes. Essential for values that YAML might misinterpret: booleans ("true" vs true), numbers ("80" vs 80), or strings with special chars. Example: value: {{ .Values.port | quote }} → value: "80".
nindent N adds a newline and indents the output by N spaces. Critical for multi-line template includes. Example: {{- include "mychart.labels" . | nindent 4 }} ensures labels are indented correctly under metadata:. Without it, YAML indentation breaks.
Create values.schema.json in the chart root. It defines required fields, types, ranges, patterns. Helm validates values against this schema during install/upgrade. Example: require replicaCount to be an integer ≥ 1, image.repository to be a non-empty string. Prevents misconfigurations.
{{- trims whitespace (including newlines) before the action. -}} trims after. {{- -}} trims both. Essential for clean YAML output — Go templates produce blank lines for if/range blocks that evaluate to nothing. Use trimming to eliminate those blank lines.
Scenario-Based
Check: 1) helm get values <release> — is the value stored? 2) helm get manifest <release> | grep image — what did the template produce? 3) Is there a typo? The template might use .Values.imageTag instead of .Values.image.tag. 4) Did you restart pods? ConfigMap changes may need a rollout restart. 5) helm template locally to verify rendering.
You're printing a Go map directly instead of converting it to YAML. Wrong: {{ .Values.resources.requests }}. Right: {{ toYaml .Values.resources.requests | nindent 12 }}. The toYaml function serializes the Go object into proper YAML format.
Provide sensible defaults so it works out of the box. Group values logically (image, service, ingress, resources). Use enabled flags for optional features: ingress.enabled: false, autoscaling.enabled: false. Document each value with comments in values.yaml. Add values.schema.json for validation. Provide example values files in a docs/ or examples/ directory.
You likely passed a partial values file that was missing some keys. By default, helm upgrade resets to chart defaults and applies only what you provide. If your values file only has image.tag, everything else reverts. Fix: either pass the complete values file, or use --reuse-values to keep previous values and apply your changes on top.
Never put secrets in values.yaml in Git. Options: 1) Pass via --set from CI/CD secrets: --set db.password=$DB_PASS. 2) Use helm-secrets plugin with SOPS encryption. 3) Use external secret operators (ExternalSecrets, Vault). 4) Reference existing Kubernetes secrets in templates with valueFrom.secretKeyRef. In the template, create a K8s Secret from the value.
🌍 Real-World Use Case
A SaaS platform uses one chart with multiple values files:
values-dev.yaml: 1 replica, debug logging, mock external services
values-staging.yaml: 2 replicas, info logging, real services, staging domain
values-prod.yaml: 5 replicas, warn logging, HA config, prod domain, HPA enabled
- CI/CD injects
--set image.tag=$GIT_SHA for each build — traceability from commit to deployment
🔐 Handling Secrets in Values
⚠️
Never Commit Secrets to Git
Putting database passwords or API keys in values.yaml and committing to Git is a security violation. Here are safe alternatives from least to most secure:
# Option 1: Inject from CI/CD secrets (simplest)
helm upgrade --install myapp ./chart \
-f values-prod.yaml \
--set database.password=$DB_PASSWORD \
--set api.key=$API_KEY
# $DB_PASSWORD comes from GitHub Secrets / Vault / AWS SSM
# Option 2: Reference existing K8s Secrets in your template
# In templates/deployment.yaml:
# env:
# - name: DB_PASSWORD
# valueFrom:
# secretKeyRef:
# name: myapp-db-credentials # Pre-created K8s Secret
# key: password
# values.yaml just references the secret name:
# existingSecret: myapp-db-credentials
# Option 3: Helm Secrets plugin + SOPS (encrypted values in Git)
# helm plugin install https://github.com/jkroepke/helm-secrets
# sops -e values-secrets.yaml > values-secrets.enc.yaml
# helm secrets upgrade myapp ./chart -f values-secrets.enc.yaml
# Option 4: External Secrets Operator (most production-ready)
# ExternalSecret CRD syncs from Vault/AWS Secrets Manager → K8s Secrets
# Your chart references the K8s Secret; the operator keeps it in sync
📝 values.schema.json — Validate Before Deploy
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "replicaCount"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"maximum": 20
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"resources": {
"type": "object",
"properties": {
"requests": { "type": "object" },
"limits": { "type": "object" }
}
}
}
}
# With schema in place, bad values fail BEFORE deploy:
helm install myapp ./chart --set replicaCount=-1
# Error: values don't meet the specifications of the schema:
# - replicaCount: Must be greater than or equal to 1
helm install myapp ./chart --set image.pullPolicy=Sometimes
# Error: image.pullPolicy must be one of: Always, IfNotPresent, Never
📝 Summary
- Values flow: chart defaults →
-f files → --set (highest priority)
- Templates use Go template syntax:
{{ .Values.key }}
- Use
quote, toYaml, nindent for proper YAML output
- Whitespace control:
{{- and -}} trim surrounding whitespace
- Always verify rendering with
helm template before install
← Back to Helm Course