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 task-api ./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 task-api ./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 task-api ./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 task-api ./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 task-api ./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.


Reflect on Your Skill​

You built a helm-chart skill in Lesson 0. Test and improve it based on what you learned.

Test Your Skill​

Using my helm-chart skill, create a _helpers.tpl file with reusable label templates.
Does my skill understand define/include patterns and nindent usage?

Identify Gaps​

Ask yourself:

  • Did my skill create templates with proper naming (chartname.templatename)?
  • Did it use include instead of deprecated template?
  • Does it handle indentation correctly with nindent?
  • Did it demonstrate passing context with the . parameter?

Improve Your Skill​

If you found gaps:

My helm-chart skill is missing [named templates / helper patterns / include syntax].
Update it to include:
- Naming convention: chartname.component (e.g., myapp.labels)
- Using include instead of template for composability
- Proper indentation with nindent function
- Context passing with `.` and root access with `$`
- Common helper patterns (labels, selectors, fullname)

Continue to Values Deep Dive →