Gendi Notes

Kubernetes Architecture

Nothing in Kubernetes calls anything. Every component watches the API server for a gap between what you asked for and what exists, then acts to close it — and that single sentence explains the whole system.

Level 3 architecture kubernetesarchitecturecontrol-plane

What is it?#

A cluster is two halves: a control plane that decides, and worker nodes that run containers. They never talk to each other directly. Everything goes through one door — the API server — and everything else is a loop watching that door.

┌──────────────────── CONTROL PLANE ─────────────────────────────────┐ │ │ │ ┌────────┐ ┌──────────────┐ ┌───────────┐ │ │ │ etcd │ ←──→ │ API SERVER │ ←──→ │ Scheduler │ │ │ └────────┘ └──────────────┘ └───────────┘ │ │ the only the ONLY door picks a node │ │ database authn · authz │ │ admission · validate ┌──────────────────┐ │ │ ↑ ←──→ │ Controller mgr │ │ │ │ └──────────────────┘ │ │ │ keeps desired = actual │ └──────────────────────────┼─────────────────────────────────────────┘ │ every arrow above and below is │ a WATCH on the API server ┌──────────────────┼──────────────────┐ ↓ ↓ ↓ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ NODE 1 │ │ NODE 2 │ │ NODE 3 │ │ kubelet │ │ kubelet │ │ kubelet │ │ kube-proxy │ │ kube-proxy │ │ kube-proxy │ │ containerd │ │ containerd │ │ containerd │ │ [ pods… ] │ │ [ pods… ] │ │ [ pods… ] │ └──────────────┘ └──────────────┘ └──────────────┘

Why it is built this way#

The alternative — components calling each other — was tried by earlier orchestrators and it does not survive partial failure.

If components called each otherWith a shared watched store
A sends to B; B is down → the instruction is lostThe desired state is written down; B acts when it returns
Every component needs every other's addressEverything needs one address
N components → N² connections to secureN connections, one auth model
State lives in messages, which vanishState lives in etcd, which persists
Recovery means replaying messagesRecovery means re-reading current state

What it is made of#

Control plane#

ComponentJobIf it stops
kube-apiserverThe only way in. Authn, authz, admission, validation, then writes to etcdNo changes possible. Running pods keep running
etcdThe only database. Every object, and the watch streamThe cluster loses its memory. Back this up
kube-schedulerAssigns a node to pods that have noneNew pods stay Pending. Existing ones untouched
kube-controller-manager~30 controllers in one binary, reconciling objectsDeploys stall silently — no error, just nothing
cloud-controller-managerCloud-specific: load balancers, node lifecycle, routesLoadBalancer Services stay <pending>

Worker node#

ComponentJobIf it stops
kubeletWatches for pods bound to its node, then makes them realNode goes NotReady; pods evicted after a timeout
kube-proxyPrograms the node's forwarding rules from EndpointSlicesExisting connections survive; new Service routing breaks
Container runtimeActually starts containers (containerd, CRI-O)Nothing runs on that node

The three plug-in interfaces#

Kubernetes deliberately implements none of these itself:

kubelet │ ├── CRI ──→ containerd / CRI-O "run this container" │ ├── CNI ──→ Calico / Cilium / flannel "give this pod an IP" │ └── CSI ──→ EBS / Ceph / NFS driver "attach this volume"

How it works: kubectl create deploy end to end#

This is the question. Follow one command all the way down.

kubectl create deployment web --image=nginx:1.27 --replicas=3

Step 1 — kubectl builds an HTTP request#

kubectl is a plain HTTP client. It reads ~/.kube/config for the API server address and your credentials, turns the flags into a Deployment object, and POSTs it.

# Prove it — this prints the request and response without the abstraction
kubectl create deployment web --image=nginx:1.27 --replicas=3 -v=8
POST https://10.0.0.1:6443/apis/apps/v1/namespaces/default/deployments
Authorization: Bearer <token>
Content-Type: application/json
{"kind":"Deployment","spec":{"replicas":3,...}}

Step 2 — the API server runs four gates, in order#

This order matters and is asked about constantly.

request │ ↓ ① AUTHENTICATION who are you? │ certs · bearer tokens · OIDC · webhook │ fails → 401 ↓ ② AUTHORIZATION may you do this? │ RBAC: can <user> <verb> <resource> in <namespace>? │ fails → 403 ↓ ③ ADMISSION should this be changed, or refused? │ MUTATING first — defaults, sidecar injection │ then VALIDATING — policy, quotas │ fails → 400 with the webhook's message ↓ ④ VALIDATION is the object well-formed? │ schema, required fields, immutable fields │ fails → 400 ↓ WRITE TO etcd ← and only now does the object exist │ ↓ 201 Created → kubectl prints "deployment.apps/web created"

Step 3 — controllers notice, in a chain#

Nobody told the controllers anything. They hold an open watch on the API server and receive the new object on that stream.

