24.3 Sprig Functions in Helm Templates
Right, let’s talk about Sprig. This is where Helm templates go from being a simple placeholder replacement system to a genuinely powerful templating engine. Sprig is a library of over 100 template functions baked directly into Helm, and it’s our Swiss Army knife for manipulating strings, numbers, lists, dictionaries, and dates. Without it, we’d be writing a lot more Go code and stuffing it into tpl functions, which is a path I don’t recommend unless you enjoy pain.
Think of Sprig as your utility belt. You don’t need to use every single function, but knowing what’s in there is half the battle. The official docs are a solid reference, but they read like a phone book. I’m here to give you the guided tour of the bits you’ll actually use and the potholes you’ll definitely hit.
The Workhorses: String and List Manipulation
Most of your time will be spent making strings and lists do what you want. You’re not just inserting a value; you’re transforming it.
Need to make a value uppercase for a environment variable name? {{ .Values.appName | upper }}. Need to truncate a container image tag to a sane length? {{ .Values.image.tag | trunc 8 }}. These are the simple ones.
The real magic starts when you chain them together. Let’s say you need to create a standard name for a ConfigMap from your application name and a release revision. You might do something like this:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ printf "%s-config-%d" (.Values.appName | lower | trunc 20) .Release.Revision | trimSuffix "-" }}
Why the trimSuffix? Because if trunc cuts off in the middle of a word, it might leave a dangling hyphen. We’re being defensive. This is the kind of thinking that separates a working chart from a robust one.
Now, for lists. You’ll often get a list from Values and need to check if something is in it or join it all together with a comma.
env:
{{- if has "debug" .Values.featureFlags }}
- name: DEBUG_MODE
value: "true"
{{- end }}
And my personal favorite, join:
args:
- "--database-hosts={{ .Values.db.hosts | join "," }}"
This takes a list like ["db1", "db2"] and seamlessly turns it into the string "db1,db2". Incredibly useful.
The Dark Arts: Dictionaries and the with Keyword
Dictionaries (or dicts) are key-value maps. Sprig lets you create them (dict "key" "value"), merge them, and pluck values from them. This is powerful, but also where you can paint yourself into a corner.
A common pattern is to create a default set of labels and then merge in custom ones from your values. The merge function is your friend here:
metadata:
labels:
{{- $defaultLabels := dict "app.kubernetes.io/name" .Chart.Name "app.kubernetes.io/instance" .Release.Name }}
{{- $customLabels := .Values.labels | default dict }}
{{- $labels := merge $defaultLabels $customLabels }}
{{- toYaml $labels | nindent 4 }}
Notice we use | default dict to ensure $customLabels is always a dictionary, even if .Values.labels is empty. This prevents merge from throwing a fit. This is a critical best practice.
Now, let’s talk about with. It changes the context (the dot .) inside its block. It’s fantastic for scoping and avoiding repetitive typing, but it has one massive gotcha.
{{- with .Values.probe }}
livenessProbe:
httpGet:
path: {{ .path }}
port: {{ .port }}
initialDelaySeconds: {{ .delay }}
{{- end }}
Inside the with block, . is now .Values.probe. This is clean. The problem? You’ve lost access to the root context. You can’t get to .Release.Name in there anymore. To get it back, you have to store it in a variable outside the block first. This trips up everyone.
{{- $releaseName := .Release.Name }}
{{- with .Values.probe }}
livenessProbe:
httpGet:
path: {{ .path }}
# You can still use $releaseName here!
port: {{ .port }}
{{- end }}
The “Why Is This Failing?!” Pitfalls
Sprig functions are strict. {{ .Values.someString | trunc -5 }} will not quietly fail; it will cause a template rendering error and your helm install will blow up. This is actually a good thing—it fails fast and tells you exactly what’s wrong.
The more insidious pitfall is with empty or missing values. {{ has "debug" .Values.featureFlags }} is fine if featureFlags is a list. But if it’s null or an empty string? Kaboom. You must defensively check or provide defaults.
# This is safer
{{- $flags := .Values.featureFlags | default list }}
{{- if has "debug" $flags }}
Another classic: toYaml. It’s essential for dumping a complex object into your template (like a pod affinity rule), but it has no concept of indentation. You must pair it with nindent to get the YAML structure correct.
affinity:
{{- .Values.affinity | toYaml | nindent 8 }}
The nindent 8 function first adds a newline, then indents the following lines by 8 spaces. This is the incantation that makes it work. Don’t question it, just use it.
Sprig is what makes Helm charts programmable. Use it to be smart, to be defensive, and to keep your templates clean. But always remember: the more logic you cram into a template, the harder it is to debug. There’s a fine line between clever and too clever. Your goal isn’t to win a code golf championship; it’s to create a chart that works reliably at 3 AM when you’re half-awake.