TT Lab
Get started
Learn Learning paths Courses

CRDs and Operators

A CR Is Both the Input to a Deployment and Its Status Report

Continue in TT Lab

In one sentence

A CR holds, in one object, a place where the user writes what they want (spec) and a place where the system reports back what it observed (status). The moment you mix the two, deployments become untraceable.

Why it was needed

Think about how to answer the question, "Is this service running fine right now?" With Helm alone, you look at the release list, find the Deployment that release created, find that Deployment's ReplicaSet, and count the Pods. What the tool tells you goes only as far as "the install command succeeded"; the state after that, a person has to assemble.

A CR brings that assembly inside the object. The user writes intent in spec, and the controller writes observed results in status. Then a single kubectl get webservice line becomes a deployment status dashboard. But for this structure to hold, you need discipline.

How it works

Discipline 1 — spec is for the user, status is for the controller. The moment a controller writes a value into spec, the GitOps repository and the cluster start to diverge. A value that is not in the manifest the user committed appears only in the cluster, and the next sync erases it. That is why status must always be written through a separate path.

잘못됨:  spec 과 status 를 한 번에 update  -> status 서브리소스가 켜져 있으면 status 는 무시됨
올바름:  status 만 서브리소스 경로로 갱신

Discipline 2 — write state, not commands. A field like spec.restartNow: true is an anti-pattern. Once it has run, the value becomes meaningless, and nobody can trace who turned it off and when. Instead, express it with a value that has lasting meaning, such as spec.version or spec.paused. That way the reconcile loop makes the same judgment whenever it runs again.

Discipline 3 — attach owner references to child resources. If you attach ownerReferences to the ConfigMaps, Deployments, and Services a CR creates, two things follow. First, when the parent is deleted, the garbage collector cleans up the children automatically. Second, the controller manages only "what I created," so the scope of reconciliation is clear. There is one important pitfall here — an owner reference links by uid, not by name. If you delete a parent and recreate one with the same name, the uid changes, and children that point to the old uid become orphans and are immediately garbage-collected.

Discipline 4 — status follows the conditions standard. A conditions array is recommended over a simple phase: Running string.

Field Meaning
type Condition name (Ready, Progressing, Degraded)
status True / False / Unknown
reason Short machine-readable reason code
message Human-readable explanation
lastTransitionTime When the status last changed

The reason for having both reason and message is the key point. Alert rules branch on reason, and the on-call engineer reads message. If you keep only one, one of the two audiences is inconvenienced.

Discipline 5 — expose lag with observedGeneration. metadata.generation rises every time spec changes, and status.observedGeneration is the generation the controller last processed. If the two differ, it means "the latest spec has not been reflected yet." Without this pair, users cannot tell whether status reflects the new spec or is a remnant of the old one.

What you see in the field

First, the incident of judging by status alone. status is a cache of what the controller observed, not the source of truth. The real state is in the actual resources. A controller that branches on status alone makes wrong decisions when status is stale.

Second, the incident of piling up an unbounded array in status. If you accumulate events or logs in a status array, the whole object is rewritten on every update, and the informer cache memory swells along with it. You should use a fixed-size structure such as conditions.

Third, the combination of custom columns and labels. The most practical result of a CR being a proper object is the label selector. Putting a value in spec.tier and attaching metadata.labels.tier are different things. The former is the intent the controller reads, and the latter is the index people and tools select by. You need both.

What you will do in the next lab

In the crd-lab namespace, you create a minimal-spec CR and a full-spec CR to confirm default injection, bind a child ConfigMap to its parent with an owner reference, and write observed values and a Ready condition directly into the status subresource. Then you pick out only what you want with label selectors and custom columns, and finally create a "desired state" object derived from the CR's spec, getting ready to move on to the next module's reconcile loop.