What Helm is: charts & releases
The package manager for Kubernetes.
On your laptop you don't install software by dragging files into the right folders one at a time. You run one command, apt or brew or winget, and it fetches the correct version, puts every piece where it belongs, writes down what it did, and lets you upgrade or remove it cleanly later. Kubernetes (the system that runs and schedules your containers across a fleet of machines) had no such command for years. Helm is that command: the package manager for Kubernetes.
Running an app on Kubernetes means describing it in files called manifests, written in YAML (a plain-text format for configuration). A Deployment to run the pods, a Service so other things can reach them, a ConfigMap for settings, maybe an Ingress for outside traffic, plus some RBAC (Role-Based Access Control, the rules that decide which accounts may do what). One small app is easily a dozen files. Now run that same app in three places, dev, staging, and production, each with a different replica count, image tag, and hostname. You end up copy-pasting the whole set and editing the same three lines in each copy. It drifts. Someone patches a bug in staging and forgets production. Helm exists to kill that copy-paste.
The chart is the package
A chart is a folder of templates plus a file of default values, bundled together and given a version number. Think of a boxed cake mix. The box (the chart) holds a recipe with blanks in it (the templates) and a default list of ingredients (values.yaml). You fill in the blanks you care about at install time, and out comes a finished cake (plain Kubernetes manifests). Helm can scaffold an empty one for you so you can see the shape:
helm create demo
Creating demo
tree demo
demo├── Chart.yaml├── charts├── templates│ ├── NOTES.txt│ ├── _helpers.tpl│ ├── deployment.yaml│ ├── hpa.yaml│ ├── ingress.yaml│ ├── service.yaml│ ├── serviceaccount.yaml│ └── tests│ └── test-connection.yaml└── values.yaml3 directories, 10 files
Two files carry the identity of the package. Chart.yaml is the label on the box. It holds the chart's own version and, separately, the version of the app inside. Those are not the same thing, and mixing them up bites people during upgrades.
apiVersion: v2name: demodescription: A Helm chart for Kubernetestype: applicationversion: 0.1.0 # the chart's own version. Bump this when you change the chart.appVersion: "1.16.0" # the version of the app the chart installs. Informational only.
The templates folder is the recipe with blanks. Inside a template, anything wrapped in double curly braces is a placeholder Helm fills in. Here is the line from the scaffolded Deployment that sets how many copies to run and which image to pull:
spec:replicas: {{ .Values.replicaCount }}# ...containers:- name: {{ .Chart.Name }}image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
Where do those .Values come from? From values.yaml, the default ingredient list. The scaffold ships with sane defaults, and you override the ones you need per environment with -f myvalues.yaml or a quick --set on the command line.
replicaCount: 1image:repository: nginxpullPolicy: IfNotPresenttag: ""
Rendering one chart into many environments
Filling the blanks is called rendering. It is the whole point of templating: one parameterized chart produces different, correct manifests depending on the values you feed it. The command that renders a chart without touching your cluster is helm template. It reads the templates, applies your values, and prints the finished YAML to your screen. Watch a value flow through:
helm template demo ./demo --set replicaCount=3 | grep replicas
replicas: 3
You asked for three, the rendered Deployment says three. That same command is also the single most useful security habit in Helm, and we come back to it below. It renders locally and applies nothing, so you can read exactly what a chart would create before it gets anywhere near your cluster.
Installing produces a release
Rendering gives you manifests. Applying them is a separate step, and when you do it through Helm, the result is a release: one named, tracked, running instance of a chart in a cluster. A release is like a shipment with a tracking number. Helm gives it a name, records every change to it as a numbered revision, and keeps that history so you can go backward. Let's install a real, widely used chart, the NGINX ingress controller. First point Helm at the repository that hosts it, then pull the latest index:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo update
"ingress-nginx" has been added to your repositoriesHang tight while we grab the latest from your chart repositories......Successfully got an update from the "ingress-nginx" chart repositoryUpdate Complete. ⎈Happy Helming!⎈
helm install web ingress-nginx/ingress-nginx \--version 4.11.3 \--namespace ingress --create-namespace \--set controller.replicaCount=2
NAME: webLAST DEPLOYED: Fri Jul 17 10:42:03 2026NAMESPACE: ingressSTATUS: deployedREVISION: 1TEST SUITE: NoneNOTES:The ingress-nginx controller has been installed.It may take a few minutes for the load balancer IP to be available....
Notice the name we chose (web) and REVISION: 1. That is the release. You can list what's installed at any time, and unlike a wall of kubectl output, this tells you the release, its chart version, and its health in one line:
helm list -n ingress
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSIONweb ingress 1 2026-07-17 10:42:03 +0000 UTC deployed ingress-nginx-4.11.3 1.11.3
Now change something. Upgrade the release to run four controller pods instead of two. Helm renders the chart again with the new value, applies the difference, and stamps it revision 2:
helm upgrade web ingress-nginx/ingress-nginx \--version 4.11.3 -n ingress \--set controller.replicaCount=4
Release "web" has been upgraded. Happy Helming!NAME: webLAST DEPLOYED: Fri Jul 17 11:05:19 2026NAMESPACE: ingressSTATUS: deployedREVISION: 2
Say revision 2 misbehaves. You don't hand-edit anything back. You roll the release to a known-good revision, and Helm re-applies the exact state that revision held. A rollback is itself recorded as a new revision, so the ledger stays honest:
helm rollback web 1 -n ingresshelm history web -n ingress
Rollback was a success! Happy Helming!REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION1 Fri Jul 17 10:42:03 2026 superseded ingress-nginx-4.11.3 1.11.3 Install complete2 Fri Jul 17 11:05:19 2026 superseded ingress-nginx-4.11.3 1.11.3 Upgrade complete3 Fri Jul 17 11:12:44 2026 deployed ingress-nginx-4.11.3 1.11.3 Rollback to 1
So Helm gives you three jobs in one tool. Templating (one parameterized chart renders correct manifests per environment). Packaging (a chart is a shareable, versioned unit, the way an apt package or an npm module is). And lifecycle management (install, upgrade, roll back, and uninstall as tracked operations, not ad-hoc edits).
Where the release actually lives
That numbered history has to be stored somewhere. It is not a file on your laptop. Helm keeps each revision inside the cluster, as an ordinary Kubernetes Secret in the release's namespace. You can see them directly:
kubectl get secret -n ingress -l owner=helm
NAME TYPE DATA AGEsh.helm.release.v1.web.v1 helm.sh/release.v1 1 30msh.helm.release.v1.web.v2 helm.sh/release.v1 1 3msh.helm.release.v1.web.v3 helm.sh/release.v1 1 1m
For a defender this cuts two ways. The good side: Helm has no hidden brain. The complete record of what it deployed is standard Kubernetes objects you can audit with kubectl and guard with your normal RBAC. The caution: those Secrets contain the rendered manifests and the values that produced them. Anyone who can read Secrets in that namespace can read a release's configuration, so treat Secret-read on production namespaces as a sensitive grant, not a routine one.
Helm v3 removed the big attack surface
There is a reason the modern tool talks straight to the cluster. Older Helm (version 2) shipped a server-side component called Tiller that ran inside your cluster. Tiller was a concierge living in the building who held a master key. You handed it your requests and it applied them using its own permissions, which were usually cluster-admin (the built-in role with full control over everything). Anyone who could reach Tiller could ask it to do anything, and its default setup often had weak authentication or none. That was a real hole, and it got exploited. Helm version 3 deleted Tiller. The helm command now speaks directly to the Kubernetes API (the cluster's control endpoint) using your kubeconfig (the file that holds your cluster address and your credentials). Check which version you are running before you trust any of the above:
helm version
version.BuildInfo{Version:"v3.17.2", GitCommit:"cc0b4b16f3d84b6bdb5b8df2b5c4b7e3a7a8e910", GitTreeState:"clean", GoVersion:"go1.23.7"}
If that ever reports a v2, stop and upgrade before you do anything else. The flip side of "Helm acts as you" is the part people skip past: a chart can do anything your account is allowed to do. Nothing sandboxes it.
In practice the inspection is one pipe. Render the chart and filter for the objects that grant power across the whole cluster:
helm template web ingress-nginx/ingress-nginx --version 4.11.3 \| grep -iE 'kind: (ClusterRole|ClusterRoleBinding)|privileged:|hostPath'
kind: ClusterRolekind: ClusterRoleBinding
An ingress controller genuinely needs cluster-wide read access to Ingress objects, so a ClusterRole here is expected and fine. The win is that you looked. Once you can tell an expected grant from a suspicious one, a hostile chart trying to mount the host disk or bind itself to cluster-admin stops being invisible.
Helm and its main alternative
Helm is how most third-party software for Kubernetes is distributed. You install ingress-nginx, Prometheus, or cert-manager from their official charts instead of copying YAML off a docs page and hoping it matches your version. It is equally good for packaging your own apps. The main alternative is Kustomize (built into kubectl), which skips templating entirely: you write plain manifests and layer patches on top for each environment. Plenty of teams run both, Kustomize for simple environment overlays and Helm for packaged software they install or ship. You will meet Kustomize in its own course. Either way, the moment you operate a real cluster you will be reading helm list output and someone's chart, so knowing what a release is and where its state lives is table stakes.
helm install for a third-party chart nobody on the team has read. Your kubeconfig has broad rights on the cluster. What is the honest description of the risk?demo/Chart.yaml carries version: 0.1.0 and appVersion: "1.16.0". You fix a typo in the chart's own templates and change nothing else. Which field should move?web is on revision 2 after an upgrade and revision 2 is misbehaving. You run helm rollback web 1 -n ingress, then helm history web -n ingress. What does the history show?Make the version check and the helm template render a reflex. Run helm version so you know you are on v3, and pipe any unfamiliar chart through helm template and read it before it touches a cluster you care about.
Try this
Run helm create demo on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: a chart applies arbitrary manifests with your credentials. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.