Helm Charts
CKA prep • The Kubernetes package manager: charts, templates/values, releases, repos
Key terms
| Term | Meaning |
|---|---|
| Helm | The package manager for Kubernetes |
| Chart | A versioned bundle of templated manifests |
| Values | The configuration that fills in the templates (values.yaml) |
| Template | A manifest with Go template placeholders ({{ }}) |
| Release | A named, installed instance of a chart in a cluster |
| Repository | An HTTP(S) index that serves packaged charts |
| Revision | Each install/upgrade bumps a release revision (for rollback) |
Problem & solution
Real apps are many manifests — Deployment, Service, ConfigMap, Ingress, HPA — and you copy-paste them per environment, changing a few values each time. That is error-prone and unversioned. Helm packages all of it into one templated, versioned chart, parameterised by values, and tracks each install as a release you can upgrade and roll back.
Solution: Package manifests as a templated chart, override per-environment values at install time, and manage the lifecycle (install/upgrade/rollback) as versioned releases stored in-cluster.
The analogy
Buying furniture as a flat-pack kit means the parts and the assembly steps are standardized, you just supply one parameter sheet per room, the wood color here, five chairs there, and the same kit builds a different finished room each time. You order the kit from a furniture catalog, fill in the sheet, and end up with a specific assembled set in that room that you can later modify or take apart. Helm works the same way: a chart is the kit of templated manifests, values is the parameter sheet you fill per environment, a release is one installed, running instance, and a repository is the catalog you pull charts from.
Helm 3 architecture (no Tiller)
It helps to see where Helm actually runs before using it. In Helm 3 the helm CLI does all
the work on your machine: it renders the chart's templates into plain Kubernetes manifests
locally, then applies them through the api-server using your own kubeconfig and permissions.
The record of each install (its "release" history) is stored back in the cluster as a
Secret. The diagram shows that flow; the note below explains why there is no longer a server
component (Tiller).
Graph legend — each node is a real part of a client-side Helm 3 install:
| Graph node | Maps to | What it does |
|---|---|---|
| helm CLI | the helm binary | Renders templates locally and talks to the API |
| plain Kubernetes manifests | rendered templates/*.yaml | The concrete objects Helm produces from the chart |
| kube-apiserver | the API server | Validates and applies the manifests under your RBAC |
| release state Secret | sh.helm.release.v1.<name>.v<rev> | Stores release history for upgrade/rollback |
Helm 3 dropped the in-cluster Tiller server. The CLI renders client-side and talks to the API directly with your RBAC; release history lives in Secrets.
Chart directory structure
Every chart follows the same layout, so once you know it you can navigate any chart. Here is what each file and folder is for:
helm create mychart # scaffold a working chart
helm lint mychart # validate structure + templates
mychart/ ├── Chart.yaml # name, version, appVersion, dependencies ├── values.yaml # default configuration values ├── charts/ # vendored sub-chart dependencies ├── templates/ # templated manifests │ ├── deployment.yaml │ ├── service.yaml │ ├── _helpers.tpl # reusable template snippets │ └── NOTES.txt # post-install message └── .helmignore
Templates and values
Templates are manifests with Go template directives; .Values comes from
values.yaml (and any override), .Release/.Chart are built-in objects.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
# values.yaml
replicaCount: 2
image:
repository: nginx
tag: "1.27"
service:
port: 80
Render and inspect before applying
Always preview what a chart will produce before it touches the cluster. These commands render or server-validate without installing anything:
helm template mychart # render to stdout, apply nothing
helm template mychart --set replicaCount=5 # see an override applied
helm install web ./mychart --dry-run --debug # server-validate without installing
Release lifecycle: install, upgrade, rollback
These are the everyday commands for the full life of a release, from first install through upgrades and rollback to removal:
# install a named release (override values inline or with a file)
helm install web ./mychart --set image.tag=1.27 -n prod --create-namespace
helm install web ./mychart -f prod-values.yaml -n prod
# list and inspect releases
helm list -n prod
helm status web -n prod
helm get values web -n prod
# upgrade (idempotent install+upgrade with one command)
helm upgrade --install web ./mychart --set replicaCount=4 -n prod
# history and rollback to a previous revision
helm history web -n prod
helm rollback web 1 -n prod
# remove the release (keep history with --keep-history)
helm uninstall web -n prod
Repositories and dependencies
Charts are shared through repositories, much like OS packages. These commands add a repo, find charts, and publish your own:
# add Bitnami's public repo, refresh the index, and search it
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/postgresql
# install a real chart as a release, overriding a couple of values
helm install db bitnami/postgresql -n data --create-namespace \
--set auth.postgresPassword=S3cret! --set primary.persistence.size=8Gi
# upgrade to a newer chart version, then roll back if it misbehaves
helm upgrade db bitnami/postgresql -n data --version 15.5.0
helm history db -n data
helm rollback db 1 -n data # back to the first revision
# package + push your OWN chart to a repo (or an OCI registry)
helm package mychart # -> mychart-0.1.0.tgz
helm push mychart-0.1.0.tgz oci://registry.example.com/charts
Declare chart dependencies in Chart.yaml and pull them with
helm dependency update, which populates charts/.
End-to-end: from chart to running release
This walks the complete path from typing helm install to having pods running, and then
shows how upgrades and rollbacks fit in. Each install or upgrade renders the templates,
applies the manifests, and records a numbered revision (a saved snapshot of that
release's state) so you can roll back later. Follow the diagram top to bottom.
Graph legend — each node is a real step from helm install to a running release:
| Graph node | Maps to | What it does |
|---|---|---|
| helm install web ./mychart -f values | helm install | Starts an install with override values |
| CLI loads Chart.yaml + values.yaml | the chart files | Reads chart metadata and defaults |
| Merge defaults with -f / --set | value merge | Layered overrides win over defaults |
| Render templates with Go templating | templates/*.yaml | Produces concrete manifests |
| Validate + apply via kube-apiserver | kubectl apply equivalent | Server-validates and creates objects |
| Store release revision 1 as a Secret | the release Secret | Records this revision for history |
| helm upgrade -> revision 2 | helm upgrade | Applies changes as a new revision |
| helm rollback web 1 | helm rollback | Restores an old revision as a new one |
End-to-end example: scaffold, install, upgrade, and roll back a chart
A full lifecycle: scaffold a chart, trim it to a Deployment + Service, install
revision 1, upgrade to revision 2 with --set, break it, then roll back to a
known-good state and confirm the resulting revision history.
- Scaffold and lint a fresh chart:
helm create web
helm lint web
# expected: 1 chart(s) linted, 0 chart(s) failed
- Replace
web/values.yamldefaults with a minimal, explicit set:
# web/values.yaml
replicaCount: 2
image:
repository: nginx
tag: "1.27"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
- Point the generated
web/templates/deployment.yamlcontainer at those values (the key lines):
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
- Preview the rendered YAML before touching the cluster:
helm template web ./web --set replicaCount=3 | grep -E "replicas|image:"
# expected: replicas: 3 and image: "nginx:1.27"
- Install revision 1 into its own namespace:
helm install web ./web -n web --create-namespace
kubectl -n web get deploy web -o jsonpath='{.spec.replicas}{"\n"}' # 2
helm list -n web
# expected: web web 1 ... deployed web-0.1.0
- Upgrade to revision 2, scaling out and bumping the image:
helm upgrade --install web ./web -n web \
--set replicaCount=4 --set image.tag=1.27.2
kubectl -n web get deploy web -o jsonpath='{.spec.replicas}{"\n"}' # 4
helm history web -n web
# expected: revision 1 superseded, revision 2 deployed
- Push a bad upgrade (nonexistent tag), watch it fail to roll out, then roll back:
helm upgrade web ./web -n web --set image.tag=does-not-exist
kubectl -n web rollout status deploy/web --timeout=30s || true
# expected: pods stuck ImagePullBackOff
helm rollback web 2 -n web
kubectl -n web rollout status deploy/web
# expected: deployment "web" successfully rolled out
- Confirm the rollback created a NEW revision that restores revision 2's state:
helm history web -n web
# expected: revision 4 deployed, description "Rollback to 2"
kubectl -n web get deploy web -o jsonpath='{..image}{"\n"}' # nginx:1.27.2
Graph legend — each node is a real command from the worked lifecycle above:
| Graph node | Maps to (command) | What it does |
|---|---|---|
| helm create web | helm create web | Scaffolds a working chart |
| Edit values.yaml + templates | chart edits | Point the Deployment at .Values |
| helm template --set replicaCount=3 | helm template | Previews rendered YAML, applies nothing |
| helm install -> revision 1 | helm install web ./web | First install; state stored as a Secret |
| helm upgrade --set replicaCount=4 tag=1.27.2 | helm upgrade | Revision 2: scale out + bump image |
| helm upgrade --set tag=does-not-exist | helm upgrade | Revision 3: a bad tag -> ImagePullBackOff |
| helm rollback web 2 | helm rollback web 2 | Revision 4 restoring revision 2's state |
| helm history | helm history web | Shows the full revision trail |
Common pitfalls
These are the common Helm mistakes and what each symptom usually means:
- YAML indentation in templates -> use nindent/indent, e.g. {{ toYaml .Values.x | nindent 8 }}
- changed values, nothing moved -> you ran `template`, not `upgrade`/`install`
- release name collision -> names are unique per namespace
- secrets in values.yaml in git -> keep secret values out of source control
- rollback didn't help -> rollback creates a NEW revision; check history
- CRDs not upgraded -> Helm does not upgrade CRDs from crds/ automatically
Key takeaways
- A chart = templated manifests + default values; an install = a release.
- Helm 3 is client-side (no Tiller); release state lives in Secrets.
helm template/--dry-runlets you preview rendered YAML safely.helm upgrade --installis the idempotent workhorse; revisions enable rollback.helm rollbackmakes a new revision that restores an old one.- Add repos (or OCI registries) to install and share charts.
Checklist
- [ ] Scaffolded a chart with
helm createand read the structure - [ ] Edited templates to consume
.Valuesand rendered withhelm template - [ ] Installed a release and overrode values with
--setand-f - [ ] Upgraded the release and rolled it back to a prior revision
- [ ] Added a repo, searched, and installed a third-party chart