Controller in Kubernetes

1. Why Do We Need Controllers?

Kubernetes is a declarative system. Users simply declare the Desired State — “spin up 3 nginx instances” — without specifying how to do it.

The API Server stores this Desired State in etcd, but it does not actually reconcile the Current State to match the Desired State.

💡 Controller = the entity that bridges the gap between Desired State and Current State

Why this architecture? (Causal reasoning)

  • Cause: In a distributed system, nodes can die, networks can disconnect, and Pods can vanish at any moment. The current state is inherently unstable.
  • Effect: Rather than “issue a command → done,” you need a structure that continuously monitors and automatically restores state whenever a discrepancy arises.
  • Analogy: A thermostat. Set it to 25°C and it continuously measures the current temperature, turning heating or cooling on whenever it deviates from the target.

2. Reconciliation Loop (Control Loop)

The core operating principle of a controller:

1. Observe Current State  → "There are currently 2 nginx Pods running"
2. Check Desired State    → "etcd says there should be 3"
3. Calculate Diff         → "We're 1 short"
4. Execute Reconcile      → "Create 1 more Pod"

This loop is called the Reconciliation Loop or Control Loop.


3. Change Detection: Watch and Informer

image.png

3-1. Polling vs Event-driven

There are two ways a controller can detect that the current state has changed:

MethodDescriptionDrawback
PollingPeriodically asks the API Server: “what’s the current state?”Number of controllers × number of resources = API Server overload
Watch (Event-driven)Maintains a long-lived connection to the API Server and receives events pushed on changeEvents can be lost if the connection drops
  • Kubernetes’s choice: Watch (Event-driven)
  • Reason: No traffic when nothing changes. API Server load scales with change frequency, not resource count — making it stable even in large clusters.

3-2. Informer = List-Watch + Local Cache

Watch alone has two shortcomings:

ProblemSolution
Watch connection drops → events are lostList-Watch pattern: fetch a full snapshot via List on startup, then switch to Watch. On disconnect, re-sync from List again
Multiple controllers querying the API Server simultaneously → increased loadLocal cache: the Informer stores data locally; controllers read from the cache instead of the API Server

💡 Informer = List-Watch + Local Cache, bundled together

3-3. Work Queue (a separate structure outside the Informer)

A separate structure attached outside the Informer. It solves consistency problems in event processing.

Why a Work Queue is needed (example): Three events arrive within one second: “Pod-A deleted” → “Pod-B deleted” → “Pod-A created”

  • If Reconcile is called immediately for each event → it reacts to the deletion events by creating extra Pods, only for them to come back via automatic restart → more Pods than intended, causing a consistency problem.

How the Work Queue solves this:

  • Instead of the event itself, an object key (e.g., default/Pod-A) is enqueued
  • If the same key already exists in the queue, it is deduplicated
  • Reconcile dequeues a key and makes decisions based on the latest local cache state at that point in time

Difference from SQS:

  • SQS messages are persistent — they remain in the queue even if the consumer dies.
  • The K8s Work Queue is in-memory — if the controller dies, the queue is lost.
  • On controller restart, stability is guaranteed by the Informer’s List operation (full re-sync).

3-4. Component Role Summary

ComponentProblem it solvesCore mechanism
List-Watch (inside Informer)Event loss when Watch connection dropsFull re-sync via List on reconnect
Local Cache (inside Informer)Query load on the API ServerRead from cache
Work Queue (outside Informer)Event duplication, consistencyKey-based dedup + decisions based on latest cache
Reconcile functionActually closing the gapCompare desired vs current → execute action

4. End-to-End Flow

kubectl apply (desired state)
        │
        ▼
   API Server ──── etcd (stores desired state)
        │
        │  List (initial full sync)
        │  Watch (subsequent change event stream)
        ▼
    Informer
        │
        ├──→ Local Cache (current state snapshot, read by controllers)
        │
        └──→ Event Handler ──→ Work Queue (key-based dedup)
                                     │
                                     │  dequeue key
                                     ▼
                              Reconcile function
                         (fetch current from cache →
                          compare with desired → execute action)

5. Where Does the Informer Live?

The Informer is not a separate process or Pod. It is a library embedded within each controller’s code (part of client-go).

[Control Plane Node]
  └─ kube-controller-manager (single process)
       ├─ Deployment Controller
       │    └─ Informer (watches Deployment resources)
       ├─ ReplicaSet Controller
       │    └─ Informer (watches ReplicaSet resources)
       ├─ Node Controller
       │    └─ Informer (watches Node resources)
       └─ ...

Multiple controllers run as goroutines inside kube-controller-manager, and each controller has its own dedicated Informer.


6. Separation of Deployment Controller and ReplicaSet Controller

6-1. The Object Chain

Deployment → ReplicaSet → Pod

A Deployment does not create Pods directly — it goes through a ReplicaSet as an intermediary.

6-2. Why the separation? (Causal reasoning)

If the Deployment Controller managed Pods directly: During a rolling update — “is this Pod v1 or v2?”, “how far can I scale down v1?”, “which Pods do I keep on rollback?” — all of this logic would be tangled in one place.

With separation:

ControllerRoleCharacteristics
Deployment Controller (strategist)Creates ReplicaSets and adjusts their replicas countNever touches Pods directly. Understands rolling/rollback strategy
ReplicaSet Controller (executor)Creates/deletes Pods to match the replicas count it was givenKnows nothing about rolling updates. Just “keep 3 running” → keeps 3

💡 This is Kubernetes’s Single Responsibility Principle — the hierarchical controller pattern