Template Functions and Pipelines

Master Helm's template functions and pipelines. Transform values with built-in and Sprig functions for powerful chart logic.

8 min read

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 controlif/else, range, and with — the constructs that let your charts adapt to any configuration.