Helm Chart Structure & Templating
Helm packages Kubernetes manifests into charts. Templates render YAML from values, release metadata, and helpers. Always helm lint and helm template before installing or upgrading in a client cluster.
Core Concepts
| Object | What it is | Example |
|---|---|---|
| Chart | Packaged templates + default values | charts/web-api/ |
| Release | Installed instance of a chart in a namespace | web-api in app-prod |
| Values | Configuration passed at install/upgrade | values-prod.yaml |
| Revision | Versioned snapshot of a release in history | Revision 14 after upgrade |
Chart + values render into a Release — a live installed instance tracked in Helm history.
Chart Structure
This tree shows the standard files inside an application chart. Use it as your checklist when creating a chart from scratch or when reviewing a client chart for missing helpers, defaults, CRDs, or notes.
web-api/
Chart.yaml # Metadata: name, version, dependencies.
values.yaml # Default values — the chart's public API.
templates/ # Go-template Kubernetes manifests.
deployment.yaml
service.yaml
ingress.yaml
_helpers.tpl # Named helper templates (not rendered directly).
NOTES.txt # Post-install instructions shown to the user.
charts/ # Vendored subchart .tgz files (after dependency build).
crds/ # CRDs installed before other resources.
.helmignore # Files excluded from packaged chart.
Chart.yaml
Chart.yaml is the chart metadata file. Use it to set the chart name, version, app version, chart type, and dependencies that Helm should download or package.
apiVersion: v2
name: web-api
description: Platform web API
type: application # Use "library" for shared helper-only charts.
version: 1.4.0 # Chart version — bump on every chart change.
appVersion: "2.1.0" # App version label; often overridden by image.tag.
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # Skip subchart when false.
Render & Validate
Use these commands before installing or upgrading a release. They catch template syntax, bad YAML, missing values, and API-server validation errors before a client cluster is changed.
helm lint ./web-api
helm template web-api ./web-api -n app -f values-prod.yaml
helm template web-api ./web-api -n app -f values-prod.yaml --debug # Show render errors with context.
helm install web-api ./web-api -n app --create-namespace --dry-run --debug
helm upgrade web-api ./web-api -n app -f values-prod.yaml --dry-run --debug
Deployment Template
This is the basic Deployment template pattern for a Helm app chart. It pulls names, labels, image, ports, and replica count from values and helpers so the same chart can run in multiple environments.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web-api.fullname" . }}
labels:
{{- include "web-api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web-api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "web-api.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.targetPort }}
Conditional Resources
Wrap optional resources in if blocks so they are omitted entirely when disabled — not rendered as empty YAML.
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "web-api.fullname" . }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.host | required "ingress.host is required when ingress.enabled=true" }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "web-api.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- end }}
Helpers
Helpers centralize naming and labels so every template renders consistent Kubernetes metadata. Use them to avoid copy-pasting labels and to keep names within Kubernetes length limits.
{{- define "web-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "web-api.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "web-api.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "web-api.labels" -}}
app.kubernetes.io/name: {{ include "web-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
Template Functions Cheat Sheet
| Function | Purpose | Example |
|---|---|---|
default | Fallback when value is empty | {{ .Values.tag | default "latest" }} |
required | Fail render if value missing | {{ .Values.host | required "host required" }} |
nindent | Indent nested YAML correctly | {{ toYaml .Values.env | nindent 8 }} |
toYaml | Render map/list as YAML | {{ toYaml .Values.resources | nindent 10 }} |
range | Loop over lists/maps | {{- range .Values.env }}... |
trunc 63 | Enforce K8s 63-char name limit | {{ .Values.name | trunc 63 }} |
Gotchas
- YAML indentation: use
nindentwhen inserting maps or lists — rawtoYamlwithout indent breaks manifests. - Selectors are immutable: never template label selectors in a way that changes after install.
- CRDs: Helm installs CRDs from
crds/on first install only; upgrading CRDs needs a separate process. - Secrets: never commit raw secrets in
values.yaml— use External Secrets, Sealed Secrets, or CI-injected--set-file. - Whitespace: leading
-in{{- if }}trims blank lines; omit it when you need YAML spacing.
helm template output to a file and run kubectl apply --dry-run=server -f to catch schema and admission webhook errors before touching the cluster.