AI EngineeringZero to ProductionHome·About·What’s new·Contact
Kubernetes Orchestration · Part 11

Operators & CRDs

Every controller you have used — Deployment, HPA, Argo Rollouts, the Prometheus Operator — is the same idea: a control loop that watches desired state and works to make actual state match. This lesson teaches that pattern directly: Custom Resource Definitions (CRDs) that extend the Kubernetes API with your own objects, and operators — controllers that reconcile those objects into real infrastructure. We model the reconcile loop in plain Python (offline-runnable), walk conceptually through writing a controller, survey how real platforms extend k8s, and answer the money question: when to build an operator vs when not to.

⏱️ ~110 min🧩 Extending k8s🎯 Advanced→Expert
🌱 Honesty up frontCRD/operator YAML and kubectl here need a cluster (minikube/kind local, or EKS) and output is illustrative. Operator SDKs (Kubebuilder, Operator SDK, the client libraries) and CRD schema fields drift — verify against current docs. The core payoff of this lesson, the reconcile loop, is modeled in plain Python that runs offline with no cluster or dependencies, so you can internalize the pattern before touching a real controller.

Learning objectives

  • Explain the controller pattern: watch → diff desired vs actual → act → repeat (a level-triggered loop).
  • Define a CRD to extend the Kubernetes API with a custom object, and a Custom Resource instance.
  • Describe what an operator is: a controller + CRDs that encodes operational knowledge.
  • Trace and implement reconcile logic (modeled offline in Python) that is idempotent and level-triggered.
  • Use existing operators to run stateful software, and know how real platforms extend k8s.
  • Decide when to build an operator vs not — the cost/benefit that keeps you honest.

1 · The pattern you've used all along: control loops

You have been trusting this pattern for ten chapters without naming it. Every time you ran kubectl scale deployment ... --replicas=3 and walked away, you were relying on something to keep three Pods alive — to notice when a node died at 3 a.m. and quietly bring a replacement up before you woke. Nobody re-ran your command. That silent, tireless custodian is a control loop, and once you see it clearly here you will recognize it inside every Kubernetes object you have ever touched, and you'll be ready to write one of your own.

The best analogy is the thermostat on your wall. You don't tell it "turn the heater on for 12 minutes" — you tell it the goal: "keep this room at 21°C." From then on it does one thing in an endless loop: read the actual temperature, compare it to the target, and if there's a gap, act (heat or cool); if there's no gap, do nothing. It never assumes it "already handled" the room — it just keeps checking and correcting. A Kubernetes controller is that thermostat pointed at cluster state instead of air temperature. You declare the goal in an object's spec; the controller reads actual reality, diffs it against the goal, and nudges the world toward it, forever.

