Intermediate Lesson 6 of 14

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 |:

yaml
# 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

yaml
{{ 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

yaml
# 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

yaml
{{ 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)

yaml
# 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)

yaml
# 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)

yaml
# _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

yaml
# 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 }}
⚠️
$ inside with/range

Inside with and range, the dot (.) changes scope. To access root-level objects (.Release, .Chart), use $ prefix: $.Release.Name.

⌨️ Hands-on

bash
# 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:

yaml
# 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)

yaml
# 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

yaml
# 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 }}
💡
K8s Connection: Template Functions → K8s API Requirements
  • trunc 63 — K8s names/labels have a 63-character limit (DNS subdomain)
  • trimSuffix "-" — K8s names can't end with a dash
  • quote — Prevents YAML misinterpretation of K8s annotation values
  • toYaml | nindent — Ensures valid K8s manifest indentation
  • required — Catches missing values before K8s API rejects the manifest

🐛 Debugging Scenarios

Scenario: "nil pointer evaluating interface {}.host"

bash
# 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

Q: What is a pipeline in Helm templates?

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.

Q: What does the 'default' function do?

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.

Q: How do you iterate over a list in Helm?

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.

Q: What is the 'include' function?

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.

Q: What does toYaml do?

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

Q: What is the 'required' function?

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.

Q: Explain the difference between include and template.

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.

Q: What is the Sprig library?

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.

Q: How does 'with' change the scope?

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.

Q: How do you handle indentation in Helm templates?

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

Q: Your template renders "map[cpu:100m memory:64Mi]" instead of proper YAML. Fix it.

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.

Q: Your chart should optionally create an Ingress resource. How?

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.

Q: You get "nil pointer evaluating interface {}.host". How do you fix it?

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.

Q: You need to generate dynamic environment variables from a map in values.yaml. How?

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.

Q: A team wants to enforce that every deployment must have resource limits. How?

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:

📝 Summary

← Back to Helm Course