Inside with and range, the dot (.) changes scope. To access root-level objects (.Release, .Chart), use $ prefix: $.Release.Name.
Template Functions & Pipelines
Built-in functions, the Sprig library, pipelines, conditionals, loops, and named templates — full control over chart rendering.
🧒 Simple Explanation (ELI5)
Template functions are like spreadsheet formulas. quote is like wrapping text in quotes. upper converts to UPPERCASE. default fills in a fallback. Pipelines chain them together like an assembly line: the output of one function feeds into the next.
🔧 Pipelines
Pipelines pass a value through a chain of functions using |:
# Pipeline: value → function1 → function2 → output
{{ .Values.env | upper | quote }}
# "info" → "INFO" → "\"INFO\""
# Multiple transforms
{{ .Values.name | default "myapp" | trunc 63 | trimSuffix "-" }}
# toYaml with indent (most common pipeline)
resources:
{{- toYaml .Values.resources | nindent 2 }}
🔧 Essential Functions
String Functions
{{ quote .Values.name }} # "myapp" — wraps in quotes
{{ .Values.name | upper }} # MYAPP
{{ .Values.name | lower }} # myapp
{{ .Values.name | title }} # Myapp
{{ trunc 63 .Values.name }} # Truncate to 63 chars (K8s name limit)
{{ trimSuffix "-" .Values.name }} # Remove trailing dash
{{ printf "%s-%s" .Release.Name .Chart.Name }} # String formatting
{{ replace "old" "new" .Values.text }} # String replace
{{ contains "prod" .Values.env }} # Returns true/false
Default & Required
# default — provide a fallback value
replicas: {{ .Values.replicaCount | default 1 }}
# Works for missing keys:
tag: {{ .Values.image.tag | default .Chart.AppVersion }}
# required — fail if value is empty/missing
image: {{ required "image.repository is required!" .Values.image.repository }}
# If not provided: Error: "image.repository is required!"
Type Conversion
{{ toYaml .Values.resources }} # Go object → YAML string
{{ toJson .Values.labels }} # Go object → JSON string
{{ .Values.port | int }} # Convert to integer
{{ .Values.enabled | toString }} # Convert to string
{{ b64enc .Values.password }} # Base64 encode
{{ b64dec .Values.encoded }} # Base64 decode
🔧 Conditionals (if/else)
# Basic if
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-svc
port:
number: {{ .Values.service.port }}
{{- end }}
# if/else
{{- if eq .Values.service.type "LoadBalancer" }}
# LoadBalancer-specific annotations
{{- else if eq .Values.service.type "NodePort" }}
# NodePort-specific config
{{- else }}
# Default ClusterIP
{{- end }}
# Falsy values: false, 0, nil, empty string "", empty list [], empty map {}
{{- if .Values.tolerations }}
tolerations:
{{- toYaml .Values.tolerations | nindent 2 }}
{{- end }}
🔧 Loops (range)
# values.yaml
env:
APP_ENV: production
LOG_LEVEL: info
WORKERS: "4"
ports:
- name: http
port: 80
- name: metrics
port: 9090
---
# template — iterate over a map
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
# template — iterate over a list
ports:
{{- range .Values.ports }}
- name: {{ .name }}
containerPort: {{ .port }}
{{- end }}
🔧 Named Templates (define/include)
# _helpers.tpl — define reusable templates
{{- define "mychart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "mychart.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
---
# Using in a template file:
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
# include vs template:
# "include" captures output as a string (can be piped)
# "template" outputs directly (cannot be piped)
# ALWAYS use "include" — it's more flexible
🔧 with — Change Scope
# values.yaml
database:
host: db.example.com
port: 5432
name: mydb
# Template with "with" — changes the dot scope
{{- with .Values.database }}
env:
- name: DB_HOST
value: {{ .host | quote }}
- name: DB_PORT
value: {{ .port | quote }}
- name: DB_NAME
value: {{ .name | quote }}
- name: RELEASE
value: {{ $.Release.Name | quote }} {{/* $ = root scope */}}
{{- end }}
⌨️ Hands-on
# Create a chart and experiment
helm create funclab
# Edit templates/configmap.yaml to practice functions:
cat > funclab/templates/configmap.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "funclab.fullname" . }}
labels:
{{- include "funclab.labels" . | nindent 4 }}
data:
app_name: {{ .Values.appName | default "myapp" | upper | quote }}
log_level: {{ .Values.logLevel | default "info" | quote }}
{{- range $key, $val := .Values.extraConfig }}
{{ $key }}: {{ $val | quote }}
{{- end }}
{{- if .Values.debug }}
debug_mode: "true"
{{- end }}
EOF
# Add values
cat > funclab/values-test.yaml <<EOF
appName: testapp
logLevel: debug
debug: true
extraConfig:
FEATURE_A: enabled
FEATURE_B: disabled
EOF
# Render and inspect
helm template test ./funclab -f funclab/values-test.yaml
🏭 Production Template Patterns
Pattern 1: Conditional Resource Creation
Create entire K8s resources only when needed — avoids deploying unused Ingress, HPA, or ServiceMonitor:
# templates/hpa.yaml — Only deploy HPA when autoscaling is enabled
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "myapp.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPU }}
{{- end }}
# values.yaml:
# autoscaling:
# enabled: false ← HPA not created by default
# minReplicas: 2
# maxReplicas: 10
# targetCPU: 80
Pattern 2: K8s-Compliant Labels (63-char limit)
# K8s label values must be ≤ 63 chars and match [a-zA-Z0-9._-]
# This is why helm create generates this in _helpers.tpl:
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
# For annotations (no length limit but good practice):
{{- define "myapp.annotations" -}}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end }}
Pattern 3: Optional Sidecar Container
# Add a log-shipper sidecar only when logging is enabled
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
{{- if .Values.resources }}
resources:
{{- toYaml .Values.resources | nindent 6 }}
{{- end }}
{{- if .Values.logging.sidecar.enabled }}
- name: log-shipper
image: "{{ .Values.logging.sidecar.image }}"
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
{{- end }}
trunc 63— K8s names/labels have a 63-character limit (DNS subdomain)trimSuffix "-"— K8s names can't end with a dashquote— Prevents YAML misinterpretation of K8s annotation valuestoYaml | nindent— Ensures valid K8s manifest indentationrequired— Catches missing values before K8s API rejects the manifest
🐛 Debugging Scenarios
Scenario: "nil pointer evaluating interface {}.host"
# This error means you're accessing a nested key that doesn't exist
# Example: {{ .Values.database.host }} but .Values.database is nil
# Fix 1: Use "with" to guard
{{- with .Values.database }}
host: {{ .host }}
{{- end }}
# Fix 2: Use default
host: {{ (.Values.database).host | default "localhost" }}
# Fix 3: Always define the key in values.yaml (even if empty)
database:
host: ""
🎯 Interview Questions
Beginner
A pipeline chains multiple functions with |. The output of the left side becomes the last argument of the right side. Example: {{ .Values.name | upper | quote }} — takes the value, converts to uppercase, wraps in quotes.
default provides a fallback value when the input is empty, nil, zero, or false. Example: {{ .Values.tag | default "latest" }}. If .Values.tag is not set, it returns "latest". Essential for making charts work with minimal configuration.
Use range: {{- range .Values.items }}. Inside the range, . refers to the current item. For maps: {{- range $key, $value := .Values.map }}. End with {{- end }}. Use $ to access root scope inside range.
include renders a named template and returns the result as a string. Unlike template, it can be piped: {{ include "mychart.labels" . | nindent 4 }}. Always prefer include over template for this reason.
toYaml converts a Go data structure (map, list) into a YAML string. Critical for rendering complex values like resource blocks: {{ toYaml .Values.resources | nindent 12 }}. Without it, Go prints the internal representation ("map[...]") instead of valid YAML.
Intermediate
required fails chart rendering with a custom error if the value is empty/missing. {{ required "image.repository is required" .Values.image.repository }}. Used to enforce mandatory values — prevents deploying with incomplete config. Combine with values.schema.json for comprehensive validation.
template outputs directly to the YAML — cannot be piped through functions. include captures output as a string — CAN be piped (e.g., | nindent 4). Since you almost always need to control indentation, include is the standard. template is legacy.
Sprig is a Go template function library that Helm includes. It provides 70+ functions beyond Go's built-ins: string manipulation (trim, replace, contains), math, date, crypto (sha256sum), lists (first, last, uniq), dicts (merge, keys), encoding (b64enc, toJson), regex, and more.
with re-scopes the dot (.) to the specified object. Inside {{- with .Values.database }}, .host means .Values.database.host. To access root scope (Release, Chart), use $: $.Release.Name. If the value is falsy, the with block is skipped entirely.
Use nindent N to add a newline + N spaces of indent. Use indent N to add N spaces without a newline. Use {{- and -}} to trim unwanted whitespace. Test indentation with helm template — invalid YAML indentation is the #1 template bug.
Scenario-Based
Replace {{ .Values.resources }} with {{ toYaml .Values.resources | nindent N }}. The toYaml function serializes the Go map into YAML format. N should match the required indentation level in the output YAML.
Add ingress.enabled: false in values.yaml. Wrap the entire ingress template in {{- if .Values.ingress.enabled }} ... {{- end }}. Users enable it with --set ingress.enabled=true or in their values file. The template only renders when enabled.
The parent key doesn't exist in values. Three fixes: 1) Define the key in values.yaml (even empty). 2) Guard with {{- with .Values.database }}. 3) Use Go's dig function from Sprig: {{ dig "database" "host" "default" .Values }} safely traverses nested keys.
Use range over the map: {{- range $key, $value := .Values.env }} / - name: {{ $key }} / value: {{ $value | quote }} / {{- end }}. Users add any env vars they want to the env: map without modifying templates.
Use required in the template: {{ required "resources.limits must be set" .Values.resources.limits }}. Also add a values.schema.json that marks resources.limits as required. Third layer: OPA/Kyverno cluster policies that reject pods without limits. Defense in depth.
🌍 Real-World Use Case: Multi-Tier Application
A healthcare platform uses template functions to enforce compliance across all K8s resources:
requiredonsecurityContext.runAsNonRoot— all containers must run as non-root (HIPAA compliance)if .Values.networkPolicy.enabled— NetworkPolicy is created in prod but not devrange .Values.env— Dynamic environment variables from a map (teams add their own without editing templates)include "base.labels"— Shared library chart provides standardized labels for all 50+ microservicesdefault .Chart.AppVersion .Values.image.tag— Image tag falls back to chart version, so deploys always have a tag
📝 Summary
- Pipelines chain functions:
{{ value | func1 | func2 }} - Essential functions:
default,required,quote,toYaml,nindent,include - if/else for conditional rendering; range for loops
- with changes dot scope; use
$for root access - Named templates in
_helpers.tpleliminate repetition