Helm is the package manager for Kubernetes. It bundles a set of manifests into a versioned, parameterised chart, installs it as a named release, and tracks enough history to upgrade and roll back.
Its two jobs are genuinely different, and it is worth being clear which one you need: consuming third-party software, and packaging your own.
| Term | Meaning |
|---|---|
| Chart | The package: templates plus default values plus metadata |
| Release | One installed instance of a chart, with a name and a version history |
| Repository | A place charts are published and fetched from |
The chart/release distinction matters: one chart can be installed many times in the same cluster under different names, each with its own values and its own history.
Installing other people's software. This is where Helm is uncontested. Prometheus, ingress-nginx, cert-manager, Grafana and almost everything else in the ecosystem ships as a chart. Installing Prometheus by hand means dozens of interdependent manifests with CRDs, RBAC and service discovery configuration:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespacePackaging your own applications. Here it is a real trade-off. You gain one chart serving every environment through values files; you pay in Go templating, which is genuinely hard to read once it grows conditionals and helper functions.
Helm 2 ran a cluster-side component called Tiller that needed broad permissions and became a well-known security problem. Helm 3 is client-side only. It talks to the Kubernetes API with your own kubeconfig credentials, so it can do exactly what you can do, no more.
Release state is stored as Secrets in the release's namespace, which is why helm list is namespace-scoped and why helm list -A exists.
# Find and inspect
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo nginx
helm show values bitnami/nginx > values.yaml
# Install
helm install my-nginx bitnami/nginx -f values.yaml
# Observe
helm list
helm status my-nginx
# Change
helm upgrade my-nginx bitnami/nginx -f values.yaml --atomic
# Undo
helm rollback my-nginx 1
# Remove
helm uninstall my-nginxhelm listNAME NAMESPACE REVISION STATUS CHART APP VERSION
my-nginx default 3 deployed nginx-18.2.1 1.27.2
Two habits worth forming early:
helm show values <chart> > values.yaml before installing anything. Charts have dozens or hundreds of settings and the defaults are rarely what you want in production. Resource limits, replica counts and persistence settings especially.
--atomic on every upgrade. Without it a failed upgrade leaves the release half-applied. With it, Helm rolls back automatically. Pair it with --timeout so it does not wait forever.
This surprises people. Helm does not diff against the live cluster by default. It computes a three-way merge between the old release manifest, the new one, and the current cluster state, then applies the result.
Two practical consequences:
Manual kubectl edit changes may be preserved or clobbered depending on the field, which is why mixing Helm with hand edits produces confusing results. Pick one.
Removing a resource from a chart does delete it on upgrade, because Helm tracks what it created. That is usually what you want, and occasionally alarming.
Preview before applying:
helm diff upgrade my-nginx bitnami/nginx -f values.yaml # requires helm-diff plugin
helm upgrade my-nginx bitnami/nginx -f values.yaml --dry-runThe helm-diff plugin is the single most useful thing you can add to Helm. Install it.
CRDs. Custom Resource Definitions in a chart's crds/ directory are installed once and never upgraded or deleted by Helm. Upgrading a chart that changed its CRDs requires applying them yourself. This causes real, confusing failures.
Templating readability. A chart with nested conditionals and _helpers.tpl indirection is hard to reason about. helm template to see the rendered output is often the only way to be sure.
Rollback is not a time machine. helm rollback reverts Kubernetes objects. It does not undo a database migration, and it will not restore data deleted by a PVC that the upgrade removed.
| Helm | Kustomize | |
|---|---|---|
| Approach | Templating with variables | Patching plain YAML |
| Readability | Harder as charts grow | Base manifests stay valid YAML |
| Distribution | Repositories, versioned packages | No packaging story |
| Release tracking | Built in, with rollback | None (relies on Git) |
| Built into kubectl | No | Yes, kubectl apply -k |
| Ecosystem | Everything ships as a chart | Mostly internal use |
The pragmatic answer most teams land on: Helm for third-party software, Kustomize for your own manifests. You get the ecosystem without writing Go templates for your own services, and your manifests stay readable YAML. ArgoCD and Flux both support either, and both support Kustomize overlays on top of a rendered Helm chart, which is a genuinely good pattern.
| Sub-topic | What you will learn |
|---|---|
| Installing Helm | Install Helm and use the repo and release commands |
| Understanding Charts | Chart structure, templates and built-in objects |
| Values and Upgrades | Override configuration, upgrade and roll back safely |
| Task | Command |
|---|---|
| Add a repository | helm repo add <name> <url> |
| Search | helm search repo <term> |
| Read a chart's settings | helm show values <chart> |
| Install | helm install <release> <chart> -f values.yaml |
| Install or upgrade | helm upgrade --install <release> <chart> |
| Releases everywhere | helm list -A |
| History | helm history <release> |
| Roll back | helm rollback <release> <revision> |
| Render without installing | helm template <release> <chart> |
| Uninstall | helm uninstall <release> |
helm show values before installing anything. Chart defaults are rarely production-appropriate.--atomic so a failure rolls back instead of leaving a half-applied release.crds/ are never upgraded by Helm. Apply CRD changes yourself.helm rollback reverts Kubernetes objects only: not migrations, not deleted data.<MCQExercise exercise={{ id: 'k8s-helm-crd-upgrade', title: 'A chart upgrade that half worked', description: 'You upgrade an operator chart to a version whose custom resources have new fields. The controller starts, but applying a resource with the new fields fails validation. What is the most likely cause?', options: [ { id: 'a', text: 'Helm never upgrades CRDs in a chart's crds/ directory, so the old CRD schema is still installed' }, { id: 'b', text: 'The upgrade needs --force to replace existing resources' }, { id: 'c', text: 'The release must be uninstalled and reinstalled for any schema change' }, { id: 'd', text: 'CRDs require a cluster restart before new fields are recognised' }, ], correctId: 'a', explanation: 'Helm installs CRDs from crds/ exactly once and deliberately never touches them again on upgrade or uninstall, because a botched CRD change can destroy every custom resource in the cluster. The consequence is that a chart upgrade can ship a controller expecting a schema the cluster does not have. Apply the new CRDs yourself with kubectl before or alongside the upgrade.', wrongExplanations: { b: '--force replaces resources Helm manages. CRDs from crds/ are outside that set, so it changes nothing here and risks disruptive replacement elsewhere.', c: 'Uninstalling would not help and is dangerous. Removing an operator can cascade to the custom resources it manages. The CRD is the only thing that needs updating.', d: 'CRDs take effect as soon as the API server accepts them. There is nothing to restart.', }, }} />
<MCQExercise exercise={{ id: 'k8s-helm-atomic', title: 'A stuck release', description: 'A helm upgrade fails partway through. The release shows status failed and some resources are on the new version while others are on the old. Which flag would have prevented this?', options: [ { id: 'a', text: '--atomic, which rolls the release back automatically if the upgrade does not succeed' }, { id: 'b', text: '--dry-run, which validates the upgrade before applying it' }, { id: 'c', text: '--force, which replaces resources instead of patching them' }, { id: 'd', text: '--reuse-values, which keeps the previous values file' }, ], correctId: 'a', explanation: '--atomic makes the upgrade all-or-nothing: if any resource fails to become ready within the timeout, Helm reverts the whole release to the previous revision. It implies --wait, so pair it with a sensible --timeout or it will wait on a genuinely stuck rollout longer than you want.', wrongExplanations: { b: '--dry-run renders and validates without applying anything, so it catches template and schema errors but not runtime failures like a crashing pod or an unschedulable replica.', c: '--force deletes and recreates resources rather than patching them, which causes downtime and makes a partial failure more disruptive rather than less.', d: '--reuse-values controls which values are carried forward. It has no bearing on what happens when the upgrade fails.', }, }} />
A chart is the package: templates, default values and metadata, versioned and distributable. A release is one installed instance of that chart in a cluster, with its own name, its own values and its own revision history. The same chart can be installed many times under different names, which is how you run several instances of the same application side by side. helm list shows releases, not charts.
Helm uses Go templating with variables substituted at render time, and adds packaging, distribution and release tracking with rollback. Kustomize patches plain YAML through overlays, so base manifests remain valid Kubernetes YAML you can apply directly, and it is built into kubectl via apply -k. Helm is stronger for distributing software to other people and for managing installed releases; Kustomize is easier to read and reason about for your own manifests. Using both, Helm for third-party charts, Kustomize for internal services, is common and works well.
By design. CRDs placed in a chart's crds/ directory are installed once on first install and never upgraded or deleted afterwards, because an incorrect CRD change can invalidate or destroy every custom resource of that type in the cluster. Helm takes the view that this is too destructive to automate. When a chart upgrade includes CRD changes, apply them yourself with kubectl apply -f before running the upgrade. The chart's release notes normally say so, and skipping it produces validation errors that look unrelated.
helm history <release> lists the revisions, then helm rollback <release> <revision> reverts to one. Helm stores each revision's manifest as a Secret in the release namespace, so rollback simply reapplies an earlier state. Two caveats: it only reverts Kubernetes objects, so a database migration run by the bad release still needs handling separately, and data lost through a deleted PersistentVolumeClaim is not coming back. Upgrading with --atomic avoids most rollbacks by reverting automatically on failure.
Helm 3 is safe in a way Helm 2 was not. Helm 2 ran a cluster-side component called Tiller that typically held broad cluster-admin permissions and was a well-documented privilege escalation risk. Helm 3 removed it entirely and runs client-side, authenticating with your own kubeconfig, so it can only do what you are already permitted to do. Release state lives in Secrets in the release's namespace rather than in a privileged service. Any guide that mentions helm init or Tiller is describing Helm 2 and is out of date.
Monitor Kubernetes with metrics-server, kubectl top, Prometheus and kube-state-metrics. Learn what to watch, which alerts matter, and how logs fit in.
Learn what Kubernetes is and what problem it solves, how it differs from Docker and Docker Compose, the core objects to know, and whether you really need it.
Understand Kubernetes architecture: API server, etcd, scheduler, controller manager, kubelet and kube-proxy, and what happens when you run kubectl apply.