Seeking Wisdom · AI/ML Engineering · Investing · Books · Projects · About ·

Kubernetes by Example · Part 14

Helm and Custom Resources

Three copies of your YAML and a station list that changes at lunchtime. One of those is a templating problem. The other needs you to write a controller.

assumes Parts 1–13~20 minutesthe last one

The symptom

Two problems that arrive together once airwaves is real.

The first is tedious. You have three environments and one directory of YAML, so you copied it three times. Last week you fixed a probe timeout in dev and forgot staging, and found out during a demo.

The second is more interesting, and templating cannot touch it. The station list lives in a database and changes during the day. Someone adds a station at eleven in the morning and expects a tuner for it.

No amount of YAML solves that. YAML is a snapshot, and you need something that keeps watching.

Packaging, and then extending

These are different problems with different answers, and conflating them is common enough to be worth separating up front.

Helm renders text. It runs when you run it and then stops. A controller runs forever, watching for differences and closing them — which is precisely what every component in Part 1 was doing.

Helm — a text templating engine templates/ the same YAML, with holes values.yaml + values-prod.yaml plain Deployments, Services, ConfigMaps nothing new. the cluster never hears of Helm. api server the only door — Part 1, still true on the last page CRD — a new noun the cluster understands kind: Station kubectl get stations works immediately …and does absolutely nothing a CRD is storage and validation only no behaviour comes with it conductor — the pedant you wrote watch Stations · compare to pods · close the difference · repeat tuner pods one per Station — because something makes it so renders, then applies stored and validated. nothing else. creates what the Station asked for no controller → Stations exist and nothing happens

What Helm is not

It is not a controller. Nothing reconciles your chart. Drift from a manual kubectl edit is invisible to Helm until the next upgrade overwrites it.

It is not a dependency manager for runtime — chart dependencies are install-time only, with no ordering guarantees beyond hooks.

It is a very good templating engine with a release history. That's enough, and expecting more leads to disappointment.

The commands

Helm, carefully

Click any piece of the command.

Custom resources

Click any piece of the command.

Running a controller of your own

Click any piece of the command.

Build it

First, the chart

Everything from Parts 1–13, parameterised.

Render it, validate it, send nothing

helm template airwaves ./chart | head -40
helm template airwaves ./chart -f chart/values-prod.yaml \
  | kubectl apply --dry-run=server -f -

Install it properly

helm upgrade --install airwaves ./chart \
  -f chart/values-dev.yaml --wait --atomic
helm list
kubectl get all -l app.kubernetes.io/instance=airwaves

Plain Deployments, Services and ConfigMaps. Nothing in that output knows it came from a chart.

Then the new noun

A CRD, and some Stations

kubectl apply -f manifests/station-crd.yaml
kubectl explain station.spec
kubectl apply -f manifests/stations.yaml
kubectl get stations

Three Stations, listed with your own printer columns, validated against your own schema. And kubectl get pods shows nothing new, because a CRD is storage and nothing else.

Then the thing that makes it mean something

The conductor

kubectl apply -f manifests/conductor-rbac.yaml
kubectl apply -f manifests/conductor.yaml
kubectl logs -f deploy/conductor

Watch its log. It lists Stations, lists tuner pods, finds three Stations and zero pods, and creates three. Then it looks again and finds nothing to do — Part 1's description of a healthy cluster, now from the inside.

Add a station at eleven in the morning

kubectl apply -f manifests/station-classical.yaml
kubectl get pods -l app=tuner -w

A fourth pod, seconds later, with nobody editing a Deployment. That is the problem from the top of the page, solved — and it was never solvable by rendering better YAML.

Break it

1 · A noun with no meaning

kubectl scale deploy/conductor --replicas=0
kubectl apply -f manifests/station-ambient.yaml
kubectl get stations
kubectl get pods -l app=tuner

The Station exists, validates, lists cleanly. No pod. Nothing is broken and nothing will happen, because the CRD was only ever the vocabulary — the controller was the meaning, and you just switched it off.

kubectl scale deploy/conductor --replicas=1
kubectl get pods -l app=tuner -w

It catches up immediately. It didn't miss an event — it recomputed from scratch, which is what makes reconcile loops robust. Part 1's claim that an idle cluster is machinery finding nothing to do, demonstrated by taking the machinery away.

2 · Forget the binding, again

kubectl delete rolebinding conductor
kubectl rollout restart deploy/conductor
kubectl logs -f deploy/conductor

kubectl auth can-i create pods \
  --as=system:serviceaccount:airwaves:conductor -n airwaves

Part 13's silent 403, now in code you wrote. The controller runs, watches, calculates correctly, and every create is refused. If your error handling is sloppy it looks exactly like doing nothing.

3 · Leak a pod by getting ownership wrong

kubectl apply -f manifests/conductor-no-owner.yaml
kubectl apply -f manifests/station-test.yaml
kubectl delete station test
kubectl get pods -l app=tuner

The Station is gone; the pod is still running. Without ownerReferences, nothing connects them, so nothing cleans up. One orphan is trivial. Months of this is a cluster full of pods nobody can account for.

Put it back and confirm: delete a Station and watch its pod vanish without the conductor doing anything. Garbage collection is handling it.

What follows from this

Because……this is what happens
Helm only renders textNo reconciliation. Manual edits drift silently until the next upgrade overwrites them.
Values layer, lists replaceOverriding one list item replaces the whole list. The commonest values surprise.
Helm reports on acceptanceWithout --wait --atomic, success means recorded, not running. Part 1, one layer up.
A CRD has no behaviourYour resource validates, lists, and does nothing. The controller is the meaning.
Controllers need RBACA missing binding means silent 403s and a controller that appears to be idle.
ownerReferences drive GCSet them and deletion cascades for free. Omit them and you leak, slowly.
Status is separate from specSpec is intent, status is achievement. Use the subresource so apply can't clobber it.

Cheat sheet

Part 14 · helm and custom resources

• Helm is a templating engine. It renders YAML and applies it. The cluster never hears of it.

• helm template first, always. Most Helm bugs are templating bugs.

• helm upgrade --install --wait --atomic is the command for CI. Plain install isn't idempotent.

• Values layer; lists are replaced, not merged.

• A checksum annotation on the pod template turns a config change into a paced rollout — Parts 4 and 8, joined up.

• A CRD teaches the API server a new kind. You get validation, explain, RBAC and selectors free.

• A CRD alone does nothing. It is storage and schema; the controller supplies behaviour.

• A controller is Part 1's loop with your noun: watch, compare, close the difference, repeat.

• Set ownerReferences on everything you create, or you leak objects quietly.

• Write to status, never to spec. Spec is what they asked for; status is what you managed.

That's the series

Fourteen parts, and one idea underneath all of them: declare what you want, have something watch the difference, and close it forever.

Everything that looked arbitrary turns out to follow. Pods are disposable, so Services select by label. Selectors are queries, so ownership is a label match and two Deployments can fight. The scheduler reads requests, so a node can be full and idle. apply only records, so you need rollout status to know anything actually worked.

And on the last page, you wrote the loop yourself.