CoursesHelmWhat Helm is: charts & releases

What Helm is: charts & releases

The package manager for Kubernetes.

Intermediate12 min · lesson 1 of 12

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:

terminal
helm create demo
output
Creating demo
terminal
tree demo
output
demo
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
3 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.

demo/Chart.yaml
apiVersion: v2
name: demo
description: A Helm chart for Kubernetes
type: application
version: 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:

demo/templates/deployment.yaml
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.

demo/values.yaml
replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
tag: ""

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:

terminal
helm template demo ./demo --set replicaCount=3 | grep replicas
output
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:

terminal
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
output
"ingress-nginx" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "ingress-nginx" chart repository
Update Complete. ⎈Happy Helming!⎈
terminal
helm install web ingress-nginx/ingress-nginx \
--version 4.11.3 \
--namespace ingress --create-namespace \
--set controller.replicaCount=2
output
NAME: web
LAST DEPLOYED: Fri Jul 17 10:42:03 2026
NAMESPACE: ingress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
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:

terminal
helm list -n ingress
output
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
web 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:

terminal
helm upgrade web ingress-nginx/ingress-nginx \
--version 4.11.3 -n ingress \
--set controller.replicaCount=4
output
Release "web" has been upgraded. Happy Helming!
NAME: web
LAST DEPLOYED: Fri Jul 17 11:05:19 2026
NAMESPACE: ingress
STATUS: deployed
REVISION: 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:

terminal
helm rollback web 1 -n ingress
helm history web -n ingress
output
Rollback was a success! Happy Helming!
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Jul 17 10:42:03 2026 superseded ingress-nginx-4.11.3 1.11.3 Install complete
2 Fri Jul 17 11:05:19 2026 superseded ingress-nginx-4.11.3 1.11.3 Upgrade complete
3 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).

How an install becomes a release
1Chart
templates + default values, versioned
2helm install web
your values merged on top of the defaults
3Render
templates become plain manifests, on your machine
4Kubernetes API
applied with your kubeconfig, as you
5Release recorded
objects run; state saved as a Secret, revision 1

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:

terminal
kubectl get secret -n ingress -l owner=helm
output
NAME TYPE DATA AGE
sh.helm.release.v1.web.v1 helm.sh/release.v1 1 30m
sh.helm.release.v1.web.v2 helm.sh/release.v1 1 3m
sh.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:

terminal
helm version
output
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.

A chart applies arbitrary manifests with your credentials
helm install renders whatever the chart's templates say and applies it to the cluster as you. A chart can create a ClusterRoleBinding that grants itself cluster-admin, install CRDs (Custom Resource Definitions, extensions that teach the cluster new object types), or run a privileged pod that mounts the host's filesystem, and all of it succeeds if your account is permitted to do those things. Popular does not mean safe. Before installing anything unfamiliar: render it with helm template, actually read what it creates (grep for ClusterRole, hostPath, and privileged), pin an exact --version so you get the bytes you reviewed, and prefer charts you can verify by signature (covered in the provenance lesson).

In practice the inspection is one pipe. Render the chart and filter for the objects that grant power across the whole cluster:

terminal
helm template web ingress-nginx/ingress-nginx --version 4.11.3 \
| grep -iE 'kind: (ClusterRole|ClusterRoleBinding)|privileged:|hostPath'
output
kind: ClusterRole
kind: 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.

Quick check
01A teammate asks you to run 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?
Incorrect — The namespace flag sets a default target, not a fence. Templates are free to render cluster-scoped objects that sit outside any namespace at all.
Incorrect — That was Tiller, the v2 server component with usually cluster-admin power. v3 deleted it, and the local helm binary now talks to the API as you.
Correct — Helm acts as you. That is why the fix is reading the render first with helm template, not trusting the chart's popularity.
Incorrect — values.yaml carries settings. The templates decide which objects exist, and they can emit ClusterRoleBindings or CRDs that values.yaml never mentions.
02The scaffolded 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?
Correct — The two run on separate clocks. Chart edits move version, app releases move appVersion, and confusing them is what bites teams during upgrades.
Incorrect — appVersion is informational and Helm never compares it to decide anything. It also supplies the default image tag, nothing more.
Incorrect — No such check exists. They are independent by design and routinely differ, as the scaffold's own 0.1.0 against 1.16.0 shows.
Incorrect — That job belongs to the separate kubeVersion field. appVersion just records the version of the application the chart installs.
03Release 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?
Incorrect — Helm never erases rows. Revision 2 stays listed as superseded so you can still see what was attempted and when.
Incorrect — Existing revisions are immutable records. Helm writes a new one for every change rather than editing what already happened.
Incorrect — The counter only moves forward. A rollback to 1 does not put you back at revision 1, it puts you at a newer revision holding revision 1's state.
Correct — Rolling back re-applies a known-good state and logs itself as a new revision, which is exactly why the history stays trustworthy.

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.

Related