Skip to main content

Named Templates and Helpers

In Lesson 1, you learned Go templating basics: variables, pipelines, conditionals, ranges. You can now write templates that branch on conditions and loop through data.

But you discovered a problem: You're repeating the same labels (app: myapp, version: 1.0, env: prod) in your Deployment, Service, and ConfigMap. That's not a templating problem—that's a code repetition problem. You need a way to define template fragments once and reuse them everywhere.

This lesson introduces named templates and the _helpers.tpl convention—the DRY principle for Helm charts. You'll define common patterns once (labels, selectors, image pull secrets, annotations) and include them wherever needed.


What You'll Learn

This lesson teaches you how to eliminate template duplication using Helm's named templates and helper conventions.

Concept GroupTopicsWhy It Matters
Template Reusedefine, end, includeStop repeating labels/selectors across manifests
Conventions_helpers.tpl, naming patternsFollow industry-standard organization
Output Controlinclude vs template, nindentPrevent broken YAML indentation
Scope Management. context, $ root referenceAccess values correctly in nested templates
Common PatternsLabels, selectors, image pull secretsApply real-world helper designs

Prerequisites:

  • Lesson 1 (Go templating: variables, pipelines, conditionals, ranges)
  • Understand YAML indentation rules
  • Familiarity with Kubernetes labels and selectors

Time Estimate: 45 minutes


1. Named Templates with define

In Lesson 1, every template you wrote was a full file that produced output. But templates can also define functions that output nothing on their own.

The define action creates a named template:

{{ define "chartname.labels" }}
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
environment: {{ .Values.environment }}
{{ end }}

Key points:

  • define starts a named template block
  • chartname.labels is the template's name (convention: chart-name.component)
  • Content between define and end is stored, not immediately output
  • The block produces no output when defined

Output:

When you define a template, nothing appears. The template is registered in memory, waiting to be called.


2. The _helpers.tpl File Convention

Helm has a naming convention: Templates starting with _ (underscore) are not rendered as standalone manifests. They're utilities meant to be included elsewhere.

Create templates/_helpers.tpl:

{{- define "myapp.labels" }}
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
{{- end }}

{{- define "myapp.selector" }}
app: {{ .Chart.Name }}
{{- end }}

{{- define "myapp.imagePullSecrets" }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end }}

Why _helpers.tpl?

  • Convention recognized by all Helm users (like _config.scss in CSS)
  • Prevents accidental rendering (underscore files are skipped)
  • Centralizes reusable fragments
  • One place to find all helpers

Output:

No files are created. _helpers.tpl is never rendered. It only provides functions for other templates to include.


3. Including Templates with include

