♻️ The Reconciliation Pattern

Every Kubernetes controller implements the same pattern: Observe → Diff → Act. Controllers never push commands at the cluster; instead they continuously compare desired state (from the API server) with actual state (from the cluster or cloud) and apply the minimum change needed to close the gap.

This makes controllers idempotent and self-healing — re-running the same reconcile loop has no side-effects if the state is already correct.

👁 Observe
Watch API server for changes
⚡ Diff
Desired vs actual state
🔧 Act
Create / Update / Delete
⏳ Wait
Re-queue or watch
kube-apiserver source of desired state kube-controller-manager Deployment ReplicaSet StatefulSet Namespace Job/CronJob + 30 more… Shared Work Queue + Rate Limiter (per controller) Watch events
ℹ️ ~40 built-in controllers in one binary kube-controller-manager is a single process running ~40 controllers as goroutines. They share a single API server connection but each maintains its own informer cache and work queue. Only the leader instance is active when running HA.

🎛️ Key Built-in Controllers

ControllerWatchesReconciles
DeploymentDeployment, ReplicaSetCreates/scales ReplicaSets; manages rolling updates
ReplicaSetReplicaSet, PodEnsures spec.replicas Pods are running; creates/deletes Pods
StatefulSetStatefulSet, Pod, PVCOrdered pod creation/deletion; stable network IDs; per-pod PVCs
DaemonSetDaemonSet, Node, PodEnsures one Pod per matching node; handles node adds/removes
JobJob, PodCreates pods to completion; retries on failure up to backoffLimit
CronJobCronJob, JobCreates Jobs on schedule; enforces concurrencyPolicy; prunes old Jobs
NamespaceNamespaceFinalises namespace deletion — removes all resources in the namespace
ServiceAccountNamespace, ServiceAccountCreates default SA and token in every new namespace
NodeNodeMarks nodes NotReady; evicts pods from unreachable nodes after timeout
NodeLifecycleNode, PodAdds node.kubernetes.io/not-ready taint; triggers pod eviction
PersistentVolumePV, PVCBinds PVCs to PVs; handles reclaim policy (Delete/Retain/Recycle)
EndpointSliceService, PodMaintains EndpointSlice objects from ready pod IPs
GarbageCollectorAll resourcesDeletes owner-referenced resources when owners are gone
HorizontalPodAutoscalerHPA, metricsAdjusts spec.replicas on target based on metric thresholds
TokenCleanerSecretRemoves expired bootstrap tokens

Deployment Controller — Rolling Update Flow

When you change a Deployment's pod template the Deployment controller executes a rolling update through ReplicaSets:

# Watch what the Deployment controller does during a rollout
kubectl rollout status deployment/nginx --watch

# Under the hood:
# 1. Deployment controller creates a new ReplicaSet (rs-v2) with replicas=0
# 2. It scales rs-v2 up by maxSurge (default 25%) and rs-v1 down by maxUnavailable
# 3. Loops until rs-v2 = desired replicas and rs-v1 = 0
# 4. Old ReplicaSet is retained (for rollback) but scaled to 0

# Check ReplicaSet history
kubectl get replicasets -l app=nginx
# NAME              DESIRED   CURRENT   READY   AGE
# nginx-7d8f9c      3         3         3       5m    ← new
# nginx-5b6c7d      0         0         0       2h    ← old (kept for rollback)

# Rollback uses the old ReplicaSet directly — instant
kubectl rollout undo deployment/nginx

GarbageCollector Controller

The GarbageCollector runs a background graph of owner references. When an owner is deleted, it cascades deletion to all dependents — this is how deleting a Deployment also deletes its ReplicaSets and Pods.

# OwnerReferences — set automatically by controllers
kubectl get pod nginx-7d8f9c-abc -o jsonpath='{.metadata.ownerReferences}'
# [{"apiVersion":"apps/v1","kind":"ReplicaSet","name":"nginx-7d8f9c",
#   "uid":"...","controller":true,"blockOwnerDeletion":true}]

# Foreground deletion: owner stays until all dependents are gone
kubectl delete deployment nginx --cascade=foreground