Now the precise version, which will land because you have the feel for it. Kubernetes is, at heart, a pile of controllers each running the same loop: observe the desired state (from an object's spec), observe the actual state (the world), compute the difference, and take action to close the gap — forever. A Deployment controller sees 'you want 3 replicas, there are 2' and creates one Pod. This is declarative: you state the goal, the controller continuously drives toward it.

The common mistake here is thinking in terms of commands instead of goals. Newcomers picture Kubernetes as a robot that executes a script — "create Pod, wait, create Pod" — and then are baffled when a Pod they deleted reappears, or frustrated that there's no "run this once" button. That mental model is wrong and it will fight you the entire chapter. The right model is a goal that is continuously enforced: you never command an action, you register a desired end-state and something works ceaselessly to maintain it. Hold onto that reframing, because an operator — the thing we build in this lesson — is nothing more than your own control loop enforcing your own goal. Master the loop and the rest of the chapter is just applying it to custom objects.

Watch desired spec Observe actual the world Diff what's missing Act → repeat converge
🗺️ How to read this diagram

Read the four boxes strictly left-to-right, following the arrows — it's one lap of a loop that then starts over.

  • The first box, "Watch desired" (spec), is where the loop begins: it reads the goal you declared in an object's spec.
  • The solid arrows carry you through the middle two boxes — "Observe actual" (the world) reads reality, then "Diff" (what's missing) computes the gap between goal and reality.
  • The last box, "Act → repeat" (converge), is the payoff: it closes the gap and loops back to the start, so the state is driven toward the goal forever rather than in a single pass.

In short: watch desired → observe actual → diff → act, then repeat forever — the control loop that keeps actual state converging on the goal.

Level-triggered, not edge-triggeredControllers are level-triggered: they act on the current gap between desired and actual, not on a one-off event. That's why they're robust — if the controller was down when a Pod died, it still notices the missing replica on its next reconcile and fixes it. The practical consequence for your own reconcile code: it must be idempotent (safe to run repeatedly) and never assume it saw every event.

2 · CRDs: teaching the API server a new noun

Here's the surprising thing about Kubernetes: you can add your own words to its vocabulary. Out of the box the API knows nouns like Pod, Deployment, Service, ConfigMap. But nothing about those is privileged — you can teach the API server an entirely new noun, say LLMService, and from that moment it behaves like a first-class citizen: kubectl get llmservice works, it obeys RBAC, it lives in etcd, it shows up in kubectl explain. The tool that performs this teaching is a CustomResourceDefinition, and it is the foundation everything else in this lesson stands on.

Think of the Kubernetes API as a database with a fixed set of tables. A CRD is you running "CREATE TABLE" — you hand the API server a schema (what fields an LLMService has, which are required, what types they must be) and it creates a new place to store and validate objects of that shape. Just like a fresh database table, defining it doesn't make anything happen: an empty table doesn't process orders, and a CRD doesn't launch Pods. It gives you validated, queryable storage and nothing more. That distinction is the single most important idea in this section, so say it plainly: on its own a CRD is just structured storage with validation; it does nothing until a controller watches it.

The mistake almost everyone makes on their first CRD is expecting behavior. They write a beautiful LLMService schema, apply it, create an instance with replicas: 3, run kubectl get pods — and see nothing. Then they conclude the CRD is broken. It isn't. A CRD with no controller is like defining a form with no clerk to read it: the paperwork is filed and validated, but no one has been hired to act on it. The "clerk" is the operator in the next section. Keeping the noun (the data) mentally separate from the verb (the behavior) will save you hours of confusion. With that framing set, suppose we want a high-level LLMService that captures 'run this model with this many replicas' so app teams don't hand-write Deployments:

apply CRD → API server learns a new noun (a validated table) CRD the schema API server kubectl get llmservice validate + store in etcd obeys RBAC Pods? none — no controller solid arrow = what a CRD gives you · dashed = the behavior it does NOT A CRD is a new table, not a new behavior. Applying it teaches the API server to store, validate and serve LLMService objects — but no workload is ever created until a controller watches the kind.
🗺️ How to read this diagram

Read left-to-right: the CRD flows into the API server (solid arrow = things you now get), and the dashed arrow on the right shows the one thing you do not get.

  • The left blue box is the CRD you apply — just a schema describing the new LLMService kind.
  • The middle panel is the API server after the CRD lands: the green rows are the real powers you gain — kubectl get llmservice works, objects are validated and stored in etcd, and they obey RBAC like any built-in.
  • The red box on the dashed arrow is the payoff/aha: still zero Pods. The dashed edge marks the missing verb — a CRD is storage only; behavior arrives in the next section when a controller watches this kind.

In short: A CRD gives you a validated, queryable place to keep your objects (the noun); it never launches a workload until an operator (the verb) reconciles them.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
yaml · a CRD defining a new LLMService kind (needs a cluster; verify schema fields vs docs)
crd.yamlapiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: { name: llmservices.ai.example.com }
spec:
  group: ai.example.com
  names: { kind: LLMService, plural: llmservices, singular: llmservice, shortNames: [llm] }
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                model:    { type: string }
                replicas: { type: integer, minimum: 1 }
              required: [model, replicas]
yaml · a Custom Resource: one instance of the new kind (needs a cluster)
llmservice.yamlapiVersion: ai.example.com/v1
kind: LLMService
metadata: { name: chat, namespace: web }
spec:
  model: llama-3-8b
  replicas: 3
# `kubectl get llmservice -n web` now lists this. Nothing happens yet —
# a controller must watch LLMService and create the real Deployment/Service.
CRD = the data; operator = the behaviorApplying the CRD gives you a validated place to store LLMService objects and query them, but the cluster won't create a single Pod from one. The operator (next section) is the controller that watches LLMService objects and reconciles each into the Deployment, Service, HPA, etc. it implies. Split the concepts: CRD is the noun; the operator is the verb.

3 · Reconcile logic, modeled offline in Python

This is the section that turns "operators" from a buzzword into something you could write on a napkin. People assume the reconcile function at the core of every operator — the thing Kubebuilder scaffolds, the thing the Prometheus Operator and cert-manager and every cloud database operator are built around — must be some intricate distributed-systems machine. It is not. Strip away the cluster plumbing and the whole idea fits in about a dozen lines of ordinary Python, and you can run it right now with no cluster, no YAML, no dependencies. Get this function into your fingers and you understand the beating heart of the entire ecosystem.

The intuition is a checklist you re-run rather than a script you run once. Imagine a diligent building superintendent whose job is "the lobby should have three working light bulbs." Every round they walk in, count the working bulbs, and compare to the goal: zero bulbs → install three; three bulbs → touch nothing and move on; someone swapped in the wrong wattage → replace the wrong ones. Crucially, they don't keep a diary of "I installed bulbs last Tuesday" and refuse to look again — each round they judge only by what they see right now. That "act on the present gap, not on memory of past events" discipline is called being level-triggered, and the "safe to run the exact same check a thousand times" property is called idempotence. Reconcile is that superintendent's round, expressed in code.

The classic beginner error is writing reconcile as if it fires once per event. The tempting shape is "when a new LLMService is created, create a Deployment" — event-driven, edge-triggered thinking. It looks fine and it is quietly broken: miss the event (your operator was restarting), get a duplicate event, or have someone delete the Deployment out from under you, and an edge-triggered handler either does nothing or does the wrong thing twice. The fix is to never ask "what just happened?" and always ask "what's the difference between what I want and what exists right now?" — then act only on that difference. That is exactly what the code below does, and it's why re-running it is always safe. Here is the heart of every operator, with the cluster replaced by a plain dict so it runs offline. The reconcile function takes the desired Custom Resource and the actual world, and makes actual match — creating what's missing, updating what drifted, and doing nothing when already correct (idempotence). Study this and you understand Kubebuilder's Reconcile():

reconcile: compare want vs have, act only on the gap want vs have desired vs actual missing → created drifted → updated equal → noop someone deletes it delete drops you back to "missing" → recreated next loop (self-heal) Three outcomes, one rule. Reconcile branches on the gap: create if missing, update if drifted, do nothing if equal — and a deletion simply reopens the "missing" branch, so the object heals itself on the next pass.
🗺️ How to read this diagram

Start at the left box and follow the arrows right: the single comparison fans into exactly three possible results.

  • The left blue box is the one comparison every reconcile makes — the desired object (want) against what actually exists (have).
  • The three coloured boxes are the only outcomes: green created when the object is missing, amber updated when it drifted, grey noop when it already matches. Colour signals how much work each does — green/amber act, grey does nothing.
  • The dashed curved arrow is the aha: if something deletes the object, the next comparison lands on "missing" again and re-creates it. Nobody re-ran anything — that's self-healing, and it works precisely because the loop acts on the present gap, not on remembered events.

In short: create / update / noop are the whole vocabulary of reconcile; because it only acts on the current gap, re-running is always safe and deletions self-heal.

python · an idempotent, level-triggered reconcile loop (offline-runnable, no cluster)
reconcile.pydef desired_deployment(cr):
    """Translate a high-level LLMService CR into the low-level object it implies."""
    return {"name": cr["name"], "replicas": cr["spec"]["replicas"],
            "image": f"vllm:{cr['spec']['model']}"}

def reconcile(cr, cluster):
    """Make the cluster match one LLMService. Returns the action taken.
    LEVEL-TRIGGERED: acts on the current gap. IDEMPOTENT: safe to re-run."""
    want = desired_deployment(cr)
    have = cluster.get(want["name"])
    if have is None:
        cluster[want["name"]] = dict(want)      # create what's missing
        return "created"
    if have != want:
        cluster[want["name"]].update(want)      # update drifted fields
        return "updated"
    return "noop"                                # already correct -> do nothing

# --- drive it like the control loop would ---
cr = {"name": "chat", "spec": {"model": "llama-3-8b", "replicas": 3}}
world = {}
print(reconcile(cr, world))          # created
print(reconcile(cr, world))          # noop   (idempotent: nothing changed)
cr["spec"]["replicas"] = 5
print(reconcile(cr, world))          # updated (drift closed)
del world["chat"]                   # something deleted our Deployment...
print(reconcile(cr, world))          # created (self-heals on next loop)
created
noop
updated
created
Idempotence is the whole gameNotice the loop can run any number of times and only acts when there's a real gap — that's why re-running is safe and why a deleted Deployment self-heals on the next reconcile. Real operators add: writing status back to the CR, finalizers for cleanup on delete, owner references so children are garbage-collected, and requeue with backoff on transient errors. But the skeleton — desired vs actual, act on the gap, do nothing when equal — is exactly the offline model above. Verify SDK specifics vs docs.

4 · Writing a controller: the conceptual walkthrough

You now hold the hardest 20% of an operator — the reconcile logic — so what's the other 80%? It's the wiring that keeps that little function fed with reality and lets it write reality back: waking it up when something changes, telling users what it decided, and cleaning up gracefully when an object is deleted. This section walks that wiring conceptually, and the good news is you will write almost none of it by hand — but you must understand it, because getting these lifecycle pieces wrong is how real operators leak resources and lie to their users.

A helpful analogy: your reconcile function is a skilled chef, but a restaurant is more than a chef. Someone has to seat customers and pass tickets to the kitchen (the watch), the kitchen has to send dishes out and call back "table 5 is ready" (status and events), and at closing time someone has to make sure the gas is off and the fridge locked before the doors are bolted (finalizers). The chef is the interesting, domain-specific part; the rest is the standard machinery every restaurant needs. An operator framework hands you a fully-staffed restaurant and asks only that you be the chef — you fill in Reconcile(), it runs the front-of-house.

The trap to avoid is treating the lifecycle pieces as optional garnish. It's tempting to write the reconcile body, see Pods appear, and declare victory — skipping status, skipping finalizers. That produces an operator that works in the demo and betrays you in production: with no status, users run kubectl describe and see nothing about why their service is stuck; with no finalizer, deleting the Custom Resource silently orphans the cloud bucket or DNS record it created, and you find out on the invoice. So read the table below not as trivia but as the checklist that separates a toy from something you'd let run unattended. A real operator wires that reconcile function to the API server. Conceptually the steps are the same whatever the SDK (Kubebuilder/Operator SDK in Go, or Kopf/client libraries in Python):

the framework runs the plumbing · you write only the middle box Watch wake on change reconcile() YOU write this Status + events tell the user Finalizers: clean up on delete + periodic resync + requeue w/ backoff You are only the chef. Watch, status/events, finalizers and requeue are standard machinery the SDK scaffolds; the one green box — reconcile() — is the domain logic you actually fill in.
🗺️ How to read this diagram

Follow the arrows left-to-right along the top row, then note the extras below — everything except the green box is provided for you.

  • The left blue box (Watch) wakes the loop whenever your kind or its children change, feeding reality in.
  • The centre green box (reconcile()) is the only part you write — the idempotent create/update/noop logic from section 3. It is deliberately highlighted because it is the whole of your job.
  • The right grey box (Status + events) writes what the operator did back to the object so users see it via kubectl describe. The amber strip and the side labels — finalizers, periodic resync, requeue-with-backoff — are the lifecycle safety nets the framework owns; skipping them is what turns a demo operator into one that leaks infra in production.

In short: Kubebuilder/Operator SDK/Kopf hand you a fully-staffed restaurant; you supply the chef (reconcile()) and let the framework run watch, status, and cleanup.

StepWhat you doWhy
1. Define the CRDSchema for your kind (spec + status)Gives the API server the new noun
2. WatchSubscribe to add/update/delete of your kind (and its children)So the loop wakes on relevant changes
3. ReconcileDiff desired vs actual; create/update/delete children idempotentlyThe control-loop body (section 3)
4. Status + eventsWrite observed state back to the CR; emit eventsUsers see what the operator did
5. FinalizersClean up external resources before the CR is deletedNo orphaned infra on delete
python · the watch→reconcile wiring, in pseudocode (illustrative; needs a cluster + SDK)
operator_loop.py# Pseudocode for the operator loop. Real code uses Kubebuilder (Go) or Kopf (Python).
for event in watch(kind="LLMService"):        # 2. wake on add/update/delete
    cr = event.object
    try:
        action = reconcile(cr, live_cluster)    # 3. the idempotent body
        set_status(cr, ready=True, note=action) # 4. write status back
    except TransientError:
        requeue(cr, backoff=True)               # retry later, don't crash
# The loop also periodically re-reconciles everything (resync) so it's level-triggered,
# not dependent on catching every event.
You rarely start from a blank fileNobody hand-writes the watch/cache/queue machinery. Kubebuilder and the Operator SDK scaffold the CRD, RBAC, and the manager, leaving you to fill in Reconcile() — the exact function you modeled offline. In Python, Kopf lets you decorate a handler. Focus your effort on correct, idempotent reconcile logic and good status reporting; let the framework own the plumbing. Verify the current scaffolding commands against the tool's docs.

5 · How real platforms extend Kubernetes

Here's the payoff that recontextualizes the whole track: you've been an operator user since chapter 8. When you installed the Prometheus Operator for observability, or Argo Rollouts for progressive delivery, or cert-manager to mint TLS certificates, you weren't using "add-ons" — you were installing custom control loops that watch CRDs and reconcile them into real infrastructure, exactly the pattern you just built in a dozen lines of Python. The reason this lesson comes near the end of the Kubernetes track is that you can finally see what those tools were all along.

The mental model to lock in is operational knowledge as software. Think about what it takes to run production Postgres: a seasoned DBA carries a runbook — how to provision, when and how to back up, how to promote a replica when the primary dies, how to upgrade a version without losing data. Historically that knowledge lived in a human's head and a wiki page, and it executed only when someone was awake to run it. A database operator takes that runbook and compiles it into a control loop: the knowledge now runs continuously, at 3 a.m., without anyone paging the DBA. That is the deep idea — an operator is a runbook that reconciles itself, turning "steps a person performs" into "state a loop maintains."

Don't mistake this for a niche, exotic corner of Kubernetes. The common misconception is that operators are something only platform wizards touch; in reality the modern cluster is mostly operators — the pattern is how the ecosystem distributes expertise. Every entry in the table below is a team packaging their hard-won domain knowledge so you get it as a short Custom Resource instead of a manual procedure, and recognizing them as instances of one pattern is what lets you reason about, debug, and choose between them. You have already been using operators throughout this track — that's the point. The pattern is how the ecosystem ships operational knowledge as software:

before: knowledge in a human's head after: knowledge as a control loop DBA runbook (wiki) provision · back up fail over · upgrade runs only when awake compile operator short Custom Resource in → self-healing Postgres out reconciles 24/7, unattended An operator is a runbook that reconciles itself. The manual, awake-only procedure on the left becomes a control loop on the right — the same expertise, now running continuously at 3 a.m. without paging anyone.
🗺️ How to read this diagram

Read it left-to-right as a before/after: the red panel is the old way, the arrow is the transformation, the green panel is what an operator gives you.

  • The left red panel is operational knowledge trapped in a human — a DBA's runbook (provision, back up, fail over, upgrade) that only executes when someone is awake to run it.
  • The middle arrow ("compile") is the key move: that runbook is turned into software — a controller with a reconcile loop.
  • The right green panel is the payoff: a team submits a short Custom Resource and gets self-healing, backed-up Postgres out, with the loop reconciling continuously and unattended. The red-vs-green colour is the whole point — "awake-only" becomes "24/7".

In short: every operator in the table below is this picture — a team's hard-won runbook compiled into a control loop you consume as a one-line Custom Resource.

Operator / CRDsWhat it managesSeen in
Prometheus OperatorPrometheus, ServiceMonitor, PrometheusRuleK8 (observability)
Argo Rollouts / Argo CDRollout, Application CRDsK10 / K6 (GitOps)
cert-managerCertificate CRDs → issued + rotated TLS certsIngress/mesh TLS
External Secrets / Sealed SecretsExternalSecret / SealedSecret → real SecretsK9 (security)
Cloud DB / Kafka / Elastic operatorsStateful software with backups, failover, upgradesData platforms

The recurring win: a database operator encodes a DBA's runbook — provision, take backups, fail over, upgrade — as a controller, so a team gets 'production Postgres' from a short Custom Resource instead of a wiki page of manual steps. That is the operator value proposition: operational knowledge as code, reconciled continuously.

Prefer a mature operator over your own for stateful softwareRunning stateful systems (databases, Kafka, search) on Kubernetes correctly — with backups, failover, safe upgrades — is genuinely hard. For common software, a well-maintained existing operator has already encoded years of that hard-won knowledge; using it is almost always better than a hand-rolled StatefulSet plus scripts. Verify the operator's maturity, support, and version compatibility before adopting.

6 · When to build an operator — and when not to

Now that you know how to build an operator, the most valuable skill is knowing when not to. There's a predictable moment on every platform team: someone learns the pattern, gets excited, and proposes "let's build an operator for that." It's the Kubernetes version of "when all you have is a hammer, everything looks like a nail" — and it produces expensive, permanently-running, over-permissioned components that solve problems a ten-line Helm chart already solved. This section is the antidote: a clear-eyed cost/benefit test so your enthusiasm for the pattern doesn't cost your team a maintenance burden they'll resent.

Frame it as a genuine cost, not a free capability. An operator isn't a script you run and forget — it's a long-lived service that sits in your cluster with broad RBAC permissions (it has to create and delete Deployments, Secrets, and more), which means it's another thing to version, patch for CVEs, monitor, and reason about when the cluster misbehaves. You are, in effect, hiring a full-time robot employee and taking on the duty to manage it forever. That's completely worth it when the robot does a job no simpler tool can — and pure overhead when it doesn't. So the question is never "could I build an operator for this?" (you almost always could) but "does this job actually need one?"

The reliable tell is whether the problem is a control-loop problem. An operator earns its keep only when there's genuine continuous reconciliation of state plus real operational logic — the backup/failover/rotate/self-heal kind of work that has to keep happening, unattended, in response to a drifting world. If instead you just need to template and install a bundle of manifests, that's Helm (K6). If you need to run a task on a schedule, that's a CronJob. If it's a one-time setup, it's a script. The honest test that cuts through the excitement: do you have domain operational logic that must run continuously and automatically, that plain Kubernetes objects can't express? Operators are powerful and are also a maintained piece of software running in your cluster with broad permissions. Building one is a real, ongoing commitment.

the honest test: is this a control-loop problem? continuous reconciliation + real ops logic? yes no build / adopt an operator install manifests → Helm schedule → CronJob · once → script "could I build one?" is always yes — ask "does it need one?" One gate, two exits. Only a genuine control-loop problem — continuous reconciliation plus real operational logic — earns an operator; everything else is Helm, a CronJob, or a script.
🗺️ How to read this diagram

Start at the diamond at the top and follow the branch that matches your problem.

  • The diamond is the single question that decides everything: does the job need continuous reconciliation of state plus real operational logic (backup, failover, rotate, self-heal)?
  • The green "yes" branch leads to building or adopting an operator — the only case where its permanent cost is justified.
  • The grey "no" branch is where most requests actually land: templating and installing manifests is Helm, a scheduled task is a CronJob, a one-time setup is a script. The bottom caption is the trap to avoid — you could build an operator for almost anything, so the real question is whether the problem needs one.

In short: route on "is this a control loop?" — yes → operator; no → the simplest of Helm / CronJob / script.

Build/adopt an operator when……don't build one when…
You repeat a multi-step operational runbook often (backup, failover, rotate)A Deployment/Job/CronJob already expresses it
You want a high-level abstraction for many teams (self-service)One team, one app — a Helm chart is enough
State must be continuously reconciled/self-healedA one-time setup script would do
A mature operator exists for your stateful software (adopt it)You'd be reinventing a well-supported one
Don't reach for an operator when Helm or a CronJob will doThe most common mistake is building an operator for something that isn't a control-loop problem. If you just need to template and install a set of manifests, that's Helm (K6). If you need to run a task on a schedule, that's a CronJob. Operators earn their cost only when there's ongoing reconciliation of state and real operational logic. Reach for the simplest tool that expresses the need; build an operator when — and only when — you genuinely need a custom control loop. Verify tooling choices against current docs.

✓ Checkpoint — you can move on when you can…

  • Explain the reconcile control loop and why level-triggered + idempotent makes it robust.
  • Define a CRD and a Custom Resource, and say why a CRD alone does nothing.
  • Trace the offline reconcile: created → noop → updated → self-heal, and why each result happens.
  • List the conceptual steps to write a controller (CRD, watch, reconcile, status, finalizers).
  • Name three real operators you've used in this track and what they manage.
  • Decide when to build/adopt an operator vs use Helm or a CronJob instead.
✓ Knowledge check

Someone deletes the Deployment that your operator created from an LLMService. Minutes later it's back, and nobody re-ran anything. Explain the mechanism, and what property of the reconcile code makes this safe.

Show answer
The operator's control loop is level-triggered: on its next reconcile (triggered by a watch event or the periodic resync) it compares desired (the LLMService still says 3 replicas) to actual (the Deployment is gone), sees the gap, and recreates the Deployment — self-healing without anyone acting. What makes this safe is idempotence: reconcile only acts on the current gap and does nothing when desired == actual, so running it any number of times (or after missing events) converges to the right state rather than duplicating or thrashing. That's exactly the created → noop → updated → created behavior in the offline model.
✓ Knowledge check

A team wants to 'build an operator' to install their app's 5 manifests with a couple of configurable values, and to run a nightly cleanup job. Is an operator the right tool? What would you suggest?

Show answer
No — neither need is a control-loop problem. Templating and installing manifests with configurable values is exactly what Helm (K6) does; a chart with a values.yaml covers it with far less to maintain than a custom controller. A nightly cleanup is a CronJob. An operator is justified only when you need continuous reconciliation of custom state and real operational logic that plain objects can't express (backup/failover/rotation, or a self-service abstraction for many teams). Building an operator here would add a permanently-running, broadly-permissioned component to maintain for no benefit — pick the simpler tool.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Explain the control loopBeginner

Context: Every k8s controller — and every operator — is the same loop; naming it is the first step.

Your task: In your own words, describe the reconcile loop and why it's called level-triggered and declarative.

Requirements:

  • State the four steps of the loop
  • Define level-triggered vs edge-triggered
  • Explain why that makes controllers robust
  • Connect it to a built-in like the Deployment controller

💡 Hint: It acts on the current gap, not on a one-time event.

Show solution

The loop: (1) read desired state (spec), (2) observe actual state (the world), (3) diff them, (4) act to close the gap — then repeat forever.

Level- vs edge-triggered: edge-triggered acts on a one-off event ('a pod died'); level-triggered acts on the current gap ('desired 3, have 2'). Level-triggered is robust because even if the controller missed events (it was down), it still fixes the gap on the next pass.

Declarative: you state the goal; the controller continuously drives toward it. The Deployment controller is exactly this — you say '3 replicas', it keeps 3 alive no matter what kills them.

Exercise 2 · Define a CRD and a Custom ResourceIntermediate

Context: Extending the API with your own kind is the foundation of every operator.

Your task: Define a CRD for a custom kind and write one instance, and explain what does (and doesn't) happen on apply.

Requirements:

  • A CRD with a small validated spec
  • A Custom Resource instance of that kind
  • State what kubectl can now do
  • State why no workload is created yet

💡 Hint: A CRD is validated storage; behavior needs a controller.

Show solution
kind: CustomResourceDefinition
metadata: { name: llmservices.ai.example.com }
spec:
  group: ai.example.com
  names: { kind: LLMService, plural: llmservices }
  scope: Namespaced
  versions: [ { name: v1, served: true, storage: true, schema: { openAPIV3Schema:
    { type: object, properties: { spec: { type: object, properties:
      { model: {type: string}, replicas: {type: integer} } } } } } } ]
---
kind: LLMService
apiVersion: ai.example.com/v1
metadata: { name: chat }
spec: { model: llama-3-8b, replicas: 3 }

After apply, kubectl get llmservice works and the object is validated + stored in etcd. But no Pod is created — a CRD is only structured storage; behavior requires a controller that watches this kind. Needs a cluster; verify schema fields vs docs.

Exercise 3 · Implement an idempotent reconcileAdvanced

Context: Idempotent reconcile is the single most important property of operator code.

Your task: Write offline Python that reconciles a desired CR into an actual world and returns created/updated/noop, and demonstrate self-heal.

Requirements:

  • Translate the CR into the object it implies
  • Create if missing, update if drifted, noop if equal
  • Show it's safe to run repeatedly
  • Show self-heal after a delete

💡 Hint: Compare want vs have; only act on a real difference.

Show solution
def reconcile(cr, world):
    want = {'name': cr['name'], 'replicas': cr['spec']['replicas']}
    have = world.get(want['name'])
    if have is None:
        world[want['name']] = dict(want);  return 'created'
    if have != want:
        world[want['name']].update(want);  return 'updated'
    return 'noop'

cr = {'name':'chat','spec':{'replicas':3}}; w = {}
reconcile(cr, w)          # created
reconcile(cr, w)          # noop
cr['spec']['replicas']=5; reconcile(cr, w)   # updated
del w['chat']; reconcile(cr, w)              # created (self-heal)

Because it only acts on the gap, re-running is safe (noop when equal) and a deleted object is recreated on the next pass — that's idempotence + level-triggering. Runs offline; no cluster needed.

Exercise 4 · Design the full controller (conceptually)Expert

Context: A production operator is reconcile plus the lifecycle concerns around it.

Your task: Describe the pieces a real controller adds around the reconcile body and why each matters.

Requirements:

  • Watch (and periodic resync)
  • Status subresource + events
  • Finalizers for cleanup on delete
  • Owner references and requeue-with-backoff

💡 Hint: These are the difference between a toy loop and a safe operator.

Show solution

Watch + resync: subscribe to add/update/delete of your kind and its children, and periodically re-reconcile everything so the loop stays level-triggered even if events are missed.

Status + events: write observed state back to the CR's status and emit Kubernetes events so users can kubectl describe and see what the operator did/why it's waiting.

Finalizers: a finalizer blocks deletion until the operator cleans up external resources (e.g. a cloud bucket) — otherwise you orphan infra.

Owner references + backoff: set the CR as the owner of children so they're garbage-collected with it; on transient errors, requeue with backoff rather than crashing. Frameworks (Kubebuilder/Kopf) provide all this scaffolding — you fill in reconcile. Verify SDK specifics vs docs.

Exercise 5 · Adopt vs build for stateful softwareProfessional

Context: A team wants production Postgres on Kubernetes and is debating a hand-rolled StatefulSet vs an operator.

Your task: Make a reasoned recommendation, covering what an operator encodes and the risks of rolling your own.

Requirements:

  • List the operational tasks production Postgres needs
  • Explain what a mature DB operator encodes
  • State the risks of a hand-rolled StatefulSet + scripts
  • Give a recommendation with caveats

💡 Hint: Backups, failover, and safe upgrades are the hard, ongoing parts.

Show solution

What production Postgres needs: provisioning, automated backups + restore, failover (promote a replica on primary loss), safe version upgrades, connection routing, and monitoring — continuously, not once.

What a mature operator encodes: exactly those runbooks as a control loop, so a short Postgres CR yields a self-healing, backed-up, failover-capable cluster — years of DBA knowledge as software.

Risks of rolling your own: a StatefulSet + scripts gives you storage and identity but not backup/failover/upgrade logic; you'd be reimplementing (and forever maintaining) the hard parts, and data systems are unforgiving of bugs.

Recommendation: adopt a mature, well-supported operator rather than build — after verifying its maturity, community, and version compatibility with your cluster. Build custom only if no suitable operator exists and the operational logic is genuinely yours.

Exercise 6 · Decide the extend-Kubernetes strategy for a platformIndustry scenario

Context: Representative scenario: your platform team is asked to give product teams 'one-line' LLM services and to run several stateful systems, and there's enthusiasm to 'build operators for everything'.

Your task: Produce a strategy for how to extend Kubernetes: where a custom operator is justified, where to adopt existing ones, and where Helm/CronJobs suffice — with the failure mode each choice avoids.

Requirements:

  • A self-service LLMService abstraction — build or not?
  • Stateful systems (DB/Kafka/search) — adopt existing operators
  • Simple installs and scheduled tasks — Helm / CronJob
  • The maintenance/permission cost of every operator you run
  • Name the failure mode each choice avoids

💡 Hint: Operators are for continuous reconciliation + real ops logic; everything else is simpler.

Show solution

Self-service LLMService (build — carefully): if many teams need a high-level 'run this model' abstraction that reconciles Deployment+Service+HPA+autoscaling policy continuously, a custom operator is justified — it encodes your serving runbook and self-heals. Scaffold with Kubebuilder/Kopf; invest in idempotent reconcile, status, and finalizers. → avoids every team hand-writing (and drifting) low-level manifests.

Stateful systems (adopt, don't build): use mature operators for Postgres/Kafka/Elastic — they encode backup/failover/upgrade. → avoids reinventing hard, data-loss-prone operational logic.

Simple installs & tasks (Helm / CronJob): templated manifests → Helm (K6); scheduled cleanup/reporting → CronJob. → avoids paying operator complexity for non-control-loop problems.

The cost lens: every operator is a permanently-running, broadly-permissioned component you must maintain, upgrade, and secure (K9). So the strategy is: build few, adopt many, template the rest — reach for a custom control loop only when there's genuine continuous reconciliation and domain ops logic. Verify all CRD/SDK/operator specifics against current docs; this ecosystem drifts fast.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in