Skip to main content
Think of a thermostat on the wall. You set it to seventy degrees — that’s the desired state. The thermostat reads the room temperature — that’s the actual state. If the two don’t match, the thermostat turns the heat on or off, then checks again and again, forever.
The image illustrates a thermostat process on the wall, showing a desired state of 70 degrees, an actual state of 68 degrees, and an action to turn the heat on.
Reconcile is exactly that loop applied to Kubernetes objects. Your WebApp’s spec is the set point; the cluster’s actual state — the Deployment, Service, ConfigMap, Pods, and so on — is the room temperature. The controller closes that gap on every event, every resync, and every restart.
The image illustrates the Kubernetes control loop, showing the relationship between the Desired State, Controller, and Actual State.
Reconcile is the heart of every controller you write. When implemented correctly, the rest of the operator becomes straightforward; when it’s wrong, you’ll be struggling with unpredictable behavior. Here is the Reconcile signature provided by controller-runtime — it has two inputs and two outputs:
The req contains a NamespacedName (the key of the object that needs attention). Notice what it does not contain: neither the object itself nor the event that triggered the call. This is intentional. Controllers in Kubernetes are level-triggered, not edge-triggered. You do not react to “a WebApp was created”; you react to “the WebApp named foo in namespace bar might need work — go look.” Embracing this mental model is essential.
Controllers are level-triggered: always reconcile to the current desired state by looking up the object by key, not by relying on the event payload.
The image contrasts edge-triggered and level-triggered concepts, suggesting that level-triggered is more important in the lesson.
Every reconcile starts by answering three questions:
  • What’s the desired state?
  • What’s the actual state?
  • What’s the diff (what needs to change)?
A typical reconcile body follows these four steps:
  1. Fetch the parent resource (the WebApp) from the cache using r.Get with the request’s NamespacedName.
  2. Compute the desired child objects (Deployment, Service, ConfigMap, etc.) from the WebApp spec.
  3. Create or update those child objects so they match the desired state.
  4. Update the WebApp’s status to reflect the observed (actual) state.
The image outlines three questions for reconciliation: "Desired? What should exist," "Actual? What does exist," and "Diff? What needs to change."
The first step — fetching the WebApp safely — is a small, important pattern. Use the API to get the object, and treat a not-found as a normal deletion case:
Notes:
  • webappv1 is the package generated from your CRD group/version.
  • apierrors is k8s.io/apimachinery/pkg/api/errors; use IsNotFound to distinguish deletions from transient API errors.
  • Owned children created with proper OwnerReferences are garbage-collected automatically. See the Kubernetes docs on OwnerReferences for details.
This fetch-and-bail pattern (under ten lines of Go) is the canonical approach to “fetch the parent and exit safely if it’s gone.” The reconcile loop must be idempotent. The same WebApp will be reconciled many times — on creation, on spec changes, on periodic resyncs, after controller restarts, and after transient errors. If running reconcile twice yields a different result than running it once, you have a bug.
Idempotency is critical: ensure reconcile is safe to run repeatedly. Avoid relying on side-effects that make subsequent runs behave differently.
The image illustrates the concept of idempotency in loops, stating "Run twice == run once" and mentions the importance of handling creation, spec changes, re-sync, restarts, and errors. It emphasizes that if running twice doesn't equal running once, it's considered a bug.
Return values from Reconcile control requeue behavior. Use these return patterns to express what should happen next: Example returns in code:
When r.Get returns a not-found error, the resource was deleted. If you created children with OwnerReference, Kubernetes will garbage collect them, so you typically return without error:
If you need to perform work during deletion (for example, to clean up external resources), implement finalizers and handle that lifecycle explicitly — this will be covered in the finalizers and cleanup section. In the next lesson you’ll wire up the rest of the loop: computing desired child resources, performing create-or-update operations, and updating status to reflect observed state. Links and references:

Watch Video