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:

  1. values.yaml in the chart (lowest priority — defaults)
  2. -f custom-values.yaml (overrides defaults)
  3. Additional -f files (each overrides the previous)
  4. --set key=value (highest priority)
bash
# 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

yaml
# values.yaml
image:
  repository: nginx
  tag: "1.25"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

config:
  logLevel: info
  features:
    - auth
    - cache
yaml
# 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

bash
# 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)

yaml
# {{ }} — 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

yaml
# Assign a variable
{{- $fullName := include "mychart.fullname" . -}}

# Use it later
metadata:
  name: {{ $fullName }}
  labels:
    app: {{ $fullName }}

Comments

yaml
{{/* This is a template comment — won't appear in output */}}

# This is a YAML comment — WILL appear in rendered output

⌨️ Hands-on

bash
# 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"

bash
# 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

bash
# 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

Q: What is values.yaml?

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.

Q: How do you override a value during install?

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.

Q: What template syntax does Helm use?

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.

Q: What does {{ .Release.Name }} resolve to?

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.

Q: What is the difference between --set and -f?

-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

Q: What is the value merge order?

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.

Q: What is the 'quote' function and when do you use it?

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".

Q: What does nindent do?

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.

Q: How do you validate values with a JSON Schema?

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.

Q: What is whitespace control in Go templates?

{{- 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

Q: You set image.tag=v2 with --set but the deployment still uses v1. What went wrong?

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.

Q: A template renders "map[cpu:100m memory:64Mi]" instead of proper YAML. What's wrong?

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.

Q: Your chart needs to work for both internal teams (simple values) and external users (complex customization). How do you design values.yaml?

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.

Q: After upgrade, some values reverted to defaults even though you passed a values file. What happened?

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.

Q: How do you handle sensitive values like database passwords?

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:

🔐 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:

bash
# 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

json
{
  "$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" }
      }
    }
  }
}
bash
# 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

← Back to Helm Course