Understanding Helm Chart Template Syntax

A comprehensive guide to Helm Chart templating and best practices

Featured image



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.

What Are Helm Chart Templates?

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
graph TD A[values.yaml] --> B[Template Engine] C[Chart Templates] --> B D[Built-in Objects] --> B B --> E[Rendered Kubernetes Manifests] E --> F[Kubernetes API] style A fill:#ffcc80,stroke:#333,stroke-width:1px style B fill:#81c784,stroke:#333,stroke-width:1px style C fill:#64b5f6,stroke:#333,stroke-width:1px style D fill:#ba68c8,stroke:#333,stroke-width:1px style E fill:#4dd0e1,stroke:#333,stroke-width:1px style F fill:#a1887f,stroke:#333,stroke-width:1px


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 }}
Whitespace Management

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:

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 }}
flowchart TD A[Command Line Values] -->|Override| B[Chart-Specific values.yaml] C[Parent Chart Values] -->|Override| B B -->|Default| D[Final Values] D -->|Available as| E[.Values Object] style A fill:#ffcc80,stroke:#333,stroke-width:1px style B fill:#81c784,stroke:#333,stroke-width:1px style C fill:#64b5f6,stroke:#333,stroke-width:1px style D fill:#ba68c8,stroke:#333,stroke-width:1px style E fill:#4dd0e1,stroke:#333,stroke-width:1px


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 }}
Range Loop Best Practices
  • 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" . }}
include vs template

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

flowchart TD A[values.yaml] --> B{Template Processing} B -->|If metrics enabled| C[ServiceMonitor Created] B -->|Always| D[Deployment Created] B -->|Always| E[Service Created] B -->|If ingress enabled| F[Ingress Created] style A fill:#ffcc80,stroke:#333,stroke-width:1px style B fill:#81c784,stroke:#333,stroke-width:1px style C fill:#64b5f6,stroke:#333,stroke-width:1px style D fill:#ba68c8,stroke:#333,stroke-width:1px style E fill:#4dd0e1,stroke:#333,stroke-width:1px style F fill:#a1887f,stroke:#333,stroke-width:1px
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

  1. Keep templates focused: Each template file should generally define a single Kubernetes resource.
  2. Use _helpers.tpl: Put all reusable template parts in this file for better organization.
  3. Consistent naming: Use consistent naming conventions for templates, values, and files.
  4. Comment your templates: Add comments to explain complex parts of your templates.

Values Management

  1. Set sensible defaults: Provide default values that work out of the box.
  2. Validate values: Use conditionals to validate required values.
  3. Structure values logically: Organize values by component or functionality.
  4. Document values: Include comments in values.yaml explaining each value.

Performance and Maintenance

  1. Limit template complexity: Break complex templates into smaller, reusable parts.
  2. Test templates: Use helm lint and helm template to validate your templates.
  3. Version chart components: Use proper versioning for your charts.
  4. Keep it DRY: Don’t repeat yourself - use named templates for repeated patterns.
Tips for Debugging Templates
  1. Use helm lint to check for syntax errors
  2. Use helm template --debug to see the rendered output
  3. Add temporary debug statements with {{ .Values.something | toYaml }}
  4. Check whitespace issues by looking at the actual rendered YAML
  5. 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.

Key Takeaways
  • 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



References