Skip to main content
We have defined our platform API with CustomResourceDefinitions (CRDs). That gives you a typed API surface — but without a controller, creating a Database custom resource (CR) only stores YAML in etcd. No external database is provisioned, no network is configured, and no status or cleanup is performed. Controllers bridge the declarative specification in the API server to the actual external resources. In this lesson you’ll learn:
  • Why CRDs are only useful when paired with controllers.
  • How the reconciliation loop works and its guiding principles.
  • Controller architecture and how informs, queues, and reconcilers interact.
  • The canonical Reconcile function pattern and a practical Go example.
  • How to report status with conditions and observedGeneration.
  • How to implement finalizers to guarantee cleanup.
  • How to pick an operator framework.
This lesson focuses on controller patterns and implementation best practices. Use a framework (Kubebuilder, Operator SDK, or Metacontroller) to avoid boilerplate and concentrate on business logic.
Example scenario A team publishes a Database CRD with validation, printer columns, and a clear schema. They advertise: You can now create databases using kubectl apply. A developer tries:
It looks like a success, but nothing happened behind the scenes: the YAML is persisted in etcd but no provisioning, status updates, or cleanup logic run.
A presentation slide titled "CRDs Alone Are Just etcd Data" showing four issues—No database is created, No status updates, No drift correction, and No cleanup on delete—each with a short explanatory note.
A CRD is the menu; the controller is the kitchen that actually prepares the order.
A presentation slide titled "CRDs Alone Are Just etcd Data" that uses a restaurant analogy to explain Kubernetes concepts. It shows three panels: CRD = Menu (describes what you can order), CR = Order ("I'd like a medium PostgreSQL, please."), and Controller = Kitchen (it actually makes the food).
The reconciliation loop The reconciliation loop is the core concept behind all controllers. It continuously observes the API server and attempts to make reality match the desired state described in CRs. The loop can be summarized into four clear phases:
  1. Watch / Observe — receive notifications (via an Informer) when resources change.
  2. Compare — compute the delta between desired state (CR.spec) and actual state (cluster or external system).
  3. Act — create, update, or delete resources to close the gap.
  4. Update status — write current progress and conditions back to CR.status.
Three principles govern robust controllers:
  • Level-triggered (not edge-triggered): react to state, not individual events. If an event is missed, the next reconcile sees the current state and fixes it.
  • Idempotency: repeated reconciles must be safe and not cause duplicate or conflicting actions.
  • Eventual consistency: the system converges toward the desired state over time; transient failures are retried.
Example: if provisioning takes five minutes, set Progressing or Provisioning in status. Reconciles continue until the database is Ready.
A presentation slide titled "The Reconciliation Loop: Desired vs Actual" showing three key principles — "Level-triggered, not edge-triggered," "Eventual consistency," and "Idempotent operations" — each with a short explanation and a colored triangular icon. The slide is branded © KodeKloud.
Quick reference: reconciliation phases Controller architecture Controllers are composed of a small stack of cooperating components:
  • API Server — the source of truth where desired state is persisted.
  • Informer — watches API server changes and keeps a local cache to reduce API traffic.
  • Work Queue — deduplicates events, rate-limits retries, and ensures single-running reconcile per resource.
  • Reconciler — your custom logic that compares and acts on resources.
When the Informer notices a change it enqueues a reconcile request. The Work Queue deduplicates rapid changes so only one reconcile runs for the latest state. The framework supplies resilience (retries/backoff), scalability (multiple workers), and consistency semantics — you implement the Reconciler.
A vertical "Controller Architecture" diagram with four colored, numbered blocks (1–4) on the left linked to labeled components on the right: API Server, Informer, Work Queue, and Reconciler with brief descriptions. The slide is copyright KodeKloud.
A presentation slide titled "Controller Architecture" showing three colored arrows labeled Resilience, Consistency, and Scalability with brief notes explaining each. A footer reads that you write the Reconciler while the framework handles watching, queueing, and retries.
The Reconcile function pattern Reconcile is where you express the desired business logic. A typical Reconcile function follows this pattern:
  1. Fetch the resource from the API server (it might have been deleted).
  2. If metadata.deletionTimestamp is set, run finalizer/cleanup logic and return.
  3. Compare spec to actual state and make necessary actions (create/update/delete).
  4. Update status (conditions, observedGeneration, etc.) via the status subresource.
Here is an idiomatic controller-runtime (Go) example that follows those steps:
Note: function names and signatures vary slightly by framework, but the pattern is the same. Status, conditions, and observedGeneration Status is how controllers communicate state to users and automation. Kubernetes-style conditions provide a concise, extensible structure:
  • type (e.g., Available, Progressing, Degraded)
  • status (one of "True", "False", "Unknown")
  • reason and message for human-readable context
  • lastTransitionTime for timing
  • observedGeneration (optional) to indicate which metadata.generation the controller has processed
Example status block:
Use observedGeneration so consumers can compare metadata.generation with status.observedGeneration to know whether the current status reflects the latest spec.
A presentation slide titled "Status Updates and observedGeneration" with a blue rounded box labeled "observedGeneration Pattern." An arrow points to the note: "Compare metadata.generation with status.observedGeneration to see if changes are processed."
Finalizers and cleanup Finalizers guarantee that external resources are cleaned up before the API server permanently deletes the CR. When a user deletes a CR the API server sets metadata.deletionTimestamp but retains the object while finalizers are present. Your controller must:
  • Detect deletionTimestamp.
  • Perform external cleanup (delete cloud resources, credentials, networking).
  • Remove the finalizer from the CR and update it.
  • Only then will the API server remove the object from etcd.
A simple finalizer flow:
If cleanup fails (for example, cloud APIs are down), the finalizer will remain and the resource will stay in Terminating state. Implement retries with backoff and robust logging so you can recover or manually resolve stuck objects.
Operator frameworks Most teams leverage a framework rather than writing all plumbing from scratch. Here’s a comparison to help you choose: Pick the framework that matches your language skills, testing needs, and integration surface. Key takeaways
  • CRDs alone are just declarative data stored in etcd; controllers make CRs do work.
  • Reconciliation must be level-triggered, idempotent, and target eventual consistency.
  • Use status.conditions and observedGeneration to communicate progress and the reconciliation state.
  • Finalizers ensure external resources are cleaned up before the CR is removed.
  • Use a framework (Kubebuilder, Operator SDK, Metacontroller) to avoid boilerplate and follow Kubernetes best practices.
A presentation slide titled "Key Takeaways" with four colorful numbered bullets.The bullets summarize Kubernetes concepts: controllers are needed for CRDs to have effect, reconciliation must be idempotent, status/observedGeneration convey resource state, and finalizers ensure cleanup before deletion.
Further reading and references

Watch Video