Lab Guide
Explore the default _helpers.tpl, write custom helpers for labels and image references, and refactor duplicated YAML into named templates.
Prerequisites
- Helm 3.12+ installed
- A running Kubernetes cluster (minikube, kind, or similar)
kubectlconfigured and pointing at the cluster
Task 1: Explore Default _helpers.tpl
Step 1 — Scaffold a chart
helm create myapp
cd myapp
Step 2 — Read the generated helpers
Open the helpers file and study each named template:
cat templates/_helpers.tpl
You should see six define blocks. Identify each one:
myapp.name— chart name with optional override, truncated to 63 charsmyapp.fullname— release + chart name combination for unique resource namesmyapp.chart— name-version string for thehelm.sh/chartlabelmyapp.labels— full recommended label setmyapp.selectorLabels— minimal immutable labels for selectorsmyapp.serviceAccountName— conditional ServiceAccount name
Step 3 — See how helpers are invoked
Check where include is used across the templates:
grep -n 'include' templates/*.yaml
Notice that every template file references helpers via include (not template), and most pipe the result into nindent for proper YAML indentation.
Step 4 — Render and trace
Render the chart and observe the helpers in action:
helm template testrelease . --set nameOverride=webapp
Search the output for the generated names and labels:
helm template testrelease . --set nameOverride=webapp | grep "app.kubernetes.io"
You should see labels like app.kubernetes.io/name: webapp and app.kubernetes.io/instance: testrelease consistently applied across Deployment, Service, and ServiceAccount resources.
Step 5 — Test fullnameOverride
helm template testrelease . --set fullnameOverride=my-custom-name | grep "name:"
All resource names should use my-custom-name instead of the default testrelease-myapp pattern.
✓ Check: You can identify all six default helpers and trace their usage across the rendered output.
Task 2: Write Custom Helpers
Step 1 — Add an image reference helper
Image references typically combine a registry, repository, and tag. Instead of repeating this logic in every container spec, create a helper. Add the following to the bottom of templates/_helpers.tpl:
{{/*
Build a fully qualified image reference.
Usage: {{ include "myapp.image" (dict "image" .Values.image "root" .) }}
*/}}
{{- define "myapp.image" -}}
{{- $registry := .image.registry | default "" -}}
{{- $repository := .image.repository -}}
{{- $tag := .image.tag | default .root.Chart.AppVersion -}}
{{- if $registry -}}
{{- printf "%s/%s:%s" $registry $repository $tag -}}
{{- else -}}
{{- printf "%s:%s" $repository $tag -}}
{{- end -}}
{{- end }}
Step 2 — Update values.yaml
Modify the image section in values.yaml to include a registry field:
image:
registry: ""
repository: nginx
pullPolicy: IfNotPresent
tag: ""
Step 3 — Use the helper in the Deployment
Edit templates/deployment.yaml and replace the existing image line. Find the line that reads:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
Replace it with:
image: "{{ include "myapp.image" (dict "image" .Values.image "root" .) }}"
Step 4 — Test with different configurations
# Default (no registry, uses appVersion as tag)
helm template testrelease . | grep "image:"
# With explicit tag
helm template testrelease . --set image.tag=1.25-alpine | grep "image:"
# With private registry
helm template testrelease . \
--set image.registry=registry.example.com \
--set image.tag=v2.0.0 | grep "image:"
Expected output for each:
image: "nginx:1.16.0" (default appVersion)
image: "nginx:1.25-alpine" (explicit tag)
image: "registry.example.com/nginx:v2.0.0" (with registry)
Step 5 — Add a common-labels helper
Sometimes you need extra labels beyond the standard set. Add a helper that merges standard labels with custom ones from values:
{{/*
Common labels: standard labels + any user-defined extra labels.
*/}}
{{- define "myapp.commonLabels" -}}
{{- include "myapp.labels" . }}
{{- with .Values.commonLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
Step 6 — Test the common-labels helper
Replace include "myapp.labels" . with include "myapp.commonLabels" . in the Deployment metadata. Then render:
helm template testrelease . \
--set commonLabels.team=platform \
--set commonLabels.cost-center=eng-42 | grep -A 10 "labels:"
✓ Check: You should see the standard Helm labels plus team: platform and cost-center: eng-42.
Task 3: Refactor Duplicated YAML into Named Templates
Step 1 — Create a second workload
Create a new template templates/worker-deployment.yaml for a background worker that reuses the same image, labels, and configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}-worker
labels:
{{- include "myapp.commonLabels" . | nindent 4 }}
app.kubernetes.io/component: worker
spec:
replicas: {{ .Values.worker.replicaCount | default 1 }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: worker
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: worker
spec:
containers:
- name: worker
image: "{{ include "myapp.image" (dict "image" .Values.image "root" .) }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["nginx", "-g", "daemon off;"]
resources:
{{- toYaml .Values.resources | nindent 12 }}
Step 2 — Identify the duplication
Compare the two Deployment files. The resources, securityContext, and container probe blocks are likely identical. Let us extract the resource limits into a helper.
Step 3 — Create a resource-limits helper
Add to templates/_helpers.tpl:
{{/*
Default resource limits. Pass a dict with "resources" key.
Falls back to chart-level .Values.resources if not provided.
*/}}
{{- define "myapp.resources" -}}
{{- $res := .resources | default dict -}}
{{- if $res }}
{{- toYaml $res }}
{{- end }}
{{- end }}
Step 4 — Create a pod-security helper
Extract the common security context:
{{/*
Standard security context for all containers.
*/}}
{{- define "myapp.securityContext" -}}
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
{{- end }}
Step 5 — Use the helpers in both Deployments
In both templates/deployment.yaml and templates/worker-deployment.yaml, replace inline resource and security blocks with the helpers:
{{- include "myapp.securityContext" . | nindent 10 }}
resources:
{{- include "myapp.resources" (dict "resources" .Values.resources) | nindent 12 }}
Step 6 — Add worker values and render
Add worker configuration to values.yaml:
worker:
replicaCount: 2
Render the full chart and verify both Deployments share the same helpers:
helm template testrelease . | grep -B2 -A6 "securityContext"
Both the main Deployment and the worker Deployment should show identical securityContext blocks produced by the myapp.securityContext helper.
Step 7 — Verify with helm lint
helm lint .
Ensure no errors or warnings are reported. If there are warnings about missing values, add sensible defaults in values.yaml.
Step 8 — Install and confirm
helm install lab13 . --namespace lab13 --create-namespace
kubectl get deployments -n lab13
✓ Check: You should see two Deployments: lab13-myapp and lab13-myapp-worker.
Cleanup
helm uninstall lab13 --namespace lab13
kubectl delete namespace lab13
cd .. && rm -rf myapp
Summary
- You explored the six default helpers generated by
helm createand traced how they are invoked withincludeandnindent. - You wrote custom helpers for image references and common labels, passing data through dicts and the root scope (
$). - You refactored duplicated YAML (resources, security contexts) into named templates, applying DRY principles across multiple Deployments.
- You confirmed that
include(nottemplate) is required to pipe helper output intonindentandtoYaml.