Skip to content

Helm Templating

First PublishedByAtif Alam

Helm templates are standard Kubernetes manifests with Go template directives ({{ }}) injected. This is what turns static YAML into reusable, parameterized packages. For Helm basics (install, upgrade, rollback, chart structure), see Helm.

Inside any template you have access to:

ObjectWhat It Contains
.ValuesMerged values from values.yaml + user overrides (-f, --set)
.ReleaseRelease metadata: .Release.Name, .Release.Namespace, .Release.Revision
.ChartContents of Chart.yaml: .Chart.Name, .Chart.Version
.CapabilitiesCluster info: .Capabilities.KubeVersion, .Capabilities.APIVersions
.TemplateCurrent template: .Template.Name, .Template.BasePath

The most common pattern — inject values into manifests:

templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
labels:
app: {{ .Chart.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.containerPort }}

With a values.yaml of:

replicaCount: 3
image:
repository: my-app
tag: v1.2.0
containerPort: 8080

Include or exclude blocks based on values:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-svc
port:
number: 80
{{- end }}

The {{- (with dash) trims whitespace before the directive, keeping the output clean.

Iterate over lists or maps:

{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}

With values:

env:
- name: NODE_ENV
value: production
- name: LOG_LEVEL
value: info

Use default to provide fallbacks and | to pipe through functions:

image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
annotations:
description: {{ .Values.description | default "No description" | quote }}

Common template functions:

FunctionExampleResult
quote{{ "hello" | quote }}"hello"
upper / lower{{ "Hello" | lower }}hello
default{{ .Values.x | default "fallback" }}value of x, or "fallback"
toYaml{{ .Values.resources | toYaml }}YAML block from a nested value
indent{{ .Values.resources | toYaml | indent 8 }}indented YAML block
include{{ include "my-chart.labels" . }}output of a named template

Define reusable snippets in _helpers.tpl (the _ prefix tells Helm not to render it as a manifest):

templates/_helpers.tpl
{{- define "my-chart.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | default .Chart.Version }}
{{- end -}}

Use them in any template with include:

templates/deployment.yaml
metadata:
name: {{ .Release.Name }}-app
labels:
{{- include "my-chart.labels" . | nindent 4 }}

nindent 4 adds a newline then indents every line by 4 spaces — essential for keeping YAML valid.

Render templates locally without applying to the cluster:

Terminal window
helm template my-release ./my-chart # render all templates
helm template my-release ./my-chart -f prod-values.yaml # with custom values
helm template my-release ./my-chart --show-only templates/deployment.yaml # one file

This is invaluable for debugging — you see exactly what YAML Helm will send to the cluster.

You can also do a dry-run against the cluster (validates API versions and schemas):

Terminal window
helm install my-release ./my-chart --dry-run --debug

Values reach your templates from multiple sources. Helm merges them in a defined precedence order (lowest to highest priority):

The values.yaml inside the chart. Every value here is the baseline that users can override:

my-chart/values.yaml
replicaCount: 1
image:
repository: nginx
tag: latest

When a chart is a dependency (sub-chart), the parent can pass values down by nesting under the child chart’s name:

parent-chart/values.yaml
my-subchart:
replicaCount: 3

One or more custom files at install/upgrade time. If you pass multiple, later files override earlier ones:

Terminal window
helm install my-app ./my-chart -f base.yaml -f prod.yaml

prod.yaml overrides anything also set in base.yaml.

Individual overrides. Highest built-in priority — overrides everything above:

Terminal window
helm install my-app ./my-chart --set replicaCount=5
helm install my-app ./my-chart --set image.tag=v2.0.0

Nested keys use dots. Lists use index syntax:

Terminal window
--set env[0].name=NODE_ENV,env[0].value=production
FlagPurpose
--setStandard — infers type (numbers, bools, strings)
--set-stringForces the value to a string (e.g. --set-string image.tag=1.0 keeps "1.0" as a string, not a number)
--set-fileSets a value to the contents of a file (e.g. --set-file tls.cert=./cert.pem)
--set-jsonSets a value from a JSON string (e.g. --set-json 'resources={"limits":{"cpu":"200m"}}')
Chart values.yaml → Parent chart values → -f files (left to right) → --set / --set-string

Later always wins. So --set replicaCount=10 beats replicaCount: 3 from any values file.


In real deployments, values rarely come from just a static file. They flow in from CI/CD pipelines, secret managers, and GitOps tools.

The most common pattern. Pipeline variables (GitHub Actions secrets, GitLab CI variables, Jenkins credentials) get passed through --set or written into a values file dynamically:

# GitHub Actions example
- run: |
helm upgrade my-app ./chart \
--set image.tag=${{ github.sha }} \
--set database.password=${{ secrets.DB_PASSWORD }}

Or generate a values file on the fly:

- run: |
cat <<EOF > ci-values.yaml
image:
tag: "${{ github.sha }}"
database:
host: "${{ vars.DB_HOST }}"
EOF
helm upgrade my-app ./chart -f ci-values.yaml

Kubernetes Secrets and ConfigMaps (Runtime)

Section titled “Kubernetes Secrets and ConfigMaps (Runtime)”

These aren’t injected as Helm values — instead, the running pods reference them at runtime:

# In the Helm template
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-db-secret
key: password

The Secret itself is managed outside Helm (created manually, by a CI step, or by an operator). Helm just references it by name.

Helm Secrets Plugin (Encrypted Values Files)

Section titled “Helm Secrets Plugin (Encrypted Values Files)”

The helm-secrets plugin uses SOPS or age to encrypt values files so you can commit them to Git safely:

Terminal window
helm secrets encrypt secrets.yaml
helm secrets install my-app ./chart -f secrets.yaml

The encrypted file lives in your repo; decryption happens at deploy time using a key from your CI/CD environment or a cloud KMS.

A Kubernetes operator that syncs secrets from external stores (Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) into Kubernetes Secret objects automatically:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-db-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: my-db-secret
data:
- secretKey: password
remoteRef:
key: prod/db
property: password

Your Helm chart references the resulting Kubernetes Secret — it doesn’t need to know the password itself.

Vault Agent Injector adds a sidecar to your pod that fetches secrets at runtime:

annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "my-app"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/prod/db"

The secret shows up as a file inside the container. No Helm value needed.

These pull chart + values from Git and apply automatically. Values can come from:

  • Values files committed to the Git repo
  • Encrypted secrets (SOPS integration)
  • ApplicationSet generators (ArgoCD) that inject cluster-specific values
SourceHow It Reaches the TemplateWhen
values.yamlBaked into chartChart defaults
-f custom.yamlFile at deploy timeEnvironment config
--set / --set-stringCLI at deploy timeCI/CD pipeline vars
--set-fileFile contents at deploy timeCerts, keys
Helm Secrets (SOPS)Encrypted values file, decrypted at deploySecrets in Git
K8s Secrets / ConfigMapsPod references at runtimeRuntime config
External Secrets OperatorExternal store → K8s Secret automaticallyVault, AWS SM, Azure KV
Vault Agent InjectorSidecar fetches at runtimeDirect Vault integration

The key distinction: Helm values are a deploy-time mechanism (what YAML gets rendered). Kubernetes Secrets, Vault, and External Secrets Operator are runtime mechanisms (what the running pod can access). In practice you use both — Helm values for non-sensitive config and image tags, runtime secrets for credentials.


  • Templates use Go templating: {{ .Values.x }} for substitution, {{- if }} / {{- range }} for control flow.
  • Put reusable labels and snippets in _helpers.tpl; reference them with include.
  • Always preview with helm template before applying — you see the exact rendered YAML.
  • Values flow from multiple sources with clear precedence: values.yaml < -f files < --set.
  • In production, values come from CI/CD pipelines, secret managers, and GitOps tools — not just static files.
  • Deploy-time values (Helm) handle config; runtime secrets (K8s Secrets, Vault, External Secrets) handle credentials.