Now you call a named template using include:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
{{ include "myapp.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{ include "myapp.selector" . | nindent 6 }}
template:
metadata:
labels:
{{ include "myapp.labels" . | nindent 8 }}
spec:
containers:
- name: app
image: myapp:{{ .Values.version }}

Syntax breakdown:

  • include "myapp.labels" . calls the named template, passing . (current context)
  • | nindent 4 pipes the output to nindent (indent and newline)
  • nindent N indents output by N spaces, preserving YAML structure

Output:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myapp
version: 1.0
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
version: 1.0
spec:
containers:
- name: app
image: myapp:1.0

4. Why include Replaces the Deprecated template

Helm originally provided template for calling named templates. template is now deprecated (removed in Helm 3.13+).

Why? The problem is output handling.

Problem with template:

metadata:
labels:
{{ template "myapp.labels" . }}

template doesn't support pipes. You can't control indentation. Output becomes:

metadata:
labels:
app: myapp
version: 1.0

Notice: No indentation. YAML is broken.

Solution with include:

metadata:
labels:
{{ include "myapp.labels" . | nindent 4 }}

include returns a string that you can pipe to nindent. Output:

metadata:
labels:
app: myapp
version: 1.0

Properly indented. Valid YAML.

Output comparison:

template (broken indentation):

metadata:
labels:
app: myapp

include (correct):

metadata:
labels:
app: myapp

Key difference:

  • template: Renders directly, no output control
  • include: Returns string, composable with pipes

Always use include. Never use template for new code.


Checkpoint: Template Definition and Inclusion

Quick Reference:

ActionSyntaxPurpose
Define template{{ define "name" }}...{{ end }}Create reusable template fragment
Include template{{ include "name" . }}Call template and return string
Control indentation{{ include "name" . | nindent 4 }}Pipe output to indent function
Store helpers_helpers.tplConvention for non-rendered templates

Self-Check Questions:

  1. What happens when you render a template file starting with _? Answer: Nothing—underscore files are not rendered as manifests.

  2. Why does {{ template "myapp.labels" . }} break YAML indentation? Answer: template renders directly without supporting pipes, so you can't control indentation.

  3. What does the . in {{ include "myapp.labels" . }} represent? Answer: The current context being passed to the named template (root context: .Chart, .Values, etc.).

  4. Where should you define all your chart's helper templates? Answer: In templates/_helpers.tpl following Helm convention.


5. Scope Rules: What . Means Inside a Named Template

When you call a named template, you pass . as the current context. Inside the template, . refers to whatever you passed.

{{- define "myapp.labels" }}
name: {{ .Chart.Name }} # Chart name from passed context
env: {{ .Values.environment }} # Values from passed context
{{- end }}

When you call:

{{ include "myapp.labels" . | nindent 4 }}

Inside myapp.labels, . is the root context (.Chart, .Values, etc. all work).

Scope rule: Inside a named template, . refers to whatever you passed to include/template.

Common mistake:

{{- define "myapp.labels" }}
{{- range .Values.tags }}
tag: {{ . }} # . is now the tag value (a string), not the root context
{{- end }}
{{- end }}

Inside the range, . becomes each tag string. You can't access .Chart anymore.

To access both root context and loop value:

{{- define "myapp.labels" }}
{{- range .Values.tags }}
tag: {{ . }}
chart: {{ $.Chart.Name }} # $ always refers to root context
{{- end }}
{{- end }}

$ is a special variable: Always refers to root context, no matter how deep you nest.


6. Common Patterns

Pattern: Labels (Most Common)

{{- define "myapp.labels" }}
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
managed-by: {{ .Release.Service }}
release: {{ .Release.Name }}
{{- end }}

Used in every Kubernetes manifest.

Pattern: Selectors

{{- define "myapp.selector" }}
app: {{ .Chart.Name }}
{{- end }}

Used in Deployments, Services, StatefulSets.

Pattern: Image Pull Secrets (Conditional)

{{- define "myapp.imagePullSecrets" }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end }}

Only rendered if imagePullSecrets is defined.

Pattern: Annotations (with defaults)

{{- define "myapp.annotations" }}
annotations:
description: {{ .Values.podAnnotations.description | default "AI Service" }}
{{- if .Values.podAnnotations.custom }}
custom: {{ .Values.podAnnotations.custom }}
{{- end }}
{{- end }}

Combines defaults with conditional fields.


7. Naming Conventions

The standard Helm convention is: chartname.component

Examples from real charts:

  • postgresql.primary.fullname
  • redis.fullname
  • nginx.ingress.className

Your chart: myapp.labels, myapp.selector, myapp.imagePullSecrets

Why this pattern?

  1. Namespacing: Prevents collisions with subchart helpers
  2. Clarity: myapp.labels immediately shows purpose
  3. Searchability: grep "myapp\." _helpers.tpl finds all your helpers
  4. Consistency: Everyone following convention means predictable code

Checkpoint: Scope and Naming Conventions

Quick Reference:

Scope ElementWhat It Refers ToExample
.Current context (what you passed to include){{ include "myapp.labels" . }}. is root context
$Always root context, even in nested loops{{ $.Chart.Name }} inside range
.Chart, .ValuesAccessible when root context is passedOnly work if you passed . to include

Self-Check Questions:

  1. Inside a {{ range .Values.tags }} loop within a named template, what does . refer to? Answer: Each individual tag value (the current iteration item), not the root context.

  2. How do you access .Chart.Name inside a range loop in a named template? Answer: Use {{ $.Chart.Name }} where $ always refers to the root context.

  3. What naming convention should you follow for helper templates? Answer: chartname.component (e.g., myapp.labels, postgresql.fullname).

  4. Why prefix helper names with the chart name? Answer: Prevents namespace collisions with subchart helpers and improves searchability.


Common Mistakes

Before you start the exercises, avoid these frequent errors:

Mistake 1: Forgetting to Pass Context (.)

Wrong:

{{ include "myapp.labels" }}

Why it fails: The template can't access .Chart, .Values, etc. without context.

Correct:

{{ include "myapp.labels" . }}

Mistake 2: Using Wrong Indentation Level

Wrong:

metadata:
labels:
{{ include "myapp.labels" . | nindent 2 }}

Why it fails: Labels are already at 4-space indent (under metadata:), but nindent 2 only indents 2 spaces.

Correct:

metadata:
labels:
{{ include "myapp.labels" . | nindent 4 }}

Rule: Count the spaces from the left margin to where the first key should appear.


Mistake 3: Using template Instead of include

Wrong:

{{ template "myapp.labels" . }}

Why it fails: Can't pipe to nindent, breaks YAML indentation.

Correct:

{{ include "myapp.labels" . | nindent 4 }}

Mistake 4: Creating Helpers Without the Chart Name Prefix

Wrong:

{{- define "labels" }}

Why it fails: If you use a subchart that also defines labels, they collide.

Correct:

{{- define "myapp.labels" }}

Mistake 5: Losing Root Context in Nested Loops

Wrong:

{{- define "myapp.labels" }}
{{- range .Values.tags }}
tag: {{ . }}
chart: {{ .Chart.Name }} # .Chart doesn't exist here—. is now the tag string
{{- end }}
{{- end }}

Correct:

{{- define "myapp.labels" }}
{{- range .Values.tags }}
tag: {{ . }}
chart: {{ $.Chart.Name }} # $ always refers to root context
{{- end }}
{{- end }}

Exercises

Exercise 2.1: Create _helpers.tpl with Common Labels

Create templates/_helpers.tpl:

{{- define "aiagent.labels" }}
app: {{ .Chart.Name }}
version: {{ .Chart.AppVersion }}
environment: {{ .Values.environment | default "development" }}
managed-by: {{ .Release.Service }}
{{- end }}

Verify it's not rendered:

helm template my-agent ./chart

Output:

No YAML with app: aiagent appears. The template is defined but not rendered.


Exercise 2.2: Include Labels in Multiple Manifests

Create templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
labels:
{{ include "aiagent.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
{{ include "aiagent.labels" . | nindent 8 }}
spec:
containers:
- name: app
image: myapp:{{ .Values.version }}

Create templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-service
labels:
{{ include "aiagent.labels" . | nindent 4 }}
spec:
selector:
app: {{ .Chart.Name }}
ports:
- port: 80
targetPort: 8000

Create templates/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
labels:
{{ include "aiagent.labels" . | nindent 4 }}
data:
app.env: {{ .Values.environment }}

Render:

helm template my-agent ./chart

Output:

Three manifests (Deployment, Service, ConfigMap) all include the same labels from your helper.


Exercise 2.3: Create Image Pull Secrets Helper with Conditional Logic

Update templates/_helpers.tpl:

{{- define "aiagent.imagePullSecrets" }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- else }}
# No image pull secrets configured
{{- end }}
{{- end }}

Update values.yaml:

environment: production
version: 1.0
imagePullSecrets:
- dockerhub-secret
- gcr-secret

Use in templates/deployment.yaml:

spec:
template:
spec:
{{ include "aiagent.imagePullSecrets" . | nindent 6 }}
containers:
- name: app

Render:

helm template my-agent ./chart

Output:

spec:
template:
spec:
imagePullSecrets:
- name: dockerhub-secret
- name: gcr-secret
containers:
- name: app

Change values.yaml to imagePullSecrets: [] and render again:

Output:

spec:
template:
spec:
containers:
- name: app

The imagePullSecrets section is omitted.


Exercise 2.4: Demonstrate Why include Works Where template Fails

Create a test file using old template syntax:

apiVersion: v1
kind: ConfigMap
metadata:
labels:
{{ template "aiagent.labels" . }}

Render:

helm template my-agent ./chart

Output (BROKEN):

apiVersion: v1
kind: ConfigMap
metadata:
labels:
app: aiagent
version: 1.0
environment: production
managed-by: Helm

YAML is invalid. Labels aren't indented under labels:.

Now use include:

apiVersion: v1
kind: ConfigMap
metadata:
labels:
{{ include "aiagent.labels" . | nindent 4 }}

Output (CORRECT):

apiVersion: v1
kind: ConfigMap
metadata:
labels:
app: aiagent
version: 1.0
environment: production
managed-by: Helm

Valid YAML, proper indentation.


Exercise 2.5: Refactor 3 Duplicate Sections into Single Include

You have three manifests with duplicate metadata.labels:

Before:

# deployment.yaml
metadata:
labels:
app: myapp
version: 1.0
env: prod

# service.yaml
metadata:
labels:
app: myapp
version: 1.0
env: prod

# configmap.yaml
metadata:
labels:
app: myapp
version: 1.0
env: prod

After:

Create helper in _helpers.tpl:

{{- define "myapp.labels" }}
app: myapp
version: 1.0
env: prod
{{- end }}

Use in all three:

metadata:
labels:
{{ include "myapp.labels" . | nindent 4 }}

Benefit: Change labels once in _helpers.tpl. All three manifests automatically use the new labels.


Try With AI

Setup

You've created a chart with separate Deployment, Service, and ConfigMap. Each has duplicate labels, annotations, and image pull secret handling. Your job is to refactor common patterns into _helpers.tpl and simplify your manifests.

Files to work with:

  • templates/_helpers.tpl (create this)
  • templates/deployment.yaml
  • templates/service.yaml
  • templates/configmap.yaml
  • values.yaml

Part 1: Initial State (Current Duplications)

Show AI your current manifests:

I have three manifests with repeated labels and selectors:

[paste your current deployment.yaml, service.yaml, configmap.yaml]

What patterns would you identify as candidates for helpers?

Part 2: Helper Design

Based on AI's suggestions, ask:

I want to create these helpers in _helpers.tpl:
1. labels (for metadata.labels in all manifests)
2. selector (for spec.selector.matchLabels in Deployment/Service)
3. imagePullSecrets (conditional, only if Values.imagePullSecrets exists)

For each helper, show me the define/end block and how I should call it with include and nindent.

Part 3: Validation

Ask AI to help you validate:

After I add these helpers and refactor my manifests to use include,
how do I verify with `helm template` that:
1. Labels appear correctly indented in all manifests?
2. Selectors are only in Deployment and Service, not ConfigMap?
3. ImagePullSecrets appear when configured, disappear when empty?

Part 4: Refinement

Show AI your refactored manifests:

Here's my refactored deployment.yaml using include:

[paste your refactored file]

Are there any places where I should add nindent, or any indentation issues?

Part 5: Final Check

Verify your work:

helm template my-agent ./chart > rendered.yaml
cat rendered.yaml

Compare the output:

  • Do all labels appear with correct indentation?
  • Do selectors only appear where needed?
  • Are image pull secrets included/excluded based on values.yaml?

Check your helpers are used everywhere they should be:

grep -r "include \"myapp\." templates/

Should show calls in deployment, service, configmap—not in _helpers.tpl definitions.