By the end you'll know
- What a Helm chart is, and what's in each of its files
- How templates and values fit together, and how to see the YAML they make
- How to change settings for one environment without touching the chart
- What Helm checks, and the mistakes it doesn't catch
- How Helm installs, upgrades and rolls back on its own, and where it keeps its records
Before you start
- You need: Lessons 1 to 4 done, with the
gitopscluster running. - Install Helm:
brew install helm. - Get the new files:
git pull. You'll havecharts/bookshop/. - Where commands run: your own computer's terminal, in
gitops-practice. Argo CD isn't involved this lesson; Lesson 6 hands it the chart.
The short version
- The problem. The bookshop's YAML has its settings baked in: the number of copies, the image, the shop's name. A staging copy would mean a second set of files, kept in step by hand.
- A chart. Helm turns the YAML into templates with gaps, and a
values.yamlthat fills them.helm templateshows the YAML that comes out. - One chart, two shops. A small staging file changed the name, the version and the copies. The chart itself didn't change.
- Helm on its own installed a release, upgraded it, and rolled it back, keeping each version as a Secret. It also accepted
replicas: twoand a misspelled setting without a word.
Settings baked into files
The bookshop's YAML from Lesson 3 works, but every setting is written into it: replicas: 2 in the Deployment, nginx:1.28 in the container, "The Bookshop" in the page. Say the team wants a staging copy, to try changes before the real shop: one copy, a name that makes it obvious it isn't the real one, and a different version in the footer. With plain YAML, that's a second folder of nearly identical files, and every change made twice, until one day someone forgets.
Helm is the Kubernetes package manager, and it solves exactly this. Think of a form letter, like the Ansible templates: one letter, with gaps for the name and address, printed once per person. A Helm chart is the letter, and a values file is the list of what goes in the gaps.
The chart
brew install helm
helm version --short
find charts -type f | sort
v4.2.0+g0646808
charts/bookshop/Chart.yaml
charts/bookshop/files/index.html
charts/bookshop/templates/_helpers.tpl
charts/bookshop/templates/configmap.yaml
charts/bookshop/templates/deployment.yaml
charts/bookshop/templates/service.yaml
charts/bookshop/values-staging.yaml
charts/bookshop/values.yaml
Helm 4. Each file has a job:
Chart.yaml: what the chart is.values.yaml: the settings, with their defaults.templates/: the Kubernetes YAML, with gaps._helpers.tplholds small reusable pieces.files/index.html: the storefront, with gaps of its own.values-staging.yaml: settings for staging, which override the defaults.
# Chart.yaml
apiVersion: v2
name: bookshop
description: The bookshop website, for the Argo CD and Monitoring tracks on kwamib.dev
type: application
version: 0.1.0 # the chart's own version
appVersion: "1" # the version of the shop it deploys
Two versions, and they mean different things. version is the chart's: it goes up when the templates change. appVersion is the app's: what the chart deploys. A chart at 0.1.0 can deploy shop version 1 today and version 2 tomorrow.
# values.yaml
replicas: 2
image:
repository: nginx
tag: "1.28"
shop:
name: The Bookshop
headline: Books for people who run things.
version: "1"
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 64Mi
Everything someone might want to change, in one place, with a sensible default. Why "1.28" and "1" in quotes? Without them, YAML reads them as numbers, and 1.28 and 1.280 would be the same number. A version is text.
Templates
templates/deployment.yaml is Lesson 3's Deployment with the settings taken out:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
{{- include "bookshop.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
{{- include "bookshop.labels" . | nindent 8 }}
spec:
containers:
- name: nginx
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
...
resources:
{{- toYaml .Values.resources | nindent 12 }}
It's the same {{ }} as Ansible's Jinja2, but Helm uses Go templates, so the details differ:
.Values.replicasreads from the values..Release.Nameis the name you give this installation, so two installations get different names..Chart.Nameisbookshop.include "bookshop.labels" . | nindent 4pastes in the labels defined in_helpers.tpl, indented 4 spaces. Indentation is the fiddly part of Helm: YAML depends on it, and templates can't see it, so you tell them.toYaml .Values.resourcescopies a whole block of values across as YAML.
One thing deliberately not templated: the selector, app: {{ .Chart.Name }}, is exactly the app: bookshop Lesson 3 used. A Deployment's selector can't be changed once it's made, and Lesson 6 switches the running bookshop over to this chart. Different labels there, and the switch would fail.
The page works the same way. files/index.html has five gaps:
<title>{{ .Values.shop.name }}</title>
...
<h1>{{ .Values.shop.headline }}</h1>
...
Deployed by Argo CD from <code>gitops-practice</code>. Version {{ .Values.shop.version }}.
And the ConfigMap template reads it in, filling the gaps on the way:
data:
index.html: |
{{- tpl (.Files.Get "files/index.html") . | nindent 4 }}
.Files.Get reads a file from the chart, and tpl treats it as a template too. So the page's text lives in an HTML file you can edit, and the shop's name in one setting.
Checking, and seeing what comes out
helm lint charts/bookshop
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
helm lint checks the chart is well formed. The only note is that it has no icon, which only matters for charts published for others. Then the most useful Helm command there is:
helm template bookshop charts/bookshop
helm template renders the chart into plain YAML, and prints it, without touching the cluster. The highlights:
kind: ConfigMap
name: bookshop-page
<title>The Bookshop</title>
<span>Deployed by Argo CD from <code>gitops-practice</code>. Version 1.</span>
kind: Service
name: bookshop
kind: Deployment
name: bookshop
replicas: 2
image: "nginx:1.28"
Every gap filled: the release name bookshop, two copies, nginx:1.28, and the shop's name and version in the page. In full, it's 233 lines, against 206 for the plain YAML: the difference is the standard labels the chart adds to everything, like app.kubernetes.io/version and helm.sh/chart. Whenever a chart does something unexpected, helm template is where to look: it shows exactly what Kubernetes would be given.
One chart, two shops
Change a value for one run:
helm template bookshop charts/bookshop --set replicas=3 | grep " replicas"
replicas: 3
Or a whole file of them. values-staging.yaml only lists what's different:
# Staging: one copy, and a name that says it isn't the real shop.
replicas: 1
shop:
name: The Bookshop (staging)
version: "2-rc"
Everything it doesn't mention keeps its default. The difference it makes, as a diff between the two renders:
diff <(helm template bookshop charts/bookshop) \
<(helm template bookshop charts/bookshop -f charts/bookshop/values-staging.yaml)
12c12
< app.kubernetes.io/version: "1"
---
> app.kubernetes.io/version: "2-rc"
21c21
< <title>The Bookshop</title>
---
> <title>The Bookshop (staging)</title>
93c93
< <div class="brand"><span class="mark">B</span> The Bookshop</div>
---
> <div class="brand"><span class="mark">B</span> The Bookshop (staging)</div>
157,158c157,158
< <span>The Bookshop, a practice app for the kwamib.dev tracks.</span>
< <span>Deployed by Argo CD from <code>gitops-practice</code>. Version 1.</span>
---
> <span>The Bookshop (staging), a practice app for the kwamib.dev tracks.</span>
> <span>Deployed by Argo CD from <code>gitops-practice</code>. Version 2-rc.</span>
...
199c199
< replicas: 2
---
> replicas: 1
Five lines of settings changed the name in three places, the version in the page and every label, and the copies. Same chart, no edits. That's the whole point: one set of templates, and a small values file for each place it runs. Lesson 7 uses exactly this for staging and production.
What Helm doesn't check
Two mistakes, on purpose. A word where a number should be:
helm template bookshop charts/bookshop --set replicas=two | grep " replicas"
replicas: two
helm exit code: 0
Accepted, exit code 0. Helm filled the gap with what it was given. Kubernetes would refuse it, but only at deploy time. And a misspelled setting:
helm template bookshop charts/bookshop --set shop.nmae=Oops | grep -c Oops
0
nmae instead of name, and Helm said nothing: it set a value no template reads, and used the default name. Both mistakes pass helm template, and the second would deploy perfectly, with the wrong result. The fix is a values.schema.json in the chart: a description of every setting, its type and its allowed values, which Helm checks before rendering. It's in Go deeper.
Helm on its own
Lesson 6 has Argo CD use the chart. But Helm can install charts by itself, and it's worth seeing how, because Argo CD does it differently. A throwaway installation, called a release, named trial, with the staging values:
helm install trial charts/bookshop -n helm-trial --create-namespace -f charts/bookshop/values-staging.yaml
helm list -n helm-trial
NAME: trial
LAST DEPLOYED: Wed Nov 4 00:25:25 2026
NAMESPACE: helm-trial
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
trial helm-trial 1 2026-11-04 00:25:25.889804 -0500 EST deployed bookshop-0.1.0 1
.Release.Name is trial, so the Deployment and Service are called trial, beside the real bookshop, without clashing. Its page:
<title>The Bookshop (staging)</title>
<span>Deployed by Argo CD from <code>gitops-practice</code>. Version 2-rc.</span>
(The footer says Argo CD, because the page is written for the real shop. Here, Helm deployed it.) An upgrade, changing one value, then the history:
helm upgrade trial charts/bookshop -n helm-trial -f charts/bookshop/values-staging.yaml --set shop.version=3
helm history trial -n helm-trial
helm get values trial -n helm-trial
Release "trial" has been upgraded. Happy Helming!
REVISION: 2
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Wed Nov 4 00:25:25 2026 superseded bookshop-0.1.0 1 Install complete
2 Wed Nov 4 00:25:30 2026 deployed bookshop-0.1.0 1 Upgrade complete
USER-SUPPLIED VALUES:
replicas: 1
shop:
name: The Bookshop (staging)
version: 3
Look at that last line: version: 3, not "3". --set guesses types, and decided 3 was a number. Here it didn't matter, because the label template quotes it, but for a value that must be text, use --set-string. Then a rollback, to revision 1:
helm rollback trial 1 -n helm-trial
helm history trial -n helm-trial
kubectl get secrets -n helm-trial
Rollback was a success! Happy Helming!
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Wed Nov 4 00:25:25 2026 superseded bookshop-0.1.0 1 Install complete
2 Wed Nov 4 00:25:30 2026 superseded bookshop-0.1.0 1 Upgrade complete
3 Wed Nov 4 00:25:30 2026 deployed bookshop-0.1.0 1 Rollback to 1
NAME TYPE DATA AGE
sh.helm.release.v1.trial.v1 helm.sh/release.v1 1 6s
sh.helm.release.v1.trial.v2 helm.sh/release.v1 1 1s
sh.helm.release.v1.trial.v3 helm.sh/release.v1 1 1s
A rollback is a new revision, 3, made from 1's settings. And there's where Helm keeps its records: one Secret per revision, in the cluster, holding the chart and values used. That's Helm's history, and it's how helm rollback knows what to go back to.
helm uninstall trial -n helm-trial && kubectl delete namespace helm-trial
kubectl get application bookshop -n argocd
release "trial" uninstalled
namespace "helm-trial" deleted
NAME SYNC STATUS HEALTH STATUS
bookshop Synced Healthy
Gone, and Argo CD's bookshop never noticed.
Helm's history, or Git's?
Notice what just happened: the trial release's history lived in the cluster, in Secrets, and came from commands someone ran. That's the push model from Lesson 1 again. Delete the namespace, and the history goes with it. Nobody reviewed --set shop.version=3.
Argo CD uses Helm differently. It runs the equivalent of helm template, takes the YAML that comes out, and applies it like any other. No helm install, no release Secrets, and helm list shows nothing. The history is Git's: which values file, which chart version, at which commit. Lesson 6 shows that in action.
In real systems
- Most software arrives as a chart. Prometheus, Grafana, databases, Argo CD itself: nearly everything has an official Helm chart. The Monitoring track installs one. You'll write fewer charts than you install.
- Values files per environment. The usual layout is one chart, and a values file for each place it runs:
values-staging.yaml,values-production.yaml. Reviewing a change to production means reading a small values diff, not a pile of YAML. - Charts get schemas. Shared charts ship a
values.schema.json, soreplicas: twoor a misspelled setting fails straight away, with a message, instead of reaching the cluster. - Render in the pull request. Teams run
helm templatein CI and post the rendered diff on the pull request, so reviewers see what will actually change in the cluster, not just which values moved.
Spot the problem
Work out what's wrong before you read the explanation.
The setting that wouldn't take. Someone wants production to run four copies of the bookshop. They add replica: 4 to values-production.yaml, render it, and deploy. The bookshop keeps running two copies. They check the file: four is right there. They redeploy; still two. What's wrong? The setting is called replicas, with an s. replica is a value nothing reads, so the template fell back to the default in values.yaml, 2, exactly like shop.nmae above. Helm didn't complain because it never does: any key is allowed in values. The quick check is helm template ... | grep replicas, which shows 2, not 4. The lasting fix is a values.schema.json with "additionalProperties": false, so a key the chart doesn't know is an error.
Keep this
| To | Run or write |
|---|---|
| Check a chart is well formed | helm lint charts/bookshop |
| See the YAML it makes | helm template NAME charts/bookshop |
| Change one value | --set replicas=3, or --set-string for text |
| Use a values file | -f charts/bookshop/values-staging.yaml |
| Compare two renders | diff <(helm template ...) <(helm template ... -f FILE) |
| Read a value in a template | {{ .Values.replicas }} |
| Paste a block, indented | {{- toYaml .Values.resources | nindent 12 }} |
| Reuse a piece | define in _helpers.tpl, then include "NAME" . | nindent 4 |
| Read a file, as a template | {{ tpl (.Files.Get "files/index.html") . }} |
| Install, upgrade, list | helm install, helm upgrade, helm list -n NAMESPACE |
| History, and roll back | helm history NAME, helm rollback NAME REVISION |
| The values a release used | helm get values NAME |
| Remove a release | helm uninstall NAME -n NAMESPACE |
Where you are now
The bookshop is a Helm chart: templates with gaps, a values file of defaults, and a staging file that makes a different shop from the same chart. You've seen Helm install, upgrade and roll back on its own, and where it keeps that history. Lesson 6 gives the chart to Argo CD: the running bookshop switches from plain YAML to the chart, with no downtime, and the ConfigMap delay from Lessons 3 and 4 gets its fix.
Check yourself
What's the difference between version and appVersion in Chart.yaml?
What does helm template do?
values-staging.yaml only lists three settings. What happens to the rest?
--set shop.nmae=Oops was accepted. What did the page show?
Where does Helm keep a release's history?
How does Argo CD use a Helm chart?
Key terms
- Helm: the package manager for Kubernetes.
- Chart: a folder of templates, default values and information about them, which Helm renders into Kubernetes YAML.
- Values: the settings that fill a chart's templates;
values.yamlholds the defaults. - Template: a file with gaps, written in Go's template language.
- Render: turning templates and values into plain YAML, as
helm templatedoes. - Release: one installation of a chart, by Helm itself, with its own name and history.
- Revision: one version of a release; a rollback makes a new one.
- values.schema.json: a description of a chart's settings, which Helm checks before rendering.
Go deeper
- The Helm documentation: Charts (opens in a new tab), and the chart template guide (opens in a new tab).
- Try: add
charts/bookshop/values.schema.jsonwithreplicasas an integer from 1 to 10, and"additionalProperties": falseat the top level. Runhelm templatewith--set replicas=two, then with--set replica=4. What happens now? - Try: add a
featuredvalue, a book's title, and put it in the page's hero section with{{ .Values.shop.featured }}. Render it with and without the value. What does the page say when it isn't set, and how woulddefaultfix that?