Guide

Helm Kubernetes explained

Helm is the de facto package manager for Kubernetes. Instead of hand-applying dozens of YAML files per environment, you bundle manifests into a versioned chart, parameterize them with values, and install releases that Helm tracks for upgrade and rollback. Whether you are deploying the official ingress-nginx chart, packaging your own microservice stack, or wiring a CI/CD pipeline to promote staging values into production, Helm sits between raw kubectl and full GitOps controllers. This guide covers Helm 3 architecture (no cluster-side Tiller), chart anatomy and Go templating, values layering, repositories, lifecycle commands, hooks and dependencies, a Harbor Fleet multi-service worked example, a Helm vs Kustomize vs operators decision table, common pitfalls, and a production checklist. For cluster primitives Helm renders into, start with Kubernetes fundamentals.

What problem Helm solves

A typical production service on Kubernetes needs a Deployment, Service, Ingress, ConfigMap, Secret references, HorizontalPodAutoscaler, PodDisruptionBudget, and maybe a ServiceMonitor for Prometheus. Copy-pasting YAML across dev, staging, and prod invites drift: one environment forgets a label selector, another hard-codes an image tag. Helm centralizes those manifests in a chart and injects environment-specific values at render time.

Helm also records release history. An helm upgrade creates a new revision; helm rollback re-applies a prior revision without hunting through git. That operational memory is why platform teams reach for Helm before they adopt Argo CD or Flux — though GitOps tools often use Helm charts under the hood.

Helm 3 vs the old Tiller model

Helm 2 ran a cluster-admin Tiller pod that applied resources — a security liability on multi-tenant clusters. Helm 3 removed Tiller; the CLI talks directly to the Kubernetes API using your kubeconfig credentials. Release metadata lives in Secrets (or ConfigMaps) in the release namespace. If you encounter Tiller references in old blog posts, ignore them.

Charts, templates, and values

A chart is a directory (or packaged .tgz) with a predictable layout:

  • Chart.yaml — name, version, appVersion, dependencies.
  • values.yaml — default parameters consumers override.
  • templates/ — Kubernetes manifests with Go-template placeholders.
  • templates/_helpers.tpl — reusable named templates (labels, names).
  • templates/NOTES.txt — post-install hints shown to the operator.

Templates use expressions like {{ .Values.replicaCount }} and control flow (if, range, with). Helm renders them into plain YAML, then applies the result. The helm template command prints rendered manifests without touching the cluster — essential for code review and CI validation.

Values layering

Override defaults in order of precedence (lowest to highest): chart values.yaml, parent chart values, user-supplied -f my-values.yaml files, and --set key=value flags. Production patterns keep a values-base.yaml plus values-staging.yaml / values-prod.yaml that only differ on image tags, replica counts, and ingress hosts. Never commit secrets in values files checked into git — inject them via secrets management (External Secrets Operator, Sealed Secrets, or CI-injected --set).

Releases and revisions

Installing my-api from chart harbor-api-1.4.2.tgz creates a release named my-api at revision 1. Each upgrade bumps the revision while keeping the release name. List history with helm history my-api; rollback with helm rollback my-api 3. Uninstall removes resources defined in the chart (unless annotated to keep resources on delete).

Repositories, dependencies, and hooks

Chart repositories are HTTP servers (or OCI registries) hosting packaged charts. Add one with helm repo add bitnami https://charts.bitnami.com/bitnami, then helm search repo nginx. The Artifact Hub index catalogs thousands of community charts — vet them for maintenance and security before production use.

Subcharts and dependencies

Declare dependencies in Chart.yaml under dependencies:. Running helm dependency update vendors subcharts into charts/. A common pattern: your application chart depends on the official postgresql or redis subchart for local dev, while production points values at a managed database instead. Pass values to subcharts with a key matching the dependency name: postgresql.auth.password: ....

Lifecycle hooks

Annotate resources with helm.sh/hook: pre-install,post-upgrade (among others) to run Jobs before or after the main manifest apply — database migrations, cache warming, smoke tests. Hooks run in weighted order; failed hooks abort the release. Use hooks sparingly: they complicate rollback and GitOps reconciliation. Prefer running migrations from CI or a dedicated Job outside Helm when possible.

