Helm¶
Helm is the standard package manager for Kubernetes. It wraps a set of manifests into a versioned, configurable artifact called a chart, and tracks deployed instances - called releases - so you can upgrade, diff, and roll back with a single command.
Without Helm, environment-specific configuration lives in duplicated YAML or fragile kustomize overlays. With Helm, you parameterize once and deploy anywhere.
How a release works¶
flowchart LR
Chart["Chart\n(templates + defaults)"] --> Render["helm template\n(render with values)"]
Values["values.yaml\n+ overrides"] --> Render
Render --> Release["Release in cluster\n(tracked in Secrets)"]
Release --> Upgrade["helm upgrade\n(diff + apply)"]
Upgrade --> History["Release history\n(rollback target)"]
Helm stores each release's rendered manifest and metadata as a Secret in the target namespace. That history is what makes helm rollback work - it renders the previous revision's manifest and re-applies it.
Chart anatomy¶
my-chart/
├── Chart.yaml # metadata: name, version, appVersion, dependencies
├── values.yaml # default values for all templates
├── charts/ # vendored sub-charts (from helm dependency build)
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── _helpers.tpl # named templates (partials) - not rendered directly
│ └── NOTES.txt # post-install user-facing instructions
└── .helmignore
Chart.yaml fields that matter¶
apiVersion: v2
name: my-app
version: 1.4.2 # chart version - bump this on every chart change
appVersion: "2.0.1" # application version - informational only
description: My application chart
type: application # or "library" for reusable partials
dependencies:
- name: postgresql
version: "12.x.x"
repository: "oci://registry-1.docker.io/bitnamicharts"
condition: postgresql.enabled
Templating with Sprig¶
Helm templates use Go's text/template engine augmented with the Sprig function library.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.env }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
Named templates (_helpers.tpl)¶
{{/*
Expand the name of the chart.
*/}}
{{- define "my-app.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels applied to every resource.
*/}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
Prefer include over template - include returns a string you can pipe through nindent, quote, and other functions.
Values architecture¶
A production values strategy has three layers:
values.yaml ← chart defaults (committed, no secrets)
values-prod.yaml ← environment overrides (committed)
values-secrets.yaml ← sensitive overrides (from Vault/SOPS, not committed)
helm upgrade my-app ./my-chart \
-f values-prod.yaml \
-f values-secrets.yaml \
--set image.tag=v2.3.1
Values from rightmost -f file win. --set wins over all files.
Secrets in values¶
Never commit plaintext secrets. Common patterns:
- External Secrets Operator: Helm creates the ExternalSecret CRD, which pulls from Vault/SSM at runtime.
- SOPS + helm-secrets plugin:
helm secrets upgrade ...decrypts on the fly. - Sealed Secrets: encrypt with the cluster's public key; commit the SealedSecret manifest.
Install and upgrade workflow¶
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install my-app ./my-chart -n platform --create-namespace -f values-prod.yaml
# Before upgrading: render and diff
helm diff upgrade my-app ./my-chart -n platform -f values-prod.yaml
helm upgrade my-app ./my-chart -n platform -f values-prod.yaml --atomic --cleanup-on-fail
# Verify, then rollback if needed
helm status my-app -n platform
helm history my-app -n platform
helm rollback my-app 3 -n platform
--atomic rolls back automatically if the upgrade fails. --cleanup-on-fail deletes newly created resources on failure.
Hooks¶
Hooks are Jobs (or Pods) that run at defined release lifecycle points.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5" # lower runs first
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "manage.py", "migrate"]
| Hook | Use case |
|---|---|
pre-install / pre-upgrade |
database migrations, CRD registration |
post-install / post-upgrade |
smoke tests, cache warm-up |
pre-delete |
drain connections, archive data |
test |
helm test triggered checks |
hook-delete-policy: before-hook-creation prevents old hook Jobs from blocking subsequent upgrades. hook-succeeded cleans up passing Jobs automatically.
Helm test¶
Ship test Jobs inside your chart to verify a deployed release:
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "my-app.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox
command: ['wget', '--spider', 'http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/health']
OCI registry support¶
Charts can be stored in OCI registries (Docker Hub, ECR, Artifact Registry, Harbor):
helm package ./my-chart
helm push my-chart-1.4.2.tgz oci://registry.example.com/charts
helm pull oci://registry.example.com/charts/my-chart --version 1.4.2
helm install my-app oci://registry.example.com/charts/my-chart --version 1.4.2
OCI registries don't need helm repo add. Use your existing container registry for chart storage and your existing registry credentials.
Library charts¶
A library chart (type: library in Chart.yaml) exports only named templates - no manifests rendered directly. Use it to share _helpers.tpl patterns across charts in a monorepo.
# Chart.yaml for an application chart
dependencies:
- name: my-lib
version: "0.1.0"
repository: "oci://registry.example.com/charts"
Dependency management¶
helm dependency update ./my-chart # resolve and download to charts/
helm dependency build ./my-chart # rebuild from Chart.lock (CI usage)
Pin dependencies to exact versions or SemVer ranges in Chart.yaml. Use condition: to make sub-charts opt-in via values.
Debugging rendered output¶
helm template my-app ./my-chart -f values-prod.yaml # render locally
helm template my-app ./my-chart -f values-prod.yaml --debug # show computed values
helm get manifest my-app -n platform # what's live in cluster
helm get values my-app -n platform # what values are active
Use the helm-diff plugin in CI to produce a diff between the current release and what helm upgrade would apply. This is the best way to catch unintended template changes before deployment.
Common production mistakes¶
| Mistake | Fix |
|---|---|
Bumping appVersion without bumping version |
Always increment version when chart content changes |
--set in CI without a diff step |
Use helm diff to catch unexpected changes |
Forgetting --atomic on upgrade |
Cluster can end up with partial upgrade if pods fail |
Using latest as the image tag |
Use explicit digest or immutable tag; appVersion should match |
| Storing release secrets in default namespace | Deploy to the namespace you intend; secrets stay there |