Helm Essentials
Helm Essentials Helm is the package manager for Kubernetes. Instead of hand-maintaining a pile of raw YAML manifests for every environment, you package an appli…
Helm Essentials
Helm is the package manager for Kubernetes. Instead of hand-maintaining a pile of raw YAML manifests for every environment, you package an application's manifests into a chart — a versioned, templated bundle — and install, upgrade, or roll it back as a single unit called a release. Helm is the tool most teams reach for the moment kubectl apply -f across a dozen slightly-different YAML files per environment stops scaling.
Chart Structure
A chart is just a directory with a fixed layout. helm create mychart scaffolds one:
mychart/
Chart.yaml # name, version, appVersion, dependencies
values.yaml # default configuration values
charts/ # bundled subcharts (dependencies)
templates/
deployment.yaml
service.yaml
ingress.yaml
_helpers.tpl # reusable named template snippets
NOTES.txt # printed to the user after install/upgrade
.helmignore
# Chart.yaml
apiVersion: v2
name: mychart
description: A Helm chart for my app
version: 1.2.0 # chart version — bump on every change to the chart itself
appVersion: "2.4.0" # version of the application it deploys (informational)
dependencies:
- name: redis
version: "18.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabledThe distinction between version and appVersion trips people up constantly: version is the chart's own release number (SemVer, required); appVersion just documents which version of the underlying app it ships — Helm never parses or enforces it.
Templates & values.yaml
Templates are Kubernetes manifests with Go template syntax layered on top. Values from values.yaml (or -f custom-values.yaml / --set overrides at install time) get injected via {{ .Values.* }}.
# values.yaml
replicaCount: 2
image:
repository: myorg/myapp
tag: "1.4.0"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
ports:
- containerPort: 8080Use helm template . to render the manifests locally without touching a cluster, and helm lint . to catch template and schema mistakes before you ever install. Both should be part of every chart's CI.
Releases: Install, Upgrade, Rollback
Every helm install creates a named release with its own revision history, tracked as Kubernetes Secrets in the target namespace by default. That history is what makes helm rollback possible — it's not magic, Helm just re-applies a prior rendered manifest set.
# First install
helm install myapp ./mychart -f values-prod.yaml --namespace prod --create-namespace
# Idempotent install-or-upgrade — the workhorse command for CI/CD
helm upgrade --install myapp ./mychart -f values-prod.yaml \
--namespace prod \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--atomic --wait --timeout 5m
# Inspect history and roll back if a deploy goes bad
helm history myapp -n prod
helm rollback myapp 3 -n prod
# See what would change without applying anything
helm diff upgrade myapp ./mychart -f values-prod.yaml # requires helm-diff plugin
helm uninstall myapp -n prod`--atomic` rolls the release back automatically if the upgrade fails, and `--wait` blocks until resources report ready — together they turn a Helm upgrade into something closer to a real deployment gate instead of a fire-and-forget `kubectl apply`.
Repositories & Hooks
Charts are distributed through repositories — an index of packaged .tgz charts, often just static files served over HTTP or an OCI registry. Hooks let a chart run one-off jobs at specific points in a release's lifecycle, like a DB migration before a new version of the app comes up.
# Add and use a chart repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/postgresql
helm install my-db bitnami/postgresql --version 13.2.0
# Pull an OCI-based chart (modern registries, e.g. GHCR, ECR)
helm install myapp oci://ghcr.io/myorg/charts/myapp --version 1.2.0
# templates/migration-job.yaml — a pre-upgrade hook
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "0"
"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: ["./manage.py", "migrate"]`hook-weight` controls ordering when multiple hooks share the same event, and `hook-delete-policy` decides whether the Job resource is cleaned up automatically — without it, failed migration Jobs pile up and block the next release from creating a Job with the same name.
Practical Tips & Gotchas
Use
helm upgrade --installin CI, never plainhelm install— the latter fails outright if the release already exists.Helm does not delete resources it no longer manages when you rename or remove a template — orphaned resources need manual cleanup or `helm uninstall` + reinstall.
Secrets should never live in `values.yaml` committed to git — use `--set-file`, a secrets operator (e.g. Sealed Secrets, External Secrets), or `helm-secrets` with SOPS instead.
`--set` values are strings by default and get parsed as YAML — `--set replicas=3` becomes a number, but `--set image.tag=1.0` can surprise you; quote ambiguous values explicitly.
Chart release history accumulates as Secrets in-cluster; `helm history` growing unbounded on high-churn releases is normal but worth pruning with `--history-max` on install.
Prefer named templates in `_helpers.tpl` over copy-pasted label blocks — every resource needing the same `selectorLabels` is a bug waiting to happen if they drift.