# Background deletion (default): owner deleted first, GC cleans up dependents async
kubectl delete deployment nginx --cascade=background

# Orphan: delete owner but keep dependents
kubectl delete deployment nginx --cascade=orphan

NodeLifecycle Controller — Pod Eviction

When a node goes unreachable, the NodeLifecycle controller adds a node.kubernetes.io/not-ready taint. After the pod eviction timeout (default 5 minutes), it evicts all Pods on that node. This is why pods reappear on other nodes after a node failure.

# Key flags controlling eviction timing
--node-monitor-grace-period=40s      # time before node marked NotReady
--pod-eviction-timeout=5m0s          # (deprecated) grace period before eviction
# Modern: controlled by node taint tolerations on pods
# Default toleration: 300s for not-ready / unreachable taints

⚙️ Work Queues, Leader Election & Rate Limiting

Work Queue Internals

Each controller uses a rate-limited work queue from client-go/util/workqueue. Events from the informer (watch cache) are enqueued as keys (namespace/name). The reconciler dequeues keys and processes them — deduplication means multiple rapid changes to the same object produce a single reconcile.

Deduplication

If an object is modified 10 times before the reconciler runs, only one reconcile fires — it always reads the latest state from the cache.

Rate Limiting

Failed reconciles are re-queued with exponential back-off (5ms → 1000s cap). Prevents a broken object from hammering the API server.

Parallel Workers

Each controller runs N goroutine workers (default varies). Increasing workers speeds up reconciliation at the cost of more API server load.

Requeue After

Controllers can request re-queue after a fixed duration (e.g. HPA re-queues every 15s to check metrics even with no change events).

// Simplified work queue pattern used by all controllers
func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
    defer c.queue.ShutDown()
    // Start informers
    go c.podInformer.Run(stopCh)
    // Wait for cache sync
    cache.WaitForCacheSync(stopCh, c.podInformer.HasSynced)
    // Start N worker goroutines
    for i := 0; i < workers; i++ {
        go wait.Until(c.runWorker, time.Second, stopCh)
    }
    <-stopCh
}

func (c *Controller) runWorker() {
    for c.processNextItem() {}
}

func (c *Controller) processNextItem() bool {
    key, quit := c.queue.Get()
    if quit { return false }
    defer c.queue.Done(key)

    err := c.reconcile(key.(string))
    if err != nil {
        // Exponential back-off re-queue
        c.queue.AddRateLimited(key)
    }
    return true
}

Leader Election

In HA control planes, multiple kube-controller-manager instances run simultaneously. Only the leader runs the reconcile loops — the others are hot-standby. Leadership is held via a Lease object in the kube-system namespace, renewed every few seconds.

# View the current leader lease
kubectl get lease kube-controller-manager -n kube-system -o yaml
# spec:
#   holderIdentity: master-1_abc-uuid   ← current leader
#   leaseDurationSeconds: 15
#   renewTime: "2024-01-15T12:34:56Z"
#   acquireTime: "2024-01-15T10:00:00Z"

# If the leader fails to renew within leaseDurationSeconds,
# a standby instance acquires the lease and becomes leader
# (typical failover: 15–30 seconds)
⚠️ Single-node control planes have no HA Leader election only matters with multiple controller-manager replicas. A single control-plane node has no failover — the entire control plane is down if it fails. Use 3-node control planes in production.

Key kube-controller-manager Flags

# Concurrency / workers per controller
--concurrent-deployment-syncs=5     # default 5 deployment workers
--concurrent-replicaset-syncs=5
--concurrent-endpoint-syncs=5
--concurrent-gc-syncs=20            # GarbageCollector workers

# Node eviction / health
--node-monitor-grace-period=40s     # wait before marking node NotReady
--node-monitor-period=5s            # how often to check node status

# Leader election
--leader-elect=true
--leader-elect-lease-duration=15s
--leader-elect-renew-deadline=10s
--leader-elect-retry-period=2s

# HPA
--horizontal-pod-autoscaler-sync-period=15s
--horizontal-pod-autoscaler-tolerance=0.1