Worked example: Harbor Fleet platform chart

Harbor Fleet runs five stateless APIs (payments, catalog, support, courier, analytics) behind a shared ingress controller on one Kubernetes cluster. Without Helm, each service duplicated Deployment/Service/Ingress YAML with copy-paste labels. The platform team built an harbor-service chart parameterized by:

  • image.repository / image.tag — per-service container.
  • ingress.hostpayments.harbor.internal etc.
  • resources — CPU/memory requests tuned per workload.
  • autoscaling.enabled and autoscaling.maxReplicas — ties into HPA templates.
  • serviceMonitor.enabled — optional Prometheus scrape config.

They install five releases from the same chart version with different values files:

helm upgrade --install payments ./harbor-service \
  -f values-base.yaml -f values-payments.yaml \
  --namespace harbor --create-namespace

helm upgrade --install catalog ./harbor-service \
  -f values-base.yaml -f values-catalog.yaml \
  --namespace harbor

A platform harbor-platform umbrella chart declares dependencies on ingress-nginx, cert-manager, and harbor-service (aliased five times). One helm upgrade on the umbrella upgrades shared infra and all apps with locked dependency versions. CI runs helm lint and helm template on every pull request; CD promotes the same chart tarball from staging to prod, changing only values-prod.yaml image tags after integration tests pass.

Helm vs alternatives — decision table

Approach Best for Trade-offs
Raw YAML + kubectl One-off demos, learning K8s primitives No parameterization, no release history, environment drift
Helm charts Packaged apps, third-party installs, multi-env parameterization Go-template complexity, hook edge cases, chart versioning discipline
Kustomize overlays Patches on base manifests without templating language Weaker packaging/distribution story; no built-in release rollback
GitOps (Argo CD / Flux) Continuous reconcile from git, audit trail, multi-cluster Operational overhead; often wraps Helm or Kustomize anyway
Operators / CRDs Stateful systems needing domain-specific lifecycle (databases, Kafka) High build cost; overkill for stateless HTTP services

Many teams use Helm for packaging and GitOps for delivery: chart version bumps merge to git; Argo CD syncs the rendered release. That split keeps templating in Helm and auditability in git.

Common pitfalls

  • Secrets in values.yaml committed to git — use External Secrets or CI-injected overrides; rotate anything ever leaked.
  • Non-deterministic templatesrandAlphaNum in templates creates new resources every upgrade; set explicit names.
  • Hook Jobs that never complete — stuck pre-upgrade hooks block all future upgrades until manually deleted.
  • Chart version vs app version confusion — bump version in Chart.yaml on every chart change; appVersion tracks the container image.
  • Blind helm upgrade --force — recreates resources and can cause brief outages; understand before using.
  • Untested subchart upgrades — dependency version bumps can change default security contexts or labels; diff rendered YAML in CI.
  • Cluster-scoped resources in app charts — ClusterRoles and CRDs belong in platform charts installed once, not per microservice release.

Production checklist

  • Pin chart and dependency versions; avoid floating latest tags in production values.
  • Run helm lint and helm template in CI on every chart change.
  • Store environment values in git (minus secrets); tag releases that match deployed revisions.
  • Document rollback runbooks: helm history, helm rollback, and when to fix-forward instead.
  • Use --atomic or --wait in CD for critical services so failed upgrades auto-rollback.
  • Namespace releases per team or environment; restrict RBAC so CI service accounts cannot helm-install cluster-wide.
  • Sign charts (Helm provenance) or pull from trusted OCI registries with digest pinning.
  • Keep NOTES.txt accurate — onboarding engineers should know the first kubectl command after install.
  • Align chart labels with your observability stack (standard app.kubernetes.io/* labels).
  • Rehearse disaster recovery: reinstall from chart + values, not from memory.

Key takeaways

  • Helm packages Kubernetes manifests into versioned charts with templated values and tracked release history.
  • Helm 3 applies directly via the API — no Tiller — with rollback via numbered revisions.
  • Layer values files per environment; never commit plaintext secrets.
  • Subcharts and repositories distribute reusable platform and application packages.
  • Helm pairs naturally with GitOps and CI/CD; it complements rather than replaces Kustomize or operators.

Related reading