Templates, Values, Functions, and Pipelines

Helm charts are reusable Kubernetes packages. Templates, values, functions, and pipelines are the parts that make one chart render different, precise manifests for development, staging, and production without copying YAML by hand. The outcome of this lesson is practical: you should be able to read a chart, predict what helm template will emit, design a small values surface, and troubleshoot a render failure before it reaches the API server.

Purpose in a Helm Release

A Helm release is a named installation of a chart plus the values used to render it. The chart contains files under templates/, default configuration in values.yaml, and helper templates such as _helpers.tpl. During install or upgrade, Helm loads the chart, merges values from several sources, evaluates Go templates, sorts the resulting Kubernetes resources, and sends them to the cluster unless you are only rendering locally.

This matters in release engineering because a chart is both source code and deployment interface. The template author decides which Kubernetes fields are fixed by the platform team and which are configurable by service teams. Too little configurability forces chart forks. Too much configurability turns the chart into an unsafe YAML generator. Good Helm design keeps the values file small, typed by convention, and aligned with Kubernetes behavior.

How Rendering Works Internally

Helm uses Go’s text/template engine with extra objects and functions. The dot, written ., is the current scope. At the top level, dot exposes objects such as .Values, .Chart, .Release, .Capabilities, and .Files. When a template enters a range or with block, dot changes to the current item or selected object. The root object can be saved as $ when nested code still needs the original release context.

Values are merged before rendering. Chart defaults from values.yaml are the base. Parent chart values can override subchart values. User-supplied files passed with -f are applied in order, then command-line --set, --set-string, and related flags take precedence. Maps are merged by key, while lists are replaced rather than appended. That list behavior is a common source of surprises when users expect to add one environment variable without restating the whole list.

Functions transform data during rendering. Helm includes Sprig functions plus Helm-specific helpers. Examples include default for fallback values, quote for YAML strings, nindent for indentation, toYaml for serializing maps and lists, include for calling named templates, and required for making a value mandatory. A pipeline sends the result on the left into the function on the right, so .Values.image.tag | quote reads as “take the tag and quote it.”

Template Anatomy

A template file is mostly Kubernetes YAML with template actions inside {{ }}. Actions can print a value, call a function, define a variable, or control flow. Whitespace markers such as {{- and -}} trim adjacent spaces or newlines. They are useful, but aggressive trimming can accidentally join two YAML lines into invalid output.

Named templates are usually placed in templates/_helpers.tpl. Files beginning with an underscore are not rendered as Kubernetes manifests, but their definitions can be reused. A common helper builds stable names from the release and chart names, then resource templates call it with include. Prefer include over the older template action when you need to pipe the result into indentation or quoting functions.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "catalogue.fullname" . }}
  labels:
    app.kubernetes.io/name: {{ include "catalogue.name" . }}
spec:
  replicas: {{ .Values.replicaCount | default 1 }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "catalogue.name" . }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "catalogue.name" . }}
    spec:
      containers:
        - name: catalogue
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            - containerPort: {{ .Values.service.port }}

This fragment fixes the Kubernetes object shape while exposing replica count, image coordinates, and service port. If replicaCount is missing, Helm renders 1. If image.tag is missing, the chart application version is used. The deterministic output depends on the chart metadata and values passed into the render.

Example 1: Simple Value Substitution

The smallest useful template substitutes scalar values. Suppose values.yaml contains the following defaults.

replicaCount: 2
image:
  repository: ghcr.io/example/catalogue
  tag: "1.4.0"
service:
  port: 8080

Rendering the deployment fragment with these values produces a container image of ghcr.io/example/catalogue:1.4.0, replicas: 2, and containerPort: 8080. If an operator runs helm template catalogue ./catalogue --set replicaCount=3, only the replica count changes because the command-line override has higher precedence than values.yaml.

The design choice is whether replicaCount should be configurable at all. For a simple Deployment it is reasonable. For an autoscaled service, the chart should usually expose HorizontalPodAutoscaler settings instead and avoid letting users set conflicting replica counts.

Example 2: Maps, Indentation, and toYaml

Kubernetes fields such as labels, annotations, tolerations, and node selectors are naturally represented as maps or lists in values. Hand-writing loops for every field is noisy and easy to indent incorrectly. toYaml serializes the value, and nindent adds a newline plus spaces so the result lands under the correct parent key.

podAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8080"
nodeSelector:
  kubernetes.io/os: linux
template:
  metadata:
    annotations:
{{- with .Values.podAnnotations }}
{{ toYaml . | nindent 6 }}
{{- end }}
  spec:
    nodeSelector:
{{- toYaml .Values.nodeSelector | nindent 6 }}

The annotations render as two keys under metadata.annotations, indented six spaces. The with block changes dot to .Values.podAnnotations only when the value is non-empty; if the map is empty, the block emits nothing. The node selector is always rendered in this example, so an empty or missing value would produce null under nodeSelector. A more polished chart would wrap it in with too, avoiding a field Kubernetes may reject or interpret differently than intended.

Example 3: Reusable Helpers and Pipelines

Helpers make naming and common labels consistent across resources. They also reduce the risk that a Service selector stops matching a Deployment after a chart rename.

