Skip to main content
Kubernetes · lesson 10 of 10
Kubernetes intermediate Lesson 10 of 10

Helm: Packaging Manifests, and a Real Rollback

Every earlier lesson hand-applies individual YAML files. Package the same Deployment and Service as a Helm chart, upgrade it with a real broken image tag, and roll it back with helm rollback.

By SkillByExample · Published

kubernetes helm packaging rollback

Every earlier lesson in this track ran kubectl apply -f against hand-written YAML files — fine for a handful of manifests, but nothing tracks what changed between versions or lets you undo a bad change in one command. Helm adds templating, versioned releases, and rollback on top of the same underlying manifests.

A minimal chart

A chart is a directory with a fixed shape: Chart.yaml for metadata, values.yaml for defaults, and templates/ for the manifests themselves, written with Go template syntax.

# mychart/Chart.yaml
apiVersion: v2
name: mychart
description: A minimal chart for the Helm lesson
type: application
version: 0.1.0
appVersion: "1.25"
# mychart/values.yaml
replicaCount: 2
image:
  repository: nginx
  tag: "1.25"
service:
  port: 80
# mychart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-web
  labels:
    app: {{ .Release.Name }}-web
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}-web
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}-web
    spec:
      containers:
        - name: web
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 80

{{ .Release.Name }} is filled in at install time from the name you give the release, not the chart — the same chart installed twice under different names produces two independently-named sets of resources with no collision.

Rendering without touching the cluster

Before installing anything, helm template shows exactly what would be applied:

helm template demo mychart
---
# Source: mychart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-web
spec:
  selector:
    app: demo-web
  ports:
    - port: 80
      targetPort: 80
---
# Source: mychart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-web
  labels:
    app: demo-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-web
  template:
    metadata:
      labels:
        app: demo-web
    spec:
      containers:
        - name: web
          image: "nginx:1.25"
          ports:
            - containerPort: 80

Every {{ .Release.Name }} became demo, and {{ .Values.replicaCount }} became the 2 from values.yaml — this is plain, ordinary Kubernetes YAML underneath the templating.

Installing a release

helm install demo mychart
NAME: demo
LAST DEPLOYED: Wed Sep 16 13:49:27 2026
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
kubectl get pods -l app=demo-web
helm list
NAME                        READY   STATUS              RESTARTS   AGE
demo-web-5947d6558b-8bgmp   0/1     ContainerCreating   0          4s
demo-web-5947d6558b-qx9dh   0/1     ContainerCreating   0          4s
NAME	NAMESPACE	REVISION	UPDATED                             	STATUS  	CHART        	APP VERSION
demo	default  	1       	2026-09-16 13:49:27.500683 +0530 IST	deployed	mychart-0.1.0	1.25

helm list is the release-tracking kubectl apply -f never gave you — one row per install, independent of how many raw Kubernetes objects the chart actually renders into.

Upgrading with —set, no file edits

helm upgrade demo mychart --set replicaCount=4
kubectl get pods -l app=demo-web
Release "demo" has been upgraded. Happy Helming!
NAME: demo
LAST DEPLOYED: Wed Sep 16 13:49:35 2026
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
NAME                        READY   STATUS              RESTARTS   AGE
demo-web-5947d6558b-8bgmp   1/1     Running             0          13s
demo-web-5947d6558b-npj5t   0/1     ContainerCreating   0          4s
demo-web-5947d6558b-qx9dh   1/1     Running             0          13s
demo-web-5947d6558b-xh6jh   0/1     ContainerCreating   0          4s

--set overrides one value from the command line without touching values.yaml — REVISION moved from 1 to 2, and the pod count really did go from 2 to 4.

A real broken upgrade

helm upgrade demo mychart --set image.tag=does-not-exist-xyz
kubectl get pods -l app=demo-web
Release "demo" has been upgraded. Happy Helming!
NAME: demo
LAST DEPLOYED: Wed Sep 16 13:49:44 2026
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None
NAME                        READY   STATUS         RESTARTS   AGE
demo-web-5947d6558b-8bgmp   1/1     Running        0          22s
demo-web-5947d6558b-qx9dh   1/1     Running        0          22s
demo-web-68489df84f-m9ld8   0/1     ErrImagePull   0          5s

Helm reports STATUS: deployed — it applied the manifests successfully, which is all it checks by default. But kubectl get pods shows the real consequence: the new pod can’t pull nginx:does-not-exist-xyz at all. The two old, working pods from revision 2 are still serving traffic; only the newly-created replica is broken, because this Deployment’s rolling-update strategy replaces pods gradually rather than all at once.

Rolling back