etcd now has: Deployment web (replicas: 3) │ │ watch event ↓ ┌────────────────────┐ │ Deployment │ "a Deployment with no ReplicaSet → │ controller │ create one, with a pod-template hash" └────────────────────┘ │ creates ↓ ReplicaSet web-7d4f9c (replicas: 3, owns pods by label) │ │ watch event ↓ ┌────────────────────┐ │ ReplicaSet │ "I want 3, I can see 0 → │ controller │ create 3 Pod objects" └────────────────────┘ │ creates ↓ Pod web-7d4f9c-a1b2 spec.nodeName: "" ← still just rows in etcd Pod web-7d4f9c-c3d4 spec.nodeName: "" Pod web-7d4f9c-e5f6 spec.nodeName: ""

Watch the chain yourself:

kubectl get deploy,rs,pods -l app=web
kubectl get events --sort-by=.lastTimestamp | tail -20

Step 4 — the scheduler binds each pod to a node#

The scheduler watches for pods where spec.nodeName is empty. For each one it runs two phases:

ALL NODES (say 20) │ │ ① FILTER — hard constraints. A node either passes or it does not. │ · enough allocatable CPU/memory for the REQUESTS? │ · tolerates the node's taints? │ · nodeSelector / affinity satisfied? │ · required ports free? volume attachable in this zone? ↓ FEASIBLE NODES (say 6) ← none left? pod stays Pending, Events say why │ │ ② SCORE — soft preferences, 0-100 each, weighted and summed │ · least requested / most requested │ · spread across zones and existing replicas │ · image already present on the node │ · inter-pod affinity satisfied ↓ BEST NODE → BIND: write spec.nodeName = "node-2" back to the API server

The scheduler's only output is a write. It never contacts the node.

# Why did this pod land where it did — or not land at all?
kubectl describe pod <pod> | tail -20        # Events name the failing predicate
kubectl get pod <pod> -o jsonpath='{.spec.nodeName}{"\n"}'

Step 5 — the kubelet makes it real#

Each kubelet watches for pods where spec.nodeName equals its own node name. When one appears, it does the actual work:

kubelet on node-2 sees a pod bound to itself │ ├─→ CNI plugin set up the network namespace, allocate a pod IP │ ├─→ CRI: pull image containerd → registry (honours imagePullPolicy │ and imagePullSecrets) │ ├─→ CSI plugin attach and mount any volumes │ ├─→ CRI: run start the pause container, then init containers │ to completion, then app containers │ ├─→ probes startup → readiness → liveness, on their schedules │ └─→ PATCH status report back to the API server: phase: Running, podIP, containerStatuses, Ready

That status write is what makes the pod appear as Running in kubectl get pods. The status you read is always the kubelet's report, not a live query.

Step 6 — the pod becomes reachable#

Two more controllers react to the pod turning Ready:

Pod becomes Ready │ ├─→ EndpointSlice controller │ adds the pod IP to the Service's EndpointSlice │ │ │ │ watch event │ ↓ │ kube-proxy ON EVERY NODE │ rewrites iptables / IPVS / eBPF rules so the │ ClusterIP now DNATs to this pod too │ └─→ CoreDNS already resolves the Service name; it did not change

Only now does traffic reach your container — and the gap between "pod Ready" and "every node has reprogrammed" is exactly the race that causes deploy-time 502s.

The whole path, in one picture#

you ──→ kubectl ──→ API server ──→ etcd ↑ │ │ │ watch │ ↓ │ Deployment ctrl ──→ ReplicaSet ctrl ──→ Pods │ │ │ ↓ │ Scheduler picks a node, writes nodeName │ │ │ ↓ └───── kubelet on that node │ ├─ CNI: IP ├─ CRI: pull + run ├─ CSI: volumes └─ report status back up EVERY arrow into the API server is a write. EVERY arrow out of it is a watch. No component ever calls another component.

How components actually communicate#

Watch, not poll#

A naive controller would poll: "list all pods, every second". With 5,000 pods that is unusable. Instead:

controller startup │ ├─ ① LIST one full read — "give me all Deployments now" │ builds an in-memory cache, with a resourceVersion │ └─ ② WATCH a long-lived HTTP stream from that resourceVersion the API server pushes only CHANGES down it │ ↓ ADDED / MODIFIED / DELETED events │ ↓ update the local cache, enqueue the object key │ ↓ worker pops the key, reads the CURRENT state from cache, and reconciles

This pattern is called an informer, and it is why client-go controllers scale. Two details worth knowing:

  • The queue holds keys, not events. Five rapid changes to one Deployment collapse into one key, reconciled once against final state. This is level-triggered behaviour falling out of the design.
  • If the watch breaks or falls too far behind, the client gets 410 Gone — resourceVersion too old and does a full re-LIST. Correctness is preserved through any disconnection.

Leader election#

kube-controller-manager and kube-scheduler run multiple replicas for availability, but only one may act — two schedulers binding the same pod to different nodes would be a disaster.

# The lease shows who currently holds it
kubectl get lease -n kube-system kube-scheduler -o yaml

They contend for a Lease object in kube-system. The holder renews it every few seconds; if it dies, the lease expires and another replica takes over. It is active–passive, not load-balanced.

What goes wrong#

