Values and Templating
Learn how Helm values work and how Go templates turn your charts into dynamic, reusable Kubernetes manifests.
Values and Templating
In the previous tutorial, we explored every file in a Helm chart. Now it's time to learn the language that makes charts dynamic — Go templates. If you've used Jinja2 (Python) or Handlebars (JavaScript), you'll pick this up fast. If not, don't worry — it's simpler than it looks.
Go Template Basics
Every template expression lives inside double curly braces:
# This is a static Kubernetes manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
# This is a Helm template:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
When you install with helm install prod ./my-chart, the {{ .Release.Name }} becomes prod, and you get:
apiVersion: v1
kind: ConfigMap
metadata:
name: prod-config
That's the core idea. Everything inside {{ }} gets evaluated, everything outside is passed through as-is.
The Dot — Your Best Friend
In Go templates, the dot (.) refers to the current scope — the top-level data object. When Helm renders a template, it passes in a root object with several fields:
# The root object looks conceptually like this:
.Release # Info about the current release
.Values # Merged values from values.yaml + overrides
.Chart # Contents of Chart.yaml
.Files # Access to non-template files in the chart
.Capabilities # Info about the Kubernetes cluster
.Template # Info about the current template file
You access nested properties with dots:
{{ .Values.image.repository }}
{{ .Release.Name }}
{{ .Chart.AppVersion }}
.Values — User Configuration
This is where you'll spend most of your time. Every key in values.yaml is accessible under .Values:
# values.yaml
database:
host: localhost
port: 5432
name: myapp
credentials:
username: admin
password: secret123
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-db-config
data:
DB_HOST: {{ .Values.database.host }}
DB_PORT: {{ .Values.database.port | quote }}
DB_NAME: {{ .Values.database.name }}
"Why
quoteon the port?"
Good catch. Kubernetes ConfigMap values must be strings. A bare 5432 in YAML is interpreted as an integer, which K8s rejects. The quote function wraps it in double quotes: "5432". We'll cover more functions in the next tutorial.
.Release — Install Metadata
# Useful for making resources unique per-release
metadata:
name: {{ .Release.Name }}-app
namespace: {{ .Release.Namespace }}
labels:
release: {{ .Release.Name }}
revision: {{ .Release.Revision | quote }}
Full list of .Release fields:
| Field | Description | Example |
|---|---|---|
.Release.Name | Release name from helm install NAME | prod |
.Release.Namespace | Namespace being deployed to | default |
.Release.Revision | Revision number (starts at 1) | 3 |
.Release.IsInstall | true on first install | true |
.Release.IsUpgrade | true on helm upgrade | false |
.Release.Service | Always "Helm" | Helm |
.Chart — Chart Metadata
Everything from Chart.yaml is available here:
labels:
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app-version: {{ .Chart.AppVersion | quote }}
Note: Chart.yaml keys are title-cased when accessed. So name becomes .Chart.Name, version becomes .Chart.Version.
.Capabilities — Cluster Info
Useful for writing charts that adapt to different Kubernetes versions:
{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
apiVersion: networking.k8s.io/v1
{{- else }}
apiVersion: networking.k8s.io/v1beta1
{{- end }}
kind: Ingress
# Check the Kubernetes version
{{ .Capabilities.KubeVersion.Major }}
{{ .Capabilities.KubeVersion.Minor }}
{{ .Capabilities.KubeVersion.Version }}
.Files — Accessing Extra Files
You can include non-template files from your chart:
# Suppose you have config/app.conf in your chart root
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-app-config
data:
app.conf: |-
{{ .Files.Get "config/app.conf" | indent 4 }}
The .Files object also has some handy methods:
# Get file content as string
{{ .Files.Get "config/app.conf" }}
# Get file content as bytes
{{ .Files.GetBytes "bindata/cert.pem" }}
# Glob for multiple files
{{ range $path, $content := .Files.Glob "config/*.conf" }}
{{ $path }}: |-
{{ $content | indent 4 }}
{{ end }}
# Base64 encode (useful for Secrets)
{{ .Files.Get "certs/tls.crt" | b64enc }}
Note: .Files cannot access files in templates/ or files excluded by .helmignore.
Whitespace Control
This is the one thing that trips up everyone. Look at this template:
metadata:
labels:
{{ if .Values.environment }}
env: {{ .Values.environment }}
{{ end }}
app: my-app
If environment is set, the output has blank lines:
metadata:
labels:
env: production
app: my-app
Gross. Fix it with dash-trimming {{- and -}}:
metadata:
labels:
{{- if .Values.environment }}
env: {{ .Values.environment }}
{{- end }}
app: my-app
Clean output:
metadata:
labels:
env: production
app: my-app
The rules:
{{-trims all whitespace (including newlines) before the tag-}}trims all whitespace after the tag{{and}}(no dash) leave whitespace alone
"When should I use dashes?"
Use {{- on control structures (if, end, range, etc.) that would leave blank lines. Don't use them on lines that output actual values — you'll eat the indentation and break your YAML.
Variables
You can assign values to variables with :=:
{{- $releaseName := .Release.Name -}}
{{- $fullName := printf "%s-%s" $releaseName .Chart.Name -}}
metadata:
name: {{ $fullName }}
labels:
release: {{ $releaseName }}
Variables are especially useful inside range loops where the dot (.) gets rebound:
# values.yaml
ports:
- name: http
port: 80
- name: https
port: 443
# Without variable — can't access .Release.Name inside range!
{{- range .Values.ports }}
- name: {{ .name }} # .name works (dot is rebound to current item)
port: {{ .port }}
# {{ .Release.Name }} # BROKEN — .Release doesn't exist here!
{{- end }}
# With variable — save root context before the loop
{{- $root := . -}}
{{- range .Values.ports }}
- name: {{ .name }}
port: {{ .port }}
release: {{ $root.Release.Name }} # Works!
{{- end }}
There's also a two-variable form for ranges:
{{- range $key, $value := .Values.env }}
{{ $key }}: {{ $value | quote }}
{{- end }}
Quoting and Type Safety
YAML type coercion is a common pitfall. Here's what catches people:
# values.yaml
port: 8080
enabled: true
version: "1.0"
# In a ConfigMap, all values must be strings
data:
PORT: {{ .Values.port }} # Renders as: PORT: 8080 (integer — ERROR!)
PORT: {{ .Values.port | quote }} # Renders as: PORT: "8080" (string — correct)
ENABLED: {{ .Values.enabled }} # Renders as: ENABLED: true (boolean — ERROR!)
ENABLED: {{ .Values.enabled | quote }} # Renders as: ENABLED: "true" (string — correct)
Rule of thumb:
- In ConfigMaps and environment variables → always
quote - In resource specs (replicas, ports, etc.) → don't quote (they expect numbers)
Rendering Complex Values with toYaml
Sometimes you want to dump an entire YAML block from values:
# values.yaml
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
# templates/deployment.yaml
containers:
- name: app
resources:
{{- toYaml .Values.resources | nindent 6 }}
The toYaml function converts the Go value back to YAML text, and nindent 6 adds a newline and indents everything by 6 spaces. Result:
containers:
- name: app
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
"What's the difference between
indentandnindent?"
indent N— adds N spaces to the beginning of each linenindent N— same thing, but also adds a newline before the first line
Use nindent when the template tag is on the same line as a YAML key (the most common case). Use indent when you're already on a new line.
Debugging Templates
When your template produces unexpected output, use helm template to render without installing:
# Render all templates locally
helm template my-release ./my-chart
# Render with custom values
helm template my-release ./my-chart -f custom-values.yaml
# Render a specific template
helm template my-release ./my-chart -s templates/deployment.yaml
# Show all computed values
helm template my-release ./my-chart --debug
There's also a fail function for intentional errors:
{{- if not .Values.image.repository }}
{{- fail "image.repository is required!" }}
{{- end }}
Quick Reference: Template Syntax
| Syntax | Purpose | Example |
|---|---|---|
{{ .Values.x }} | Output a value | {{ .Values.replicaCount }} |
{{- ... -}} | Trim whitespace | {{- if .Values.x }} |
{{/* comment */}} | Comment (not rendered) | {{/* TODO */}} |
{{ $var := val }} | Variable assignment | {{ $name := .Chart.Name }} |
{{ func arg }} | Function call | {{ quote .Values.port }} |
| pipe | Pipeline (same thing) | port pipe quote |
What's Next?
You now know how to access values, use the built-in objects, handle whitespace, and debug your templates. These are the building blocks for everything that follows.
In the next tutorial, we'll explore template functions and pipelines — the rich library of string, math, list, and dictionary functions that make Helm templates truly powerful.