Skip to content
Published on

Helm Template Engine Deep Dive: Go Templates, Sprig, Named Templates

Authors

1. Go Template Internals

1.1 Parsing Stage

Helm's template engine is built on Go's text/template standard library. Template processing is divided into parsing and execution stages.

The parsing stage transforms template text into an AST (Abstract Syntax Tree):

Template text -> Lexer -> Token stream -> Parser -> AST

1.2 Execution Stage

The execution stage traverses the AST while applying the data context (dot .):

AST + Data context -> Rendered text output

The Pipeline concept is key:

# Pipeline: left output is passed as right input
name: { { .Values.name | lower | trunc 63 | trimSuffix "-" } }

# Execution order:
# 1. Evaluate .Values.name -> "My-Application-Name"
# 2. Apply lower -> "my-application-name"
# 3. Apply trunc 63 -> "my-application-name"
# 4. Apply trimSuffix "-" -> "my-application-name"

1.3 Whitespace Control

# Hyphens (-) remove whitespace
metadata:
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
    #^ removes left whitespace/newlines

    {{ include "my-chart.labels" . | nindent 4 -}}
    #                                           ^ removes right whitespace/newlines

    {{- include "my-chart.labels" . | nindent 4 -}}
    #^ removes both sides

2. Sprig Library Functions

2.1 String Functions

upper: {{ "hello" | upper }}          # HELLO
lower: {{ "HELLO" | lower }}          # hello
title: {{ "hello world" | title }}    # Hello World
trim: {{ "  hello  " | trim }}        # hello
substr: {{ substr 0 5 "hello world" }}# hello
trunc: {{ "hello" | trunc 3 }}        # hel
contains: {{ contains "lo" "hello" }} # true
replace: {{ "hello world" | replace " " "-" }}  # hello-world
snakecase: {{ "HelloWorld" | snakecase }}        # hello_world
camelcase: {{ "hello_world" | camelcase }}       # HelloWorld
kebabcase: {{ "HelloWorld" | kebabcase }}        # hello-world
regexMatch: {{ regexMatch "^[a-z]+$" "hello" }} # true
randAlphaNum: {{ randAlphaNum 10 }}    # 10-char random alphanumeric

2.2 Math Functions

add: { { add 1 2 } } # 3
sub: { { sub 10 3 } } # 7
mul: { { mul 2 3 } } # 6
div: { { div 10 3 } } # 3
mod: { { mod 10 3 } } # 1
max: { { max 1 2 3 } } # 3
min: { { min 1 2 3 } } # 1
ceil: { { ceil 1.1 } } # 2
floor: { { floor 1.9 } } # 1
round: { { round 1.5 0 } } # 2

2.3 Date Functions

now: { { now } }
date: { { now | date "2006-01-02" } }
dateModify: { { now | dateModify "+24h" | date "2006-01-02" } }

2.4 Crypto Functions

sha256sum: {{ "hello" | sha256sum }}
b64enc: {{ "hello" | b64enc }}          # aGVsbG8=
b64dec: {{ "aGVsbG8=" | b64dec }}       # hello
uuid: {{ uuidv4 }}

2.5 List Functions

list: { { list "a" "b" "c" } }
first: { { first (list "a" "b" "c") } } # a
last: { { last (list "a" "b" "c") } } # c
append: { { append (list "a" "b") "c" } } # [a b c]
has: { { has "b" (list "a" "b" "c") } } # true
without: { { without (list "a" "b" "c") "b" } } # [a c]
uniq: { { uniq (list "a" "b" "a") } } # [a b]
sortAlpha: { { sortAlpha (list "c" "a" "b") } } # [a b c]

2.6 Dictionary Functions

{{- $myDict := dict "key1" "value1" "key2" "value2" }}
get: {{ get $myDict "key1" }}              # value1
hasKey: {{ hasKey $myDict "key1" }}         # true
keys: {{ keys $myDict }}                    # [key1 key2]
values: {{ values $myDict }}                # [value1 value2]
{{- $merged := merge (dict "a" "1") (dict "b" "2") }}

3. Named Templates

3.1 define and template

# templates/_helpers.tpl
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "my-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

3.2 template vs include

# template: outputs directly (no pipeline)
{{ template "my-chart.name" . }}

# include: returns as string (pipeline available)
{{ include "my-chart.labels" . | nindent 4 }}

include is recommended because it supports post-processing via pipelines (indentation, etc.).

3.3 _helpers.tpl Convention

# Standard helper patterns
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

4. Flow Control

4.1 if/else

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-chart.fullname" . }}
{{- end }}

{{- if eq .Values.service.type "NodePort" }}
  nodePort: {{ .Values.service.nodePort }}
{{- else if eq .Values.service.type "LoadBalancer" }}
  loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

Falsy values: false, 0, empty string "", nil, empty collections

4.2 with

{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

# Access parent scope with $ inside with blocks
{{- with .Values.container }}
  name: {{ $.Chart.Name }}
  image: {{ .image }}
{{- end }}

4.3 range

# List iteration
{{- range .Values.extraEnvVars }}
- name: {{ .name }}
  value: {{ .value | quote }}
{{- end }}

# Map iteration
{{- range $key, $value := .Values.configData }}
{{ $key }}: {{ $value | quote }}
{{- end }}

# Fixed count
{{- range until 5 }}
- replica-{{ . }}
{{- end }}

5. Advanced Template Patterns

5.1 toYaml and nindent

spec:
  template:
    spec:
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

5.2 tpl Function

# values.yaml
configTemplate: |
  server.name={{ .Release.Name }}

# templates/configmap.yaml
data:
  config.properties: |
    {{ tpl .Values.configTemplate . | nindent 4 }}

5.3 lookup Function

{{- $secret := lookup "v1" "Secret" .Release.Namespace "my-secret" }}
{{- if $secret }}
password: {{ index $secret.data "password" }}
{{- else }}
password: {{ randAlphaNum 16 | b64enc }}
{{- end }}

Note: lookup always returns empty results with helm template.


6. Library Charts

Library charts (type: library) render no resources themselves, only provide named templates:

# Chart.yaml
apiVersion: v2
name: my-library
type: library
version: 1.0.0

Benefits: shared template logic, centralized label/annotation standards, DRY compliance.


7. Debugging

# Local template rendering
helm template my-release ./my-chart
helm template my-release ./my-chart -s templates/deployment.yaml
helm template my-release ./my-chart --debug

# Lint
helm lint ./my-chart --strict

8. Summary

The Helm template engine provides a powerful programming model beyond simple string substitution:

  1. Go template-based: Two-stage parsing-execution pipeline architecture
  2. Sprig library: Over 100 utility functions
  3. Named templates: Modular, reusable templates with define/include
  4. Flow control: Conditional and iterative rendering with if/with/range
  5. Library charts: Centralized management and sharing of common logic

The next post analyzes the complete Helm release lifecycle (install, upgrade, rollback).