Why Helm Exists
The Raw YAML Problem
Imagine deploying the grade-api — a simple REST API with a PostgreSQL database — to three environments: development, staging, and production. Each environment needs different replica counts, different image tags, different resource limits, different database hostnames, and different secret references. With raw YAML you have three options, all bad:
- Copy YAML three times: Drift is inevitable. A critical security fix in prod never makes it to staging.
- Use sed/envsubst: Fragile, hard to test, impossible to track what's deployed where.
- One YAML with all environments: Impossible — the differences are too deep.
Charts, Releases, Repositories
Three concepts underpin everything in Helm:
- Chart: A bundle of YAML templates, a
values.yamlwith defaults, and metadata. Stored as a tarball. Versioned with semver. - Release: A named, running instance of a chart in a cluster. Helm tracks release state in Kubernetes Secrets (in the same namespace). Running
helm listshows all releases. - Repository: An HTTP server (or OCI registry) that indexes and serves chart tarballs. ArtifactHub.io is the public discovery layer.
flowchart LR
REPO[Chart Repository
bitnami, ingress-nginx]
CHART[Chart
nginx-ingress v4.x.x]
VALUES[values.yaml
overrides]
RELEASE1[Release: ingress-prod
namespace: ingress-nginx]
RELEASE2[Release: ingress-dev
namespace: dev]
K8S[Kubernetes Objects
Deployment, Service, CRDs...]
REPO --> CHART
CHART --> RELEASE1
CHART --> RELEASE2
VALUES --> RELEASE1
VALUES --> RELEASE2
RELEASE1 --> K8S
RELEASE2 --> K8S
style REPO fill:#e8f4f4,stroke:#3B9797,color:#132440
style CHART fill:#e8f4f4,stroke:#3B9797,color:#132440
style RELEASE1 fill:#f0f4f8,stroke:#16476A,color:#132440
style RELEASE2 fill:#f0f4f8,stroke:#16476A,color:#132440
style K8S fill:#fff5f5,stroke:#BF092F,color:#132440
Installing Helm
kubectl configured. If you haven't set one up yet, refer to the Minikube or Kind tracks in this series. Helm reads your ~/.kube/config — no extra configuration needed.
macOS
# Install via Homebrew (recommended)
brew install helm
# Verify
helm version
# version.BuildInfo{Version:"v3.15.x", ...}
# Alternative: script install
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Windows
# Install via Chocolatey
choco install kubernetes-helm
# Install via Scoop
scoop install helm
# Install via winget
winget install Helm.Helm
# Verify
helm version
Linux
# Method 1: Script (detects distro automatically)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Method 2: apt (Debian/Ubuntu) — via official signing key
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | \
sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] \
https://baltocdn.com/helm/stable/debian/ all main" | \
sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
# Method 3: Binary release
HELM_VERSION="v3.15.3"
curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | tar xz
sudo mv linux-amd64/helm /usr/local/bin/helm
helm version
Chart Repositories
Adding Repositories
A repository is just an index.yaml file served over HTTP. Helm caches this locally. The most commonly used repos:
# Add popular repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add cert-manager https://charts.jetstack.io
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo add argo https://argoproj.github.io/argo-helm
# Update local cache (like apt-get update)
helm repo update
# List configured repos
helm repo list
# NAME URL
# bitnami https://charts.bitnami.com/bitnami
# ingress-nginx https://kubernetes.github.io/ingress-nginx
# ...
Searching Charts
# Search ArtifactHub (public index — requires internet)
helm search hub postgres
# Search local repo cache
helm search repo postgres
# NAME CHART VERSION APP VERSION
# bitnami/postgresql 16.x.x 16.x.x
# bitnami/postgresql-ha ... ...
# Show all available versions of a chart
helm search repo bitnami/postgresql --versions | head -10
# Inspect chart before installing — see all configurable values
helm show values bitnami/postgresql | head -60
# Inspect chart README
helm show readme bitnami/postgresql | head -30
ArtifactHub vs Private Registries
Public repos like Bitnami are fine for open-source components (PostgreSQL, Redis, nginx). For your own services, most teams push charts to an OCI registry — AWS ECR, GCP Artifact Registry, Azure ACR, or Harbor. OCI support in Helm 3.8+ means you use oci://registry/repo/chart:tag as the chart reference. We cover OCI publishing in Part 4.
First Deploy: nginx Ingress Controller
helm install
Let's deploy the nginx Ingress Controller — a real production-grade workload — to understand how helm install works:
# Create a dedicated namespace
kubectl create namespace ingress-nginx
# Install the chart
# Format: helm install [flags]
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.resources.requests.cpu=100m \
--set controller.resources.requests.memory=128Mi
# What just happened?
# 1. Helm fetched the chart from the ingress-nginx repo
# 2. Rendered templates with your --set overrides
# 3. Applied all resulting manifests to the cluster
# 4. Stored release metadata as a Secret in the namespace
# 5. Waited for resources to become ready (default: no wait)
# Add --wait to block until all resources are ready
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--wait \
--timeout 5m
# NAME: ingress-nginx
# LAST DEPLOYED: Fri Jun 6 10:00:00 2026
# NAMESPACE: ingress-nginx
# STATUS: deployed
# REVISION: 1
Inspecting a Release
# List all releases across all namespaces
helm list -A
# List releases in a specific namespace
helm list -n ingress-nginx
# NAME NAMESPACE REVISION STATUS CHART APP VERSION
# ingress-nginx ingress-nginx 1 deployed ingress-nginx-4.x.x 1.11.x
# Show the computed manifests Helm applied (what's actually running)
helm get manifest ingress-nginx -n ingress-nginx
# Show the values used for this release (includes defaults + your overrides)
helm get values ingress-nginx -n ingress-nginx
# Show all values including defaults
helm get values ingress-nginx -n ingress-nginx --all
# Show release status and notes
helm status ingress-nginx -n ingress-nginx
Deploying grade-api with Helm
Throughout this track we use grade-api as our running example — a REST API that stores student grades in PostgreSQL. In Part 2 we'll write its Helm chart from scratch. For now, let's deploy it using raw values passed to helm install with Bitnami's generic web app pattern, then compare that to the chart-native approach.
The Manual YAML Baseline
This is what we want to stop writing by hand for every environment:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: grade-api
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: grade-api
template:
metadata:
labels:
app: grade-api
version: v1.0.0
spec:
containers:
- name: grade-api
image: ghcr.io/example/grade-api:v1.0.0
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: grade-api-config
key: db.host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: grade-api-secrets
key: db-password
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: grade-api
namespace: default
spec:
selector:
app: grade-api
ports:
- port: 80
targetPort: 8080
That's just two resources. Production grade-api also needs a ConfigMap, a Secret, an HPA, a PodDisruptionBudget, and an Ingress. That's 600+ lines of YAML that varies between environments. Helm makes this a values.yaml change.
Deploying PostgreSQL for grade-api
Let's deploy PostgreSQL using Bitnami's chart — this is exactly how a real project starts:
# Create a values file for grade-api's database
cat > postgres-values.yaml <<'EOF'
auth:
database: grades
username: grade_user
password: "changeme-use-secrets-in-prod"
primary:
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1
memory: 512Mi
persistence:
size: 10Gi
metrics:
enabled: true
EOF
# Deploy PostgreSQL
helm install grade-db bitnami/postgresql \
--namespace grade-api \
--create-namespace \
--values postgres-values.yaml \
--wait
# Verify
kubectl get pods -n grade-api
# NAME READY STATUS RESTARTS AGE
# grade-db-postgresql-0 2/2 Running 0 45s
# Connect to verify (optional)
kubectl run pg-client --rm -it --restart=Never \
--image=bitnami/postgresql:16 \
--namespace grade-api \
-- psql -h grade-db-postgresql -U grade_user -d grades
Upgrade & Rollback
helm upgrade --install
helm upgrade --install is the idempotent pattern you'll use in CI/CD pipelines — it installs if the release doesn't exist, upgrades if it does. This is the command for automated deployments.
# Upgrade to a new chart version
helm upgrade grade-db bitnami/postgresql \
--namespace grade-api \
--values postgres-values.yaml \
--set primary.resources.limits.memory=1Gi \
--wait
# Idempotent install-or-upgrade (CI/CD pattern)
helm upgrade --install grade-db bitnami/postgresql \
--namespace grade-api \
--create-namespace \
--values postgres-values.yaml \
--wait \
--atomic # Roll back automatically if upgrade fails
# Pin to a specific chart version (important for reproducibility!)
helm upgrade --install grade-db bitnami/postgresql \
--namespace grade-api \
--version 16.2.3 \
--values postgres-values.yaml
# Dry run — see what would change without applying
helm upgrade grade-db bitnami/postgresql \
--namespace grade-api \
--values postgres-values.yaml \
--dry-run
helm upgrade ... bitnami/postgresql without --version pulls the latest chart version, which may introduce breaking changes. Use --version X.Y.Z and update deliberately. Track chart versions in your Git repository alongside your values.yaml.
helm rollback
# View release history (Helm stores 10 revisions by default)
helm history grade-db -n grade-api
# REVISION STATUS CHART DESCRIPTION
# 1 superseded postgresql-16.2.0 Install complete
# 2 superseded postgresql-16.2.3 Upgrade complete
# 3 deployed postgresql-16.2.3 Upgrade complete
# Roll back to a specific revision
helm rollback grade-db 1 -n grade-api
# Roll back to previous revision (shorthand)
helm rollback grade-db -n grade-api
# Rollback with wait
helm rollback grade-db 1 -n grade-api --wait
# After rollback, history shows the new revision
helm history grade-db -n grade-api
# REVISION STATUS CHART DESCRIPTION
# 1 superseded postgresql-16.2.0 Install complete
# 2 superseded postgresql-16.2.3 Upgrade complete
# 3 superseded postgresql-16.2.3 Upgrade complete
# 4 deployed postgresql-16.2.0 Rollback to 1
Rollback Doesn't Roll Back Data
A common misconception: helm rollback rolls back Kubernetes object definitions (Deployments, ConfigMaps, Services) but does not roll back PersistentVolume data. If a database migration ran during an upgrade and you roll back the chart, the schema migration is still there. Always design schema changes to be backward-compatible and plan database rollbacks separately from Helm rollbacks.
Release History & Secrets
Helm stores each release revision as a Kubernetes Secret in the release's namespace. This means release history survives pod restarts — it's stored in etcd, not Helm's local state:
# Release history is stored as Secrets
kubectl get secrets -n grade-api -l owner=helm
# NAME TYPE DATA AGE
# sh.helm.release.v1.grade-db.v1 helm.sh/release.v1 1 5m
# sh.helm.release.v1.grade-db.v2 helm.sh/release.v1 1 3m
# sh.helm.release.v1.grade-db.v3 helm.sh/release.v1 1 1m
# Control history size (saves space in etcd)
helm upgrade grade-db bitnami/postgresql \
--namespace grade-api \
--history-max 5 \
--values postgres-values.yaml
Release Lifecycle Commands
A quick reference for the complete Helm release lifecycle:
# ── DISCOVERY ──────────────────────────────────────────────
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/postgresql --versions
helm show values bitnami/postgresql
# ── INSTALL ────────────────────────────────────────────────
helm install my-release chart/name \
--namespace my-ns \
--create-namespace \
--values values.yaml \
--set key=value \
--version 1.2.3 \
--wait --timeout 5m
# ── INSPECT ────────────────────────────────────────────────
helm list -n my-ns
helm status my-release -n my-ns
helm get values my-release -n my-ns
helm get manifest my-release -n my-ns
helm history my-release -n my-ns
# ── UPGRADE ────────────────────────────────────────────────
helm upgrade --install my-release chart/name \
--namespace my-ns \
--values values.yaml \
--atomic --wait
# ── ROLLBACK ───────────────────────────────────────────────
helm rollback my-release [REVISION] -n my-ns --wait
# ── UNINSTALL ──────────────────────────────────────────────
helm uninstall my-release -n my-ns
# Keeps history by default; use --keep-history to retain Secrets
Exercises
cache namespace using helm install. Override the replica count to 1. Verify with kubectl get pods -n cache. Then upgrade to enable persistence (--set master.persistence.enabled=true) and observe helm history.
--set replicaCount=not-a-number). Observe the failure. Roll back to revision 1. Confirm the rollback with helm status and kubectl get pods.
helm upgrade --install --atomic --wait to deploy PostgreSQL. Run it twice — the first run installs, the second run is a no-op (same values). Verify that helm history shows revision 2 as "Upgrade complete" with no actual changes. This is the idempotent pattern all CI pipelines should use.
helm diff plugin (helm plugin install https://github.com/databus23/helm-diff). Run helm diff upgrade on an existing release with a minor values change. Understand the output before applying. This is essential for safe production upgrades.
Key Takeaways & Next Steps
- Helm solves the multi-environment YAML duplication problem through parameterised charts and named releases
helm upgrade --install --atomic --waitis the correct idempotent CI/CD pattern- Always pin chart versions with
--versionin production — never leave it floating - Release history lives in Kubernetes Secrets — it survives pod restarts and cluster redeployments
helm rollbackrolls back object definitions, not PersistentVolume data- Use
helm diffplugin before any production upgrade
Next in This Track
In Part 2: Chart Structure & Templates, we write the grade-api Helm chart from scratch — directory layout, Chart.yaml, values.yaml, Go template syntax, and the built-in objects that make Helm charts powerful.