Helm

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Kustomize customizes plain YAML by patching it. Helm takes the opposite route: the manifests are Go templates, parameterised by a values file, and the result of installing them is a named, versioned release that Helm can upgrade and roll back. This page is written against Helm 4.3 (Helm 3.x is still maintained but the section targets 4) and Kubernetes 1.37; see the section index for the full version baseline. Delivering charts from a Git repository is covered in GitOps and CI/CD, and chart-packaged operators appear in Custom Resources and Operators.

Concepts

  • A chart is a directory (or a .tgz archive) of templates and metadata describing one application.

  • A release is one installation of a chart into a cluster under a name and namespace. Installing the same chart twice with different names gives two independent releases. Each install or upgrade creates a numbered revision, and Helm stores release state as a Secret in the release namespace (type helm.sh/release.v1).

  • A repository is an HTTP server with an index.yaml of chart archives; an OCI registry stores charts as artifacts (oci:// URLs), the same registries described in Registries and Docker Hub. OCI is the recommended distribution path today.

  • Values are the configuration input: the chart’s values.yaml defaults, overridden by -f file.yaml and then by --set key=value (later sources win).

What changed from Helm 3 to Helm 4

Helm 4 is a major release that keeps chart apiVersion: v2 working. Verify the authoritative list in the Helm 4 release notes before relying on any detail; the themes are:

  • Server-side apply is the default for new installs, replacing the legacy three-way merge and improving behaviour when several controllers manage the same object. Upgrades and rollbacks keep the release’s previous apply method, so releases created by Helm 3 stay on client-side apply unless --server-side is passed.

  • Post-renderers moved to a plugin-based model, and the plugin system was reworked (including WebAssembly-based plugins).

  • Wait handling uses kstatus-based readiness (--wait) for more reliable "is it ready" decisions.

  • Renamed flags: --atomic became --rollback-on-failure and --force became --force-replace.

  • Some long-deprecated flags and Go SDK APIs were removed, so automation written for Helm 3 should be tested with helm template and helm lint before switching.

Charts written for Helm 3 generally install unchanged, which is why the examples below stay version-neutral where possible.

The CLI

helm install bookshelf ./charts/bookshelf -n bookshelf --create-namespace -f values-dev.yaml
helm upgrade --install bookshelf ./charts/bookshelf -n bookshelf -f values-prod.yaml --set image.tag=1.0.0
helm list -n bookshelf
helm history bookshelf -n bookshelf
helm rollback bookshelf 1 -n bookshelf
helm get values bookshelf -n bookshelf
helm uninstall bookshelf -n bookshelf

# Offline work: render, validate, inspect
helm template bookshelf ./charts/bookshelf -f values-dev.yaml
helm lint ./charts/bookshelf --strict
helm show values oci://registry.example.com/charts/bookshelf --version 0.3.0

# Preview an upgrade against the live release (helm-diff plugin)
curl -sL https://github.com/databus23.gpg | gpg --import
helm plugin install https://github.com/databus23/helm-diff/releases/latest/download/helm-diff-linux-amd64.tgz
helm diff upgrade bookshelf ./charts/bookshelf -n bookshelf -f values-prod.yaml --set image.tag=1.0.0

helm upgrade --install is idempotent and is the form used in CI, because it works for the first deployment and every later one. helm template prints the manifests without contacting a cluster, which is the quickest way to debug a template. helm rollback creates a new revision whose content is the old one. helm diff is not built in: the helm-diff plugin compares the deployed release with what an upgrade would apply, which makes it a common review step in CI. Helm 4 verifies plugin signatures, hence the key import and a release archive for the local OS and architecture; on Helm 3, helm plugin install https://github.com/databus23/helm-diff is enough.

The Bookshelf chart

charts/bookshelf/
├── Chart.yaml
├── values.yaml
├── values.schema.json
├── charts/                 # packaged dependencies
└── templates/
    ├── _helpers.tpl
    ├── NOTES.txt
    ├── deployment-catalog-api.yaml
    ├── service-catalog-api.yaml
    ├── configmap.yaml
    └── tests/
        └── test-connection.yaml
# Chart.yaml
apiVersion: v2
name: bookshelf
description: The Bookshelf demo application
type: application
version: 0.3.0        # chart version (SemVer)
appVersion: "1.0.0"   # version of the packaged application
dependencies:
  - name: postgresql
    version: "16.x.x"
    repository: oci://registry.example.com/charts
    condition: postgresql.enabled
# values.yaml
replicaCount: 2
image:
  repository: ghcr.io/example/bookshelf-catalog-api
  tag: ""            # defaults to .Chart.AppVersion
  pullPolicy: IfNotPresent
service:
  port: 80
resources:
  requests:
    cpu: 100m
    memory: 256Mi
postgresql:
  enabled: true

A values.schema.json makes bad input fail at install time rather than at runtime:

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["image"],
  "properties": {
    "replicaCount": { "type": "integer", "minimum": 1 },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    }
  }
}

