Template Functions and Pipelines
Master Helm's template functions and pipelines. Transform values with built-in and Sprig functions for powerful chart logic.
Template Functions and Pipelines
In the previous tutorial, we learned Go templates basics and how to access values. But outputting raw values can only get you so far. What if you need to uppercase a string, set a default, or convert a map to YAML? That's where template functions come in.
Pipelines — The Unix of Templates
Before we dive into functions, let's understand pipelines. Just like Unix pipes, you chain operations with |:
# Unix: echo "hello" | tr '[:lower:]' '[:upper:]'
# Helm template: {{ "hello" | upper }}
# You can chain multiple functions
{{ .Values.hostname | lower | quote }}
# Result: "my-host.example.com"
Pipelines read left to right — the output of each function becomes the last argument of the next function. These two are equivalent:
# Pipeline style (preferred — reads naturally)
{{ .Values.name | default "app" | quote }}
# Nested function call style (same result)
{{ quote (default "app" .Values.name) }}
String Functions
Helm includes the Sprig library which gives you 70+ functions. Let's start with strings:
# Case conversion
{{ "hello world" | upper }} # HELLO WORLD
{{ "HELLO WORLD" | lower }} # hello world
{{ "hello world" | title }} # Hello World
{{ "hello world" | camelcase }} # helloWorld
{{ "hello world" | snakecase }} # hello_world
{{ "hello world" | kebabcase }} # hello-world
# Trimming
{{ " hello " | trim }} # hello
{{ "hello---" | trimSuffix "-" }} # hello-- (trims ONE suffix)
{{ "---hello" | trimPrefix "-" }} # --hello
# Quoting
{{ "hello" | quote }} # "hello"
{{ "hello" | squote }} # 'hello'
# Substring & length
{{ "hello world" | trunc 5 }} # hello
{{ "hello" | len }} # 5 (as a number, not rendered)
# Contains / HasPrefix / HasSuffix
{{ if contains "prod" .Release.Name }}...{{ end }}
{{ if hasPrefix "v" .Values.tag }}...{{ end }}
{{ if hasSuffix ".com" .Values.host }}...{{ end }}
# Replace
{{ "hello-world" | replace "-" "_" }} # hello_world
# Printf-style formatting
{{ printf "%s-%s" .Release.Name .Chart.Name }}
The Most Important String Functions
In practice, you'll use these constantly:
# default — provides fallback for empty/missing values
{{ .Values.hostname | default "localhost" }}
# quote — wraps in double quotes (essential for ConfigMaps)
{{ .Values.port | quote }}
# printf — string formatting
{{ printf "%s-config" .Release.Name }}
# trunc — truncate to N characters (K8s name limit is 63)
{{ .Release.Name | trunc 63 | trimSuffix "-" }}
Numeric Functions
# Math
{{ add 5 3 }} # 8
{{ sub 10 3 }} # 7
{{ mul 4 3 }} # 12
{{ div 10 3 }} # 3 (integer division)
{{ mod 10 3 }} # 1
{{ max 5 10 }} # 10
{{ min 5 10 }} # 5
# Rounding
{{ 3.7 | ceil }} # 4
{{ 3.7 | floor }} # 3
{{ 3.745 | round 2 }} # 3.75
# Type conversion
{{ "42" | atoi }} # 42 (string to int)
{{ 42 | toString }} # "42" (int to string)
{{ int64 42 }} # int64 value
{{ float64 42 }} # float64 value
Practical example — calculating resource requests as a fraction of limits:
resources:
limits:
cpu: {{ .Values.cpuLimit }}
memory: {{ .Values.memoryLimit }}
requests:
# Request 50% of the limit
cpu: {{ div (atoi (trimSuffix "m" .Values.cpuLimit)) 2 }}m
List Functions
# Create a list
{{ list "a" "b" "c" }}
# First and rest
{{ list "a" "b" "c" | first }} # a
{{ list "a" "b" "c" | rest }} # [b c]
{{ list "a" "b" "c" | last }} # c
# Append and prepend
{{ list "b" "c" | append "d" }} # [b c d]
{{ list "b" "c" | prepend "a" }} # [a b c]
# Check membership
{{ if has "admin" .Values.roles }}...{{ end }}
# Join into string
{{ list "a" "b" "c" | join "," }} # a,b,c
# Remove duplicates
{{ list "a" "b" "a" "c" | uniq }} # [a b c]
# Sort
{{ list "banana" "apple" "cherry" | sortAlpha }} # [apple banana cherry]
Practical example — building a comma-separated list of hostnames:
# values.yaml
# hosts:
# - app1.example.com
# - app2.example.com
# - app3.example.com
data:
ALLOWED_HOSTS: {{ .Values.hosts | join "," | quote }}
# Result: "app1.example.com,app2.example.com,app3.example.com"
Dictionary (Map) Functions
# Create a dict
{{ $myDict := dict "key1" "val1" "key2" "val2" }}
# Get a value
{{ get $myDict "key1" }} # val1
# Set a value (mutates the dict)
{{ $_ := set $myDict "key3" "val3" }}
# Check for key
{{ if hasKey $myDict "key1" }}...{{ end }}
# Merge dictionaries (later values win)
{{ $merged := merge $dict1 $dict2 }}
# Get all keys or values
{{ keys $myDict | sortAlpha }} # [key1 key2 key3]
{{ values $myDict }} # [val1 val2 val3]
YAML and JSON Functions
These are crucial for rendering complex values:
# Convert Go value to YAML string
{{ toYaml .Values.resources }}
# Convert Go value to JSON string
{{ toJson .Values.labels }}
# Parse YAML string to Go value
{{ fromYaml $yamlString }}
# Parse JSON string to Go value
{{ fromJson $jsonString }}
The toYaml + nindent combo deserves a closer look because you'll use it everywhere:
# values.yaml
# nodeSelector:
# disktype: ssd
# region: us-east-1
spec:
nodeSelector:
{{- toYaml .Values.nodeSelector | nindent 4 }}
# Result:
# spec:
# nodeSelector:
# disktype: ssd
# region: us-east-1
"Why not just use
{{ .Values.nodeSelector }}?"
Because Go would render the map as map[disktype:ssd region:us-east-1] — not valid YAML. You need toYaml to serialize it properly.
Encoding Functions
# Base64 (essential for Kubernetes Secrets)
{{ "my-secret" | b64enc }} # bXktc2VjcmV0
{{ "bXktc2VjcmV0" | b64dec }} # my-secret
# SHA256 hash
{{ "my-value" | sha256sum }} # long hex string
# Practical: Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-secret
type: Opaque
data:
password: {{ .Values.password | b64enc }}
Date Functions
# Current date
{{ now | date "2006-01-02" }} # 2024-03-15
{{ now | date "2006-01-02T15:04:05Z" }} # ISO 8601
# Duration
{{ now | dateModify "+2h" | date "15:04" }} # 2 hours from now
"Why 2006?"
Go uses a reference time (Mon Jan 2 15:04:05 MST 2006) for date formatting instead of YYYY-MM-DD patterns. Yes, it's weird. Just memorize: 2006-01-02 15:04:05.
Cryptographic Functions
Useful for generating random passwords and certificates:
# Random alphanumeric string
{{ randAlphaNum 16 }} # e.g., aB3kW9mN2pL5qR8x
# Generate a password if not provided
{{ .Values.password | default (randAlphaNum 24) }}
# UUID
{{ uuidv4 }} # e.g., 550e8400-e29b-41d4-a716-446655440000
Warning: Random functions generate new values on every
helm template/helm upgrade. If you use them for passwords, the password changes on every upgrade! For secrets, use a lookup to preserve existing values or manage them outside Helm.
The lookup Function
Query live resources from the Kubernetes cluster during rendering:
# Check if a Secret already exists
{{- $existing := lookup "v1" "Secret" .Release.Namespace "my-secret" -}}
{{- if $existing -}}
# Reuse existing password
password: {{ index $existing.data "password" }}
{{- else -}}
# Generate a new one
password: {{ randAlphaNum 24 | b64enc }}
{{- end }}
Note: lookup returns empty during helm template (no cluster connection). Always handle the empty case.
The tpl Function
Render a string as a template — useful when values themselves contain template expressions:
# values.yaml
# configTemplate: "{{ .Release.Name }}-database"
# Without tpl — rendered literally:
{{ .Values.configTemplate }}
# Result: {{ .Release.Name }}-database (not what we want!)
# With tpl — template is evaluated:
{{ tpl .Values.configTemplate . }}
# Result: my-release-database (correct!)
Commonly Used Patterns
Here's a cheat sheet of patterns you'll see in production charts:
# Safe name generation (handles overrides + length limit)
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
# Conditional annotations
metadata:
annotations:
{{- with .Values.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
# Image string with optional digest
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
# Required value (fails if not provided)
{{ required "A valid database host is required!" .Values.database.host }}
# Ternary
{{ ternary "yes" "no" .Values.enabled }}
What's Next?
Functions transform data — but you also need logic. In the next tutorial, we'll cover flow control — if/else, range, and with — the constructs that let your charts adapt to any configuration.