# Namespace lifecycle
--namespace-sync-period=5m0s

📊 Observability & Production Tips

Key Metrics

MetricAlert thresholdWhat it means
workqueue_depth{name="deployment"}> 100 sustainedController falling behind — too many changes or too few workers
workqueue_retries_totalHigh rateReconcile errors causing repeated re-queues
workqueue_queue_duration_seconds p99> 30sItems waiting too long to be processed
rest_client_requests_total{code="429"}> 0API server rate-limiting the controller — reduce workers or tune QPS
node_collector_unhealthy_nodes_in_zone> 0Nodes failing health checks — may trigger evictions

Common Issues & Fixes

# Issue: Deployment stuck in rollout — pods not becoming ready
kubectl rollout status deployment/myapp
# Waiting for deployment "myapp" rollout to finish: 1 out of 3 new replicas updated

# Diagnose: check events and pod status
kubectl describe deployment myapp
kubectl get pods -l app=myapp --sort-by='.status.startTime'
kubectl describe pod myapp-<new-rs>-<hash>

# Issue: Pods not evicted after node failure (5min default delay)
# Speed up for testing — reduce toleration on pods
spec:
  tolerations:
    - key: "node.kubernetes.io/not-ready"
      operator: "Exists"
      effect: "NoExecute"
      tolerationSeconds: 30   # evict after 30s instead of 300s

# Issue: HPA not scaling
kubectl describe hpa myapp-hpa
# Check: is metrics-server running?
kubectl top pods -n my-namespace
# Check: are resource requests set on pods? (HPA requires them)

Increase Workers for Large Clusters

On clusters with thousands of Deployments, increase --concurrent-deployment-syncs to reduce reconcile latency during rollouts.

Monitor Work Queue Depth

Persistent work queue depth means the controller is overwhelmed. Tune workers, reduce churn, or check for reconcile errors causing retries.

HA Control Plane

Run 3 controller-manager replicas with leader election. Failover takes ~15s when a leader dies — plan accordingly for critical operations.

Watch Cache Warm-Up

On startup, controllers wait for the informer cache to sync before processing. Large clusters with many objects have a longer warm-up — don't restart unnecessarily.

📝 Knowledge Check

Q1. You delete a Deployment. How does Kubernetes ensure its ReplicaSets and Pods are also deleted?
  • A) The API server deletes all child objects synchronously during the DELETE request
  • B) The Deployment controller manually deletes each Pod one by one
  • C) The GarbageCollector controller detects orphaned owner references and deletes dependents
  • D) kubelet detects the missing Deployment and kills the pods on each node
C) GarbageCollector controller. When the Deployment is deleted, the GarbageCollector watches for objects whose ownerReferences point to a non-existent owner. It then issues delete requests for those dependents (ReplicaSets → Pods). This is cascade deletion via owner references.
Q2. An object is modified 50 times in rapid succession. How many times does the controller's reconcile function run?
  • A) 50 times — once per event
  • B) At least once, possibly fewer — the work queue deduplicates enqueued keys
  • C) Exactly twice — first and last event
  • D) Zero — the controller batches and processes every 30 seconds
B) At least once — the queue deduplicates. The work queue stores only the key (namespace/name). If the same key is enqueued multiple times before a worker picks it up, it is only processed once — reading the latest state from the informer cache. This is a core property of level-triggered (not edge-triggered) reconciliation.
Q3. A node loses network connectivity for 8 minutes. What does the NodeLifecycle controller do to pods on that node?
  • A) Nothing — pods remain on the node indefinitely until it reconnects
  • B) Pods are evicted immediately when the node goes unreachable
  • C) After ~40s the node is marked NotReady; after ~5min the controller evicts pods via taint-based eviction
  • D) The scheduler reschedules pods to other nodes without evicting the originals
C) ~40s NotReady then ~5min eviction. The NodeLifecycle controller marks the node NotReady after --node-monitor-grace-period (40s default), adding a not-ready taint. Pods tolerate this taint for tolerationSeconds (300s by default). After that, the taint-based eviction controller deletes the pods so they can be rescheduled elsewhere.