TL;DR

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

ObjectWhat it isExample
ChartPackaged templates + default valuescharts/web-api/
ReleaseInstalled instance of a chart in a namespaceweb-api in app-prod
ValuesConfiguration passed at install/upgradevalues-prod.yaml
RevisionVersioned snapshot of a release in historyRevision 14 after upgrade
Chart templates/ values.yaml Chart.yaml Values -f values-prod --set image.tag helm install render templates + release metadata Release web-api @ rev 14 → Deploy, Svc, Ing One chart can have many releases (dev, staging, prod) with different values files.

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.

text chart-tree.txt
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.

yaml Chart.yaml
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.

bash helm-render.sh
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.

yaml templates/deployment.yaml
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.

yaml templates/ingress.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.

gotemplate templates/_helpers.tpl
{{- 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

FunctionPurposeExample
defaultFallback when value is empty{{ .Values.tag | default "latest" }}
requiredFail render if value missing{{ .Values.host | required "host required" }}
nindentIndent nested YAML correctly{{ toYaml .Values.env | nindent 8 }}
toYamlRender map/list as YAML{{ toYaml .Values.resources | nindent 10 }}
rangeLoop over lists/maps{{- range .Values.env }}...
trunc 63Enforce K8s 63-char name limit{{ .Values.name | trunc 63 }}

Gotchas

  • !YAML indentation: use nindent when inserting maps or lists — raw toYaml without 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.
💡
Render before apply Save 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.