
Helm and Kustomize: Consume a Chart, Overlay a Base, Render Before You Apply
Helm and Kustomize: Consume a Chart, Overlay a Base, Render Before You Apply
Part 8 of the CKA roadmap · Phase 2 — Cluster Architecture · 25%
The 2025 curriculum added the objective "Use Helm and Kustomize to install cluster components". The verb is use. Nothing in it asks you to author a chart. What it asks, in practice:
- install a chart someone else wrote, at the version you are told
- change its values, and know what your change did to the values that were already there
- undo a bad upgrade
- adapt one set of plain YAML to two environments without copying it
- see what any of this will produce before it reaches the cluster
This post does all of that on the two-node cluster from part 1.
Two tools, two ideas
They get mentioned together, but they solve the problem from opposite ends.
| Helm | Kustomize | |
|---|---|---|
| Model | Templates with values filled in | Plain YAML with patches layered on top |
| Unit | A chart, installed as a named release | A directory with a kustomization.yaml |
| State in the cluster | Yes — a revision history per release | None. It renders YAML and kubectl applies it |
| Undo | helm rollback | Apply the previous YAML again |
| Where it lives | A separate helm binary | Built into kubectl (-k, kubectl kustomize) |
The row that matters most is the third. Helm remembers what it did, so it can roll back. Kustomize remembers nothing — which is exactly why it can render without a cluster at all.
The task
Helm. Add the repository
https://stefanprodan.github.io/podinfoand install the chartpodinfoas releasewebin namespacedemo. Scale it to three replicas with--set, then upgrade again from a values file that sets only a UI message. Confirm which values are in effect after each step. Roll the release back to the three-replica revision, and show its history.Kustomize. From one base Deployment and Service, produce a
devand aprodvariant: each in its own namespace with its own name prefix,prodwith three replicas, a newer image and resource limits, both with a generated ConfigMap. Render both and compare them before applying either.
Written from scratch for this series, like every task in it.
Part 1 — Helm
Step 0 — Helm on your own cluster
The exam machines have Helm. A kubeadm cluster you built yourself does not:
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
helm completion bash | sudo tee /etc/bash_completion.d/helm > /dev/nullThat installs Helm 3, which is what I practised on. Helm 4 was released in late 2025, with its own script, get-helm-4. Its breaking changes are in plugins, post-renderers and a few renamed flags. None of the commands in this post are affected.
Helm reads the same kubeconfig as kubectl. If helm version works but helm list cannot reach the cluster, the problem is your KUBECONFIG — not Helm.
Step 1 — Find the chart, and the right version of it
helm repo add podinfo https://stefanprodan.github.io/podinfo
helm repo update
helm search repo podinfo --versions | headRead two columns. CHART VERSION is the version of the package. APP VERSION is the version of the software inside it. Helm's --version flag always means the first one. When a task says "install version X of the chart", that is the column to match.
Why podinfo and not a Bitnami chart
podinfo is small, starts in seconds, and has values that are easy to see change. Bitnami used to be the default choice for labs, but its free public catalogue was cut back in 2025, and many of its images no longer pull.
Step 2 — Install: revision 1
helm install web podinfo/podinfo -n demo --create-namespace
helm list -n demo
kubectl get deploy,po -n demoReleases are namespaced. helm list without -n shows only the current namespace, which is usually default. A release you just installed that seems to have vanished almost always means a missing -n. When in doubt, look everywhere: helm list -A.
Now look at the values in effect:
helm get values web -n demo
helm get values web -n demo --all | head -30Without --all, you see only the values you supplied — nothing yet. With --all, you see the full set, chart defaults included.
Step 3 — Upgrade with --set: revision 2
helm upgrade web podinfo/podinfo -n demo --set replicaCount=3
kubectl get deploy -n demo
helm get values web -n demo
helm history web -n demoThree replicas, replicaCount: 3 in the user-supplied values, and REVISION 2.
Step 4 — Upgrade from a file: revision 3
Write a values file that sets only the message — nothing about replicas:
cat > podinfo-values.yaml <<'YAMLEOF'
ui:
message: "CKA lab revision 3"
YAMLEOFBefore running it, see what it would do:
helm upgrade web podinfo/podinfo -n demo -f podinfo-values.yaml --dry-run | grep -n -i replicasThen run it and check:
helm upgrade web podinfo/podinfo -n demo -f podinfo-values.yaml
helm get values web -n demo
kubectl get deploy -n demoBack to one replica. The user-supplied values now hold only ui.message. replicaCount: 3 from the previous upgrade is gone.
The biggest trap in `helm upgrade`
An upgrade replaces the set of values you supplied. It does not add to the previous one. Anything you set last time and do not set again falls back to the chart default.
- To keep previous values and add to them:
--reuse-values - One exception: an upgrade with no
-fand no--setat all keeps the previous values. Helm copies them forward when you pass nothing. The moment you pass anything, the old set is dropped.
Run helm get values after every upgrade. It is the only way to be sure.
Step 5 — Roll back: revision 4
helm history web -n demo
helm rollback web 2 -n demo
helm history web -n demo
helm get values web -n demo
kubectl get deploy -n demoThree replicas again. But look at the history: it now has four rows, and the fourth one says Rollback to 2.
A rollback does not rewind the revision counter. It takes the configuration of an old revision and releases it as a new one. History is never lost, so a rollback can itself be rolled back.
Without a revision number, helm rollback web -n demo goes back exactly one step.
Step 6 — Uninstall
helm uninstall web -n demo
helm list -n demo
kubectl delete ns demouninstall deletes the revision history too, so there is nothing left to roll back to. To keep it, use helm uninstall web -n demo --keep-history. The release then shows as uninstalled, and helm rollback can still bring it back.
For charts that install CRDs, uninstall also leaves those CRDs behind, on purpose. The Operators and CRDs post has an example.
Reaching a Service on a cloud VM
kubectl port-forward listens on localhost — of the VM, not of your laptop. To check a forwarded Service, curl it from a second SSH session on the same VM, or open an SSH tunnel (-L 9898:localhost:9898) when you connect.
Don't use --address 0.0.0.0 plus a firewall rule to reach it from a browser. That publishes the Service to the internet. In the exam none of this comes up — everything runs on the machine you are typing on.
Part 2 — Kustomize
Step 1 — The tree
mkdir -p kustomize-lab/base kustomize-lab/overlays/dev kustomize-lab/overlays/prod
cd kustomize-labkustomize-lab/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── dev/kustomization.yaml
└── prod/
├── kustomization.yaml
└── resources-patch.yamlStep 2 — The base
A Deployment that reads its environment from a ConfigMap called app-config:
cat > base/deployment.yaml <<'YAMLEOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.21.1
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: app-config
YAMLEOFcat > base/service.yaml <<'YAMLEOF'
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
YAMLEOFcat > base/kustomization.yaml <<'YAMLEOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
YAMLEOFThe base does not define app-config. Each overlay generates its own.
`labels:`, not `commonLabels:`
commonLabels is deprecated in Kustomize v5, the version built into kubectl 1.33. Use the labels: block.
includeSelectors: false keeps the label out of selector.matchLabels. A Deployment's selector is immutable, so a label that lands there makes every later apply to a running Deployment fail.
Render the base before building on it:
kubectl kustomize baseStep 3 — The dev overlay
cat > overlays/dev/kustomization.yaml <<'YAMLEOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
namePrefix: dev-
resources:
- ../../base
replicas:
- name: web
count: 1
configMapGenerator:
- name: app-config
literals:
- ENV=dev
- LOG_LEVEL=debug
YAMLEOFStep 4 — The prod overlay
A patch that adds resource requests and limits:
cat > overlays/prod/resources-patch.yaml <<'YAMLEOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: nginx
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
YAMLEOFcat > overlays/prod/kustomization.yaml <<'YAMLEOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
namePrefix: prod-
resources:
- ../../base
replicas:
- name: web
count: 3
images:
- name: nginx
newTag: 1.25.3
patches:
- path: resources-patch.yaml
target:
kind: Deployment
name: web
configMapGenerator:
- name: app-config
literals:
- ENV=prod
- LOG_LEVEL=info
YAMLEOFEvery name in the overlay — in replicas, in the patch's metadata.name, in target — is the original name, web, not prod-web. The prefix is applied to the output, not to what you refer to.
Step 5 — Render, then compare
kubectl kustomize overlays/devkubectl kustomize overlays/prodSeen side by side, the two environments are easiest to check:
diff <(kubectl kustomize overlays/dev) <(kubectl kustomize overlays/prod)Apart from the ConfigMap — different values, so a different hashed name — five things should differ: namespace, name prefix, replicas, image tag and the resources block. Anything else in the diff is a mistake.
Step 6 — Apply
kubectl create ns dev
kubectl create ns prod
kubectl apply -k overlays/dev
kubectl apply -k overlays/prodkubectl get deploy,svc,cm -n dev
kubectl get deploy,svc,cm -n prodStep 7 — Why the ConfigMap has a hash in its name
The generated ConfigMap is not called dev-app-config. It is dev-app-config- plus a hash of its contents. And the Deployment's configMapRef was rewritten to match:
kubectl get deploy dev-web -n dev \
-o jsonpath='{.spec.template.spec.containers[0].envFrom[0].configMapRef.name}{"\n"}'That is deliberate. Change a value, and the name changes. The Deployment's pod template now refers to a new name, so the Deployment rolls out new pods by itself. A ConfigMap edited in place, by contrast, reaches running pods only when they restart.
See it happen. In overlays/dev/kustomization.yaml, change LOG_LEVEL=debug to LOG_LEVEL=trace, then:
kubectl kustomize overlays/dev | grep -n 'name: dev-app-config'
kubectl apply -k overlays/dev
kubectl rollout history deploy/dev-web -n devA new ConfigMap name, and a second rollout revision. The old ConfigMap is still there — apply -k never deletes anything that is no longer in the output.
If a task requires a fixed name, turn the hash off:
generatorOptions:
disableNameSuffixHash: trueRender before you apply
kubectl kustomize needs no cluster at all, so there is never a reason to apply -k blind. Make it a reflex with a deliberate mistake. In overlays/dev/kustomization.yaml, change ../../base to ../../bases, then:
kubectl kustomize overlays/devThe error arrives in a second, and nothing in the cluster was touched. Fix it and render again.
There are three levels of preview. Each one checks something the one before it cannot:
| Command | What it checks |
|---|---|
kubectl kustomize <dir> | What YAML Kustomize produces — no cluster needed |
kubectl apply -k <dir> --dry-run=client | What kubectl would send, without asking the API server |
kubectl apply -k <dir> --dry-run=server | Whether the API server would accept it — validation, admission, quota — without storing it |
The Helm equivalents are helm template (render only), helm upgrade --dry-run, and the helm diff plugin. The plugin will not be on an exam machine.
Verification
kubectl kustomize overlays/prod | grep -E 'namespace: prod|image:|replicas:'
kubectl get deploy -A -o wide | grep -E 'dev-web|prod-web'
kubectl get cm -n devBefore you call it done, check each of these:
- Helm — the history has four revisions, the last one
Rollback to 2, andhelm get valuesshowsreplicaCount: 3(check before Step 6 uninstalls the release) prodrender — namespaceprod, imagenginx:1.25.3,replicas: 3- Running —
dev-webandprod-webeach in their own namespace, with the replica counts you asked for - ConfigMaps in
dev— two generated ConfigMaps, one for eachLOG_LEVELyou applied
Traps
My notes record no mistakes from this lab. For the Kustomize session that is because it ran clean. For the Helm session it is because I closed it without writing anything down. So the traps below are not a list of my own errors. They are the edges the lab was built around.
Helm
| Trap | Symptom | The fix |
|---|---|---|
Forgot -n | The release "isn't there" | Releases are namespaced — helm list -A |
| Upgraded with a partial values file | Earlier overrides silently gone | Pass everything, or --reuse-values. Check with helm get values |
| Expected rollback to rewind history | Revision count keeps going up | Correct behaviour — rollback releases an old config as a new revision |
| Uninstalled, then tried to roll back | release: not found | Use --keep-history if you may want it back |
| Matched the app version | Wrong chart installed | --version is the chart version |
Kustomize
| Trap | Symptom | The fix |
|---|---|---|
| Wrong file name | unable to find ... kustomization | It must be named kustomization.yaml (or .yml, or Kustomization) |
apply -k pointed at a file | Fails immediately | -k takes a directory; -f takes files |
| Base path wrong | Resources not found | resources: paths are relative to the kustomization.yaml they are in |
Used patchesStrategicMerge | Deprecation warning | Kustomize v5 uses patches: with path and target |
| Patch has no effect | Output identical to the base | The patch must name the resource before namePrefix is applied |
| Forgot the namespace | namespaces "dev" not found | namespace: in a kustomization does not create the namespace |
| A label in the selector | Apply to a running Deployment fails | includeSelectors: false on the labels: block |
Speed
There are no times in my notes for this lab, so I have no number to give. The rule I hold every lab to still applies: anything over seven minutes goes on the list of slowest tasks, to be re-run against a clock before the exam.
The commands worth knowing without looking:
| Job | Command |
|---|---|
| Register a repo | helm repo add <name> <url> then helm repo update |
| Find versions | helm search repo <chart> --versions |
| Install an exact version | helm install <rel> <repo>/<chart> --version <x.y.z> -n <ns> |
| Install or upgrade, whichever applies | helm upgrade --install <rel> <repo>/<chart> -n <ns> |
| See your overrides | helm get values <rel> -n <ns> |
| History and undo | helm history <rel> -n <ns> · helm rollback <rel> <rev> -n <ns> |
| Render a chart without installing | helm template <rel> <repo>/<chart> |
| Render a kustomization | kubectl kustomize <dir> |
| Apply or delete a kustomization | kubectl apply -k <dir> · kubectl delete -k <dir> |
The checklist:
The docs pages
- Declarative Management of Kubernetes Objects Using Kustomize — the one to bookmark. It covers generators,
namePrefix, patches, bases and overlays, and it is on kubernetes.io, so you can open it during the exam. - Helm —
helm upgradeandhelm rollback— the flag references. Check the Linux Foundation's current list of allowed resources to see whether Helm's docs are open to you in the exam. If they are not,helm <command> --helphas the same flags.
Say it out loud
Before you move on, answer these without notes:
- Name the Helm commands for consuming a chart, from adding the repo to removing the release.
- Which command previews a Kustomize result without applying it?
- Give two key differences between Helm and Kustomize.
- You upgrade with a values file that leaves out a value you set last time. What happens to it?
- What does
helm historyshow after a rollback, and why?
Clean up
helm uninstall web -n demo --ignore-not-found
kubectl delete -k overlays/dev
kubectl delete -k overlays/prod
kubectl delete ns demo dev prod --ignore-not-founddelete -k removes only what the current render produces, so the first dev-app-config from Step 7 survives it. Deleting the namespace takes that ConfigMap with it.
Next: Operators and CRDs · Back to the CKA roadmap