Helm Templating
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.
Built-in Objects
Section titled “Built-in Objects”Inside any template you have access to:
| Object | What It Contains |
|---|---|
.Values | Merged values from values.yaml + user overrides (-f, --set) |
.Release | Release metadata: .Release.Name, .Release.Namespace, .Release.Revision |
.Chart | Contents of Chart.yaml: .Chart.Name, .Chart.Version |
.Capabilities | Cluster info: .Capabilities.KubeVersion, .Capabilities.APIVersions |
.Template | Current template: .Template.Name, .Template.BasePath |
Value Substitution
Section titled “Value Substitution”The most common pattern — inject values into manifests:
apiVersion: apps/v1kind: Deploymentmetadata: 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: 3image: repository: my-app tag: v1.2.0containerPort: 8080Conditionals
Section titled “Conditionals”Include or exclude blocks based on values:
{{- if .Values.ingress.enabled }}apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: {{ .Release.Name }}-ingressspec: 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: infoDefault Values and Pipelines
Section titled “Default Values and Pipelines”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:
| Function | Example | Result |
|---|---|---|
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 |
Named Templates (_helpers.tpl)
Section titled “Named Templates (_helpers.tpl)”Define reusable snippets in _helpers.tpl (the _ prefix tells Helm not to render it as a manifest):
{{- 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:
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.
Previewing Rendered Output
Section titled “Previewing Rendered Output”Render templates locally without applying to the cluster:
helm template my-release ./my-chart # render all templateshelm template my-release ./my-chart -f prod-values.yaml # with custom valueshelm template my-release ./my-chart --show-only templates/deployment.yaml # one fileThis 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):
helm install my-release ./my-chart --dry-run --debugValue Injection Methods
Section titled “Value Injection Methods”Values reach your templates from multiple sources. Helm merges them in a defined precedence order (lowest to highest priority):
1. Chart’s values.yaml (Defaults)
Section titled “1. Chart’s values.yaml (Defaults)”The values.yaml inside the chart. Every value here is the baseline that users can override:
replicaCount: 1image: repository: nginx tag: latest2. Parent Chart Values (Sub-Charts)
Section titled “2. Parent Chart Values (Sub-Charts)”When a chart is a dependency (sub-chart), the parent can pass values down by nesting under the child chart’s name:
my-subchart: replicaCount: 33. Values Files (-f / --values)
Section titled “3. Values Files (-f / --values)”One or more custom files at install/upgrade time. If you pass multiple, later files override earlier ones:
helm install my-app ./my-chart -f base.yaml -f prod.yamlprod.yaml overrides anything also set in base.yaml.
4. Command-Line Overrides (--set)
Section titled “4. Command-Line Overrides (--set)”Individual overrides. Highest built-in priority — overrides everything above:
helm install my-app ./my-chart --set replicaCount=5helm install my-app ./my-chart --set image.tag=v2.0.0Nested keys use dots. Lists use index syntax:
--set env[0].name=NODE_ENV,env[0].value=production--set Variants
Section titled “--set Variants”| Flag | Purpose |
|---|---|
--set | Standard — infers type (numbers, bools, strings) |
--set-string | Forces the value to a string (e.g. --set-string image.tag=1.0 keeps "1.0" as a string, not a number) |
--set-file | Sets a value to the contents of a file (e.g. --set-file tls.cert=./cert.pem) |
--set-json | Sets a value from a JSON string (e.g. --set-json 'resources={"limits":{"cpu":"200m"}}') |
Precedence Order
Section titled “Precedence Order”Chart values.yaml → Parent chart values → -f files (left to right) → --set / --set-stringLater always wins. So --set replicaCount=10 beats replicaCount: 3 from any values file.
Where Values Come From in Production
Section titled “Where Values Come From in Production”In real deployments, values rarely come from just a static file. They flow in from CI/CD pipelines, secret managers, and GitOps tools.
CI/CD Pipeline Variables
Section titled “CI/CD Pipeline Variables”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.yamlKubernetes 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 templateenv: - name: DB_PASSWORD valueFrom: secretKeyRef: name: my-db-secret key: passwordThe 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:
helm secrets encrypt secrets.yamlhelm secrets install my-app ./chart -f secrets.yamlThe encrypted file lives in your repo; decryption happens at deploy time using a key from your CI/CD environment or a cloud KMS.
External Secrets Operator
Section titled “External Secrets Operator”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/v1beta1kind: ExternalSecretmetadata: name: my-db-secretspec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: ClusterSecretStore target: name: my-db-secret data: - secretKey: password remoteRef: key: prod/db property: passwordYour Helm chart references the resulting Kubernetes Secret — it doesn’t need to know the password itself.
HashiCorp Vault (Direct Injection)
Section titled “HashiCorp Vault (Direct Injection)”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.
GitOps Tools (ArgoCD, Flux)
Section titled “GitOps Tools (ArgoCD, Flux)”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
Summary
Section titled “Summary”| Source | How It Reaches the Template | When |
|---|---|---|
values.yaml | Baked into chart | Chart defaults |
-f custom.yaml | File at deploy time | Environment config |
--set / --set-string | CLI at deploy time | CI/CD pipeline vars |
--set-file | File contents at deploy time | Certs, keys |
| Helm Secrets (SOPS) | Encrypted values file, decrypted at deploy | Secrets in Git |
| K8s Secrets / ConfigMaps | Pod references at runtime | Runtime config |
| External Secrets Operator | External store → K8s Secret automatically | Vault, AWS SM, Azure KV |
| Vault Agent Injector | Sidecar fetches at runtime | Direct 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.
Key Takeaways
Section titled “Key Takeaways”- Templates use Go templating:
{{ .Values.x }}for substitution,{{- if }}/{{- range }}for control flow. - Put reusable labels and snippets in
_helpers.tpl; reference them withinclude. - Always preview with
helm templatebefore applying — you see the exact rendered YAML. - Values flow from multiple sources with clear precedence:
values.yaml<-ffiles <--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.