helm history demo
REVISION	UPDATED                 	STATUS    	CHART        	APP VERSION	DESCRIPTION     
1       	Wed Sep 16 13:49:27 2026	superseded	mychart-0.1.0	1.25       	Install complete
2       	Wed Sep 16 13:49:35 2026	superseded	mychart-0.1.0	1.25       	Upgrade complete
3       	Wed Sep 16 13:49:44 2026	deployed  	mychart-0.1.0	1.25       	Upgrade complete
helm rollback demo 2
kubectl get pods -l app=demo-web
helm history demo
Rollback was a success! Happy Helming!
NAME                        READY   STATUS    RESTARTS   AGE
demo-web-5947d6558b-8bgmp   1/1     Running   0          34s
demo-web-5947d6558b-8j26l   1/1     Running   0          4s
demo-web-5947d6558b-ddsdf   1/1     Running   0          4s
demo-web-5947d6558b-qx9dh   1/1     Running   0          34s
REVISION	UPDATED                 	STATUS    	CHART        	APP VERSION	DESCRIPTION     
1       	Wed Sep 16 13:49:27 2026	superseded	mychart-0.1.0	1.25       	Install complete
2       	Wed Sep 16 13:49:35 2026	superseded	mychart-0.1.0	1.25       	Upgrade complete
3       	Wed Sep 16 13:49:44 2026	superseded	mychart-0.1.0	1.25       	Upgrade complete
4       	Wed Sep 16 13:49:57 2026	deployed  	mychart-0.1.0	1.25       	Rollback to 2

helm rollback 2 doesn’t reverse revision 3 in place — it creates a brand-new revision 4 whose manifests are identical to revision 2’s. History keeps growing forward; nothing is deleted. All 4 pods are Running again on the good nginx:1.25 image, and the broken ErrImagePull pod is gone.

Practice

1. Run helm get values demo after the upgrades above. What does it show?
helm get values demo
USER-SUPPLIED VALUES:
replicaCount: 4

Only replicaCount: 4 — not image.tag. Revision 2 (the rollback’s target) only ever had --set replicaCount=4 applied to it; the broken image.tag=does-not-exist-xyz override belongs to revision 3, which this release rolled back past. helm get values shows exactly what was overridden at the currently-active revision, not a running log of every override ever set — image.tag isn’t there because at revision 2 it was still the chart’s plain default, never a user override.

2. Uninstall the release. Does the chart directory on disk disappear too?
helm uninstall demo
release "demo" uninstalled

No — mychart/ on disk is untouched; helm uninstall only removes the release (the Kubernetes resources it created, plus Helm’s own release history for that name). The chart itself is just a template you can helm install again under the same or a different name.

3. Try helm rollback demo 99 — a revision number that was never created.
helm rollback demo 99
Error: release has no 99 version

A specific, named error rather than a generic failure — Helm distinguishes “this release exists but that revision doesn’t” from “this release doesn’t exist at all” (the error Practice 2’s helm rollback would produce after the uninstall, since by then there’s no release left to have any revision).

4. Package the chart with helm package instead of installing it. What does it produce?
helm package mychart
Successfully packaged chart and saved it to: /path/to/mychart-0.1.0.tgz

A single versioned .tgz archive at the full path shown — Chart.yaml’s version field names the file. This is the distributable unit for a chart repository: helm install and helm upgrade both accept a .tgz path or repo reference exactly like they accept a local directory.

5. Add a second value to values.yaml that nothing in templates/ references, then run helm template. Does Helm complain?

An unused value (e.g. adding unusedKey: hello to values.yaml) produces no warning or error from helm template — Helm only fails on a value template references but can’t find (an undefined key), never on an extra one sitting unused. Values files commonly accumulate settings for optional features a given chart’s templates don’t all consume.

Frequently Asked Questions

Is Helm required to use Kubernetes?
No — every earlier lesson in this track ran kubectl apply -f directly against plain YAML files, and that works fine for a handful of manifests. Helm becomes valuable once you're templating the same app across environments (dev/staging/prod with different values), sharing charts across teams, or wanting release history and rollback, none of which plain kubectl apply tracks on its own.
What's the difference between helm template and helm install?
helm template renders the chart's templates against your values and prints the resulting YAML to your terminal — nothing touches the cluster. helm install renders the same YAML and actually applies it, then records the result as a numbered release Helm can track, upgrade, and roll back.
Why did helm upgrade report success even though the new pod failed to start?
Helm's default behavior is to apply the rendered manifests and consider the release upgraded once the Kubernetes API accepts them — it does not wait for pods to become Ready unless you pass --wait. That's why this lesson's broken-image upgrade shows STATUS: deployed from Helm's side while kubectl get pods simultaneously shows a real ErrImagePull; the two commands are reporting on different things.
What exactly does helm rollback restore?
It re-applies the exact set of rendered manifests from the target revision's release record — not a snapshot of live cluster state, and not source files. Helm stores each revision's fully-rendered YAML internally (as a Secret, by default) precisely so a rollback doesn't depend on your local chart files being unchanged since that revision was deployed.