How does the Kubernetes Operator pattern use controller reconciliation loops to manage complex stateful applications, and what architectural mistakes lead to unreliable operators in production?
Quick Answer
An Operator extends Kubernetes with a Custom Resource Definition and a controller that watches for changes to that resource, then reconciles the actual state toward the desired state in a loop. Unreliable operators result from non-idempotent reconciliation, missing status updates, unbounded requeue storms, lack of finalizers for cleanup, and not handling partial failures during multi-step workflows.
Detailed Answer
Think of a building superintendent who checks every apartment every morning. If a tenant requested a repair, the superintendent inspects the current state, compares it to the work order, does what is needed, and notes the result. If the superintendent is interrupted mid-repair, they pick up where they left off the next morning because the work order still exists. A Kubernetes Operator controller works the same way — it continuously reconciles the world toward what the Custom Resource says it should be.
The Operator pattern was introduced to encode human operational knowledge into software. A CRD (Custom Resource Definition) defines the schema for a new Kubernetes resource — for example, a PostgresCluster resource that describes a desired database cluster. The controller watches the API server for changes to these resources and runs a Reconcile function each time a resource is created, modified, deleted, or when a requeue timer fires. The Reconcile function reads the current resource state, inspects the actual cluster state (Pods, Services, PVCs, ConfigMaps), computes the difference, and takes action to close the gap.
Internally, the controller uses an informer to cache resource state locally and a work queue to process reconciliation events. When the informer detects a change, it enqueues the resource key. The controller dequeues it and calls Reconcile. If Reconcile returns an error or a requeue request, the controller puts the key back in the queue with exponential backoff. This level-triggered design means the controller does not need to track every individual event — it just needs to know the current desired state and current actual state each time Reconcile runs.
At production scale, the most common architectural mistakes are: writing non-repeatable without side effects (idempotent) reconciliation logic that creates duplicate resources on re-runs, failing to update the status subresource with meaningful conditions so users and other controllers cannot observe progress, creating requeue storms by always returning a short requeue interval regardless of whether work is needed, skipping finalizers so external resources (cloud load balancers, DNS entries, storage) are orphaned when the Custom Resource is deleted, and attempting complex multi-step workflows in a single Reconcile call without checkpointing progress in status.
The non-obvious gotcha is that Reconcile can be called at any time for any reason — not just when the Custom Resource changes. Node pressure, informer resyncs, and dependent resource changes all trigger reconciliation. If Reconcile has side effects that are not idempotent (like sending a notification or creating a cloud resource without checking if it already exists), the operator will misbehave under real-world conditions. Architects should design every operation inside Reconcile to be safely repeatable and should test operators with chaos engineering to ensure they recover from partial failures and controller restarts.