{{- define "catalogue.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "catalogue.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "catalogue.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

The first helper chooses nameOverride when set, otherwise the chart name. The pipeline then truncates the result to the DNS label length used by many Kubernetes names and removes a trailing dash if truncation cut the name there. The second helper prefixes the release name. With release prod, chart name catalogue, and no override, include "catalogue.fullname" . renders prod-catalogue. With nameOverride: api, it renders prod-api.

Design Choices and Trade-offs

Use values for differences between environments, not for arbitrary Kubernetes pass-through unless your users are platform engineers who need that control. A focused values file documents the supported deployment interface. A giant values file that mirrors every Pod field shifts Kubernetes API complexity onto every application team.

Prefer computed defaults when the computation is stable and obvious, such as names and labels. Prefer explicit values when a wrong default could create a security or availability problem, such as storage class, ingress host, or external secret name. Use required sparingly for values that truly cannot be inferred; too many required fields make local rendering and testing painful.

Keep types predictable. YAML turns unquoted values such as on, false, and numbers into booleans or integers. Quote strings that must remain strings, and use --set-string for command-line overrides such as image tags that could look numeric. When accepting structured values, render them with toYaml rather than concatenating fragments.

Failure Modes and Troubleshooting

Symptom: helm template fails with a message like nil pointer evaluating interface. Cause: the template accessed a nested value that does not exist, such as .Values.image.repository when image is missing. Diagnose: render with helm template --debug and inspect the referenced file and line. Correct: define the missing map in values.yaml, use required for mandatory values, or restructure the template with with and defaults.

Symptom: Kubernetes rejects the manifest with a YAML or schema error even though Helm rendered successfully. Cause: indentation, whitespace trimming, or a value type produced syntactically valid text that is not a valid Kubernetes object. Diagnose: run helm template ./chart > rendered.yaml, then validate with kubectl apply --dry-run=server -f rendered.yaml against a cluster. Correct: adjust nindent, guard optional fields with with, and quote string fields.

Symptom: an upgrade unexpectedly removes existing list entries, such as tolerations or environment variables. Cause: Helm value merging replaces lists instead of merging individual list items. Diagnose: compare helm get values RELEASE, the new override file, and helm template output. Correct: restate the full list in the higher-precedence values file or redesign the values shape as a map when keyed merging is important.

Security, Performance, and Reliability

Templates can leak secrets if they print sensitive values into annotations, labels, notes, or ConfigMaps. Store secret material in Kubernetes Secrets or an external secret mechanism, and be careful with helm template --debug output in logs. The tpl function evaluates a value as a template; use it only for trusted values because it gives values authors access to template functions and chart context.

Rendering performance is usually not the bottleneck for small charts, but excessive nested loops over large values can make CI and upgrades slower. Reliability concerns are more common: unstable names recreate resources, selector changes orphan Pods, and changing immutable fields makes upgrades fail. Helpers should produce stable names, and templates should keep selectors independent from user-editable labels.

Hands-on Lab: Render and Verify a Chart

Prerequisites: Helm installed locally, a shell, and access to a Kubernetes cluster if you want server-side validation. The lab does not require installing resources.

  1. Create or use a small chart named catalogue with helm create catalogue.
  2. Replace the generated Deployment values with replicaCount, image.repository, image.tag, and service.port like the first example.
  3. Add the helper templates for catalogue.name and catalogue.fullname.
  4. Render locally with helm template demo ./catalogue --set replicaCount=3.
  5. Verify that the Deployment name is demo-catalogue, the image uses the configured repository and tag, and the rendered replica count is 3.
  6. Run helm lint ./catalogue. If you have a cluster, also run a server dry run against the rendered output.
  7. Cleanup by deleting the temporary chart directory or reverting the files you changed. If you installed the release during experimentation, run helm uninstall demo.
helm template demo ./catalogue --set replicaCount=3
helm lint ./catalogue
helm template demo ./catalogue | kubectl apply --dry-run=server -f -

Expected behavior is deterministic for the local render: the manifests are printed to standard output, and the Deployment contains replicas: 3. helm lint should finish without chart structure or template errors. The server dry run succeeds only when the cluster supports the rendered API versions and the manifests satisfy admission rules.

Assessment Exercises

  1. A chart has tolerations defaults in values.yaml. A production values file supplies one toleration and the rendered Pod has only that one. Explain why and propose a values shape that would merge by key.
  2. Given {{ .Values.image.tag | default .Chart.AppVersion | quote }}, predict the output when image.tag is empty and appVersion is 2.1.0. Why is quoting useful here?
  3. A Service selector uses .Values.podLabels. What failure can occur if an operator changes those labels during upgrade, and how should the chart separate selectors from decorative labels?
  4. Rewrite a template that always emits nodeSelector: null so the field is omitted when no node selector is configured.
  5. Describe when required improves a chart and when it creates unnecessary friction for local testing.

Summary

Helm templates turn chart files and merged values into Kubernetes manifests. Values define the chart’s supported configuration surface, functions transform and validate data, and pipelines keep those transformations readable. The practical skill is not adding braces to YAML; it is designing stable names, predictable types, safe defaults, and tests that prove the rendered output is the Kubernetes object you intended to release.