The template language

Templates use Go’s text/template plus the Sprig function library. Because the delimiters are double braces, every template example below is shown in a listing block, where AsciiDoc leaves them alone. The constructs to know:

  • include calls a named template from _helpers.tpl and returns a string, so it can be piped (| nindent 4); tpl renders a string value as a template itself.

  • range loops over lists and maps; with narrows scope and skips empty values.

  • toYaml converts a value to YAML and nindent N adds a newline and indents it, the standard way to embed a values block.

  • required "message" .Values.x aborts rendering when a value is missing; default supplies a fallback.

# templates/_helpers.tpl
{{- define "bookshelf.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "bookshelf.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end -}}

Each Deployment adds its own app.kubernetes.io/component label to both the selector and the Pod template, so the selectors of catalog-api, reviews-api and web in the same release never overlap.

# templates/deployment-catalog-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "bookshelf.fullname" . }}-catalog-api
  labels:
    {{- include "bookshelf.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/instance: {{ .Release.Name }}
      app.kubernetes.io/component: catalog-api
  template:
    metadata:
      labels:
        {{- include "bookshelf.labels" . | nindent 8 }}
        app.kubernetes.io/component: catalog-api
    spec:
      containers:
        - name: catalog-api
          image: "{{ required "image.repository is required" .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}

NOTES.txt is a template printed after install; use it for the URL and the next command to run. See ReplicaSets and Deployments for the fields the Deployment above relies on.

Dependencies, hooks and tests

  • Dependencies are declared in Chart.yaml and fetched with helm dependency update into charts/; condition and tags switch them on or off, and a subchart reads its own values under a key named after the subchart.

  • Hooks are ordinary manifests with a helm.sh/hook annotation (pre-install, post-upgrade, pre-delete and so on). A pre-upgrade Job that runs database migrations is the classic case; add helm.sh/hook-delete-policy: before-hook-creation so the Job can run again. See Jobs and CronJobs.

  • Tests are Pods in templates/tests/ annotated helm.sh/hook: test; helm test bookshelf -n bookshelf runs them and reports pass or fail.

Artifact Hub, provenance and signing

Artifact Hub is the public index for discovering charts (and other cloud-native artifacts); it lists metadata but does not host charts. To verify that a chart was not tampered with, Helm supports provenance files: helm package --sign --key <key-name> --keyring <keyring-path> produces a .prov file containing the chart hash signed with a PGP key, and helm install --verify or helm pull --verify checks it. Charts in OCI registries can additionally be signed with registry-level tools such as Sigstore cosign. Never commit signing keys; keep them in your CI secret store.

Publishing to an OCI registry:

helm package charts/bookshelf
helm registry login ghcr.io --username <user> --password-stdin < token-file
helm push bookshelf-0.3.0.tgz oci://ghcr.io/example/charts

From values to a release

flowchart LR V["values.yaml\n+ -f files\n+ --set"] --> M["merged values"] S["values.schema.json"] -. validates .-> M T["templates/\n+ _helpers.tpl"] --> R["render engine"] M --> R R --> Y["rendered manifests"] Y --> A["apply to API server"] A --> REL["release revision N\n(stored as a Secret)"] REL -->|helm rollback| A