17 min to read
Understanding Helm Chart Template Syntax
A comprehensive guide to Helm Chart templating and best practices

Overview
Helm is the package manager for Kubernetes, enabling easy deployment and management of applications. At its core are Helm Charts - packages of pre-configured Kubernetes resources that can be customized through templating.
This guide explores Helm Chart template syntax, from basic concepts to advanced techniques, helping you create flexible and maintainable charts for your Kubernetes applications.
Helm Chart templates are files that combine static YAML with dynamic templating directives to generate valid Kubernetes manifests. They allow charts to be customizable and reusable across different environments and configurations.
Key benefits of Helm templating include:
- Environment-specific configuration without changing chart files
- Dynamic generation of resource names, labels, and other metadata
- Conditionally including resources based on configuration values
- Reusing common patterns across multiple resources
- Parameterizing deployments for different scenarios
Helm Chart Structure
Before diving into templating, it’s important to understand the basic structure of a Helm chart:
mychart/
├── Chart.yaml # Metadata about the chart
├── values.yaml # Default configuration values
├── templates/ # Template files
│ ├── deployment.yaml # Kubernetes resources
│ ├── service.yaml
│ ├── _helpers.tpl # Template helpers
│ └── ...
├── charts/ # Dependencies (subchart)
└── README.md # Documentation
Template Language Fundamentals
Helm uses Go templates as its templating language, which is extended with additional functions specific to Helm.
Basic Syntax
Templates use double curly braces ({{ }}
) to enclose template directives:
# Empty curly braces (not useful, but valid syntax)
{{ }}
# Basic template directive
{{ .Release.Name }}
# Placeholder pattern
{{ ... }}
# Access release information
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
myvalue: {{ .Values.name }}
version: {{ .Chart.Name }}-{{ .Chart.Version }}
Helm templates can control whitespace with special syntax:
{{- ... }}
: Removes whitespace before the directive{{ ... -}}
: Removes whitespace after the directive{{- ... -}}
: Removes whitespace on both sides
This helps produce clean YAML without unnecessary blank lines or spaces.
Built-in Objects
Helm provides several built-in objects for access to chart data and runtime information:
Object | Description | Example Usage |
---|---|---|
.Values |
Values from the values.yaml file and user-supplied values | {{ .Values.replicaCount }} |
.Release |
Information about the release (name, namespace, etc.) | {{ .Release.Name }}-database |
.Chart |
Contents of the Chart.yaml file | {{ .Chart.Version }} |
.Files |
Access to files in the chart | {{ .Files.Get "config.json" }} |
.Template |
Information about the current template | {{ .Template.Name }} |
.Capabilities |
Information about Kubernetes capabilities | {{ .Capabilities.KubeVersion }} |
Working with Values
Values can be defined in various places:
- The chart’s
values.yaml
file (defaults) - Parent chart’s
values.yaml
(for subcharts) - Command-line with
--set
or-f
options
Accessing Values
Values are accessed through the .Values
object:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-deployment
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
Default Values
You can provide defaults for values that might not be defined:
# Using default function
replicas: {{ .Values.replicaCount | default 1 }}
# Using if/else
replicas: {{ if .Values.replicaCount }}{{ .Values.replicaCount }}{{ else }}1{{ end }}
Control Structures
Conditionals
Conditionals allow you to include or exclude blocks of YAML based on values:
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-service
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
{{- if .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
Complex Conditionals
{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ .Release.Name }}-metrics
{{- if .Values.metrics.serviceMonitor.namespace }}
namespace: {{ .Values.metrics.serviceMonitor.namespace }}
{{- end }}
spec:
endpoints:
- port: metrics
interval: {{ .Values.metrics.serviceMonitor.interval }}
path: /metrics
{{- end }}
Loops with Range
The range
function iterates over lists or maps:
# Iterating over a list
spec:
containers:
{{- range .Values.containers }}
- name: {{ .name }}
image: {{ .image }}
ports:
{{- range .ports }}
- containerPort: {{ . }}
{{- end }}
{{- end }}
# Iterating over a map
env:
{{- range $key, $val := .Values.env }}
- name: {{ $key | upper }}
value: {{ $val | quote }}
{{- end }}
- Use meaningful variable names with
range $name, $value := .Values.something
- Be careful with indentation - always test your templates
- Remember to use
and
to control whitespace
- When iterating over empty collections, nothing will be rendered
Template Functions
Helm provides a rich set of functions for transforming and manipulating values.
String Functions
# Quote a string
annotations:
app.kubernetes.io/name: {{ .Release.Name | quote }}
# Convert to upper case
label: {{ .Values.label | upper }}
# Truncate a string
name: {{ .Values.serviceName | trunc 63 }}
# Replace parts of a string
path: {{ .Values.path | replace "/" "-" }}
Working with YAML
# Convert a structure to YAML
data:
{{- toYaml .Values.podAnnotations | nindent 4 }}
# Convert to JSON
data:
{{ .Values.config | toJson | indent 4 }}
Math Functions
# Basic arithmetic
replicas: {{ add .Values.replicaCount 1 }}
timeout: {{ mul .Values.baseTimeout 2 }}
Path Functions
# Check if a value exists
{{- if hasKey .Values "serviceAccount" }}
serviceAccountName: {{ .Values.serviceAccount.name }}
{{- end }}
# Get a nested value with a default
timeout: {{ get .Values.timeouts "http" | default 30 }}
Function Category | Examples |
---|---|
String | quote , upper , lower , title , trim , trimSuffix , trimPrefix , replace , trunc |
Data Type | toYaml , toJson , fromYaml , fromJson , toString , toInt |
Flow Control | default , if/else , and , or , not , eq , ne , lt , gt , le , ge |
List/Map | list , dict , get , hasKey , keys , values , pluck , dig |
Math | add , sub , mul , div , mod , floor , ceil , round |
Named Templates and Reusing Code
Named templates allow you to define reusable chunks of template code. They’re typically defined in the _helpers.tpl
file.
Defining a Template
{{/* Generate basic labels */}}
{{- define "myapp.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 a Template
To use a template, you can use the include
or template
functions:
# Using include (recommended)
metadata:
labels:
{{- include "myapp.labels" . | nindent 4 }}
# Using template (doesn't support piping)
metadata:
labels:
{{ template "myapp.labels" . }}
Always prefer include
over template
for reusable code blocks. The key advantages of include
are:
- It captures the output as a string that can be passed through functions like
indent
- It allows for better whitespace control
- It provides more flexibility in how the output is used
Template Scope and the Dot
When working with templates, the dot (.
) represents the current scope. You can pass different scopes to templates:
# Passing the root scope
{{- include "myapp.labels" . }}
# Passing a specific value
{{- include "myapp.container" .Values.mainContainer }}
# Passing multiple values using dict
{{- include "myapp.deployment" (dict "root" . "name" "frontend" "replicas" 3) }}
Advanced Techniques
Partials and Template Composition
Break complex templates into smaller, manageable chunks:
{{- define "myapp.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .name }}
labels:
{{- include "myapp.labels" .root | nindent 4 }}
spec:
replicas: {{ .replicas }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" .root | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" .root | nindent 8 }}
spec:
{{- include "myapp.podSpec" .root | nindent 6 }}
{{- end -}}
Working with Complex Data
Handle complex data structures with powerful functions:
# Using with to change the scope
{{- with .Values.serviceAccount }}
serviceAccountName: {{ .name }}
{{- end }}
# Creating temporary variables
{{- $fullName := include "myapp.fullname" . -}}
{{- $servicePort := .Values.service.port -}}
{{- range .Values.ingress.hosts }}
- host: {{ . }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}
servicePort: {{ $servicePort }}
{{- end }}
Debugging Templates
Helm provides tools for debugging templates:
# Print a value for debugging
{{ .Values.someValue | quote }}
{{/* The above line will be rendered in the output */}}
# Debug with special function
{{ .Values.complicated.structure | toYaml | debug }}
Use Helm’s template validation and testing:
# Validate templates without installing
helm template -f values.yaml /path/to/chart
# See all generated manifests
helm install --debug --dry-run myrelease /path/to/chart
Practical Examples
Let’s look at some common use cases for Helm templating:
Deployment with Conditional Configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.command }}
command:
{{- toYaml .Values.command | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
protocol: TCP
{{- if .Values.probes.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
Secret with ImagePullSecrets
The following example shows how to create Docker registry secrets dynamically:
ConfigMap with File Content
This example demonstrates loading configuration from a file:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
data:
{{- if .Files.Glob "config/*.ini" }}
{{- range $path, $_ := .Files.Glob "config/*.ini" }}
{{ base $path }}: |-
{{ $.Files.Get $path | nindent 4 }}
{{- end }}
{{- end }}
{{- range $key, $val := .Values.config }}
{{ $key }}: {{ $val }}
{{- end }}
Best Practices
Structure and Organization
- Keep templates focused: Each template file should generally define a single Kubernetes resource.
- Use _helpers.tpl: Put all reusable template parts in this file for better organization.
- Consistent naming: Use consistent naming conventions for templates, values, and files.
- Comment your templates: Add comments to explain complex parts of your templates.
Values Management
- Set sensible defaults: Provide default values that work out of the box.
- Validate values: Use conditionals to validate required values.
- Structure values logically: Organize values by component or functionality.
- Document values: Include comments in values.yaml explaining each value.
Performance and Maintenance
- Limit template complexity: Break complex templates into smaller, reusable parts.
- Test templates: Use
helm lint
andhelm template
to validate your templates. - Version chart components: Use proper versioning for your charts.
- Keep it DRY: Don’t repeat yourself - use named templates for repeated patterns.
- Use
helm lint
to check for syntax errors - Use
helm template --debug
to see the rendered output - Add temporary debug statements with
{{ .Values.something | toYaml }}
- Check whitespace issues by looking at the actual rendered YAML
- Start simple and gradually add complexity, testing at each step
Conclusion
Helm Chart templating is a powerful system that allows you to create flexible, reusable Kubernetes application packages. By mastering the syntax and patterns described in this guide, you can create charts that work across different environments and use cases, saving time and reducing configuration errors.
Remember that good Helm Charts strike a balance between flexibility and simplicity - provide customization options where needed, but keep the default experience straightforward and well-documented.
- Helm uses Go templating extended with Kubernetes-specific functions
- Templates can access values from values.yaml, command line, and parent charts
- Named templates promote code reuse and maintainability
- Control structures like conditionals and loops enable dynamic configuration
- Using the provided functions effectively can simplify complex transformations
- Well-structured templates with proper whitespace management produce clean YAML
Comments