SymptomWhich componentHow to confirm
kubectl hangs or times outAPI server or network to itkubectl cluster-info, kubectl get --raw /healthz
Everything Pending, no EventsScheduler down or not leadingkubectl get pods -n kube-system, check its lease
apply succeeds, nothing happenscontroller-manager downSame; the absence of Events is the tell
Pods stuck ContainerCreatingCNI or CSI, not Kuberneteskubectl describe pod → Events name the plugin
Node NotReadykubelet stopped, or lost the API serversystemctl status kubelet, journalctl -u kubelet
Service routing broken on one nodekube-proxy on that node`kubectl get pods -n kube-system -o wide \grep proxy`
etcdserver: request timed outetcd slow — almost always disk latencyetcd needs low-latency SSD; check fsync metrics
Random 500s from the API serverAn admission webhook is failingkubectl get validatingwebhookconfigurations

Interview corner · الأسئلة اللي بتتسأل#

Walk me through exactly what happens when you run kubectl apply -f deploy.yaml.

Answer as a chain of watches, not a list of components — that is the distinction being tested.

  1. kubectl reads ~/.kube/config and POSTs the object over HTTPS. It is an ordinary HTTP client; nothing about it is privileged.
  2. API server runs four gates in order: authentication → authorization (RBAC) → admission (mutating, then validating) → schema validation, then writes to etcd. It returns 201 Created. Nothing is running yet.
  3. Deployment controller, watching, sees a Deployment with no matching ReplicaSet and creates one.
  4. ReplicaSet controller, watching, sees 0 of 3 pods and creates three Pod objects — still rows in etcd, with spec.nodeName empty.
  5. Scheduler, watching for unbound pods, filters feasible nodes, scores them, and binds by writing nodeName. It never contacts the node.
  6. kubelet on that node, watching for pods bound to itself, calls CNI for an IP, CRI to pull and run, CSI for volumes, then PATCHes status back.
  7. EndpointSlice controller adds the now-Ready pod, and kube-proxy on every node reprograms its forwarding rules.
The control plane is completely down. Is the application down? · الـ control plane وقع

No. The data path does not go through the control plane.

Still worksWhy
Pods keep runningThe kubelet supervises them locally
A crashed container restartsThe kubelet does that without asking anyone
Service traffic flowsForwarding rules are already programmed on each node
DNS resolvesCoreDNS pods are just pods, already running
StopsWhy
Deploys, scaling, kubectlEvery write goes through the API server
Rescheduling after a node diesThat is a controller's job
New Service endpointsThe EndpointSlice controller is not running
Certificate rotation, secret updatesAlso control-plane work
Why does Kubernetes use a watch-based model instead of components calling each other?

Because a call-based system loses information on partial failure, and partial failure is the normal state of a distributed system.

The property that matters is level-triggered versus edge-triggered:

Edge-triggered (events)Level-triggered (state)
Reacts to"a pod was deleted""there are 2, I want 3"
Miss a messagePermanently wrongSelf-corrects on next read
Component restartsMust replay historyRe-reads current state
Duplicate deliveryDouble-actsIdempotent by construction

Consequences you should name:

  • Any controller can be killed and restarted with no coordination — it re-LISTs and continues.
  • Reconciliation is idempotent, so retries are free and require no deduplication logic.
  • Failure looks like silence, not errors. A dead controller produces nothing happening, which is why kubectl describe showing no Events is itself a strong diagnostic signal.
An admission webhook is down. What breaks, and how do you recover?

Depends entirely on failurePolicy:

  • Ignore — the write proceeds without the webhook. Degraded policy enforcement, no outage.
  • Fail — every matching write is rejected cluster-wide.

With Fail, the danger is a deadlock: if the webhook matches pods in kube-system, you cannot create the pods that would restore the webhook.

Recovery, in order:

# 1. See which webhooks exist and what they match
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# 2. Is its backing service actually up?
kubectl get pods -n <webhook-namespace>

# 3. If you are locked out, delete the configuration. The webhook stops
#    being consulted immediately; reinstall it once the service is healthy.
kubectl delete validatingwebhookconfiguration <name>

Prevention: narrow namespaceSelector/objectSelector, always exclude kube-system, short timeoutSeconds (the default 10s is a long time to add to every write), and reserve failurePolicy: Fail for genuinely security-critical policy.

Key takeaways#

  • Nothing calls anything. Every component watches the API server and reconciles a difference. That one sentence is the architecture.
  • The API server is the only door, and it runs authn → authz → admission → validation → write, in that order.
  • created means "recorded in etcd", not "running". Always wait for rollout status.
  • The scheduler only writes nodeName. It never contacts a node; the kubelet notices the binding itself.
  • Level-triggered, not edge-triggered — which is why any controller can be killed and restarted with nothing lost.
  • The control plane is the change path, not the request path. Running pods survive its loss; your ability to fix anything does not.
  • ContainerCreating usually means CNI or CSI, not Kubernetes itself.
  • A failurePolicy: Fail webhook can deadlock the cluster. Exclude kube-system.

Last updated 2026-09-13