Kubernetes Architecture
Every Kubernetes cluster is really two very different populations of machine working together: a small control plane that decides what should be running, and a larger set of nodes that actually run it. What Is Kubernetes, and Why covered why that split exists at all; this page opens the box and names every part inside it. On the control-plane side that's kube-apiserver (the single front door), etcd (the one place state is durably stored), kube-scheduler (which node a new Pod lands on), kube-controller-manager (the control loops that keep reality honest), and cloud-controller-manager (the seam to whatever cloud the cluster runs on). On every node that's kubelet, kube-proxy, and a container runtime reached through the CRI. None of these eight names are optional vocabulary — CKA's very first exam domain is built entirely on knowing what each one does and doesn't do.
Picture a small film studio. The control plane is the production office up front: a front-desk clerk (kube-apiserver) who is the only person allowed into the archive room to read or update the master script; a director (kube-scheduler) who decides which soundstage shoots which scene; a floor manager (kube-controller-manager) constantly checking that every scene that's supposed to be filming today actually is; and, if the studio rents its lot from a bigger cloud studio, a liaison (cloud-controller-manager) who deals with that landlord. Out on the actual soundstages (the nodes), a stage manager (kubelet) reads today's shooting schedule and gets the right cameras and lights (containers) switched on, while a mail router (kube-proxy) makes sure anything addressed to "Stage 3" finds Stage 3 no matter which room the crew moved it to overnight. The clerk never leaves the front desk, and nobody but the clerk ever walks into the archive.
The shape of a cluster: control plane and nodes
☺ Like you're 10: A cluster is a school with one office that plans the day and a bunch of classrooms that actually run it — lose the office for an hour and every class already in session keeps right on going.
A control-plane machine and a worker node run the same underlying software (a kubelet, a container runtime) but do fundamentally different jobs. The control plane holds the cluster's decisions — what should exist, where it should run — while nodes hold the cluster's work, the actual containers serving traffic. A lab cluster (kind, minikube) can squeeze both roles onto one machine; a real production cluster keeps them apart, and by default a control-plane node carries a taint (node-role.kubernetes.io/control-plane:NoSchedule) specifically so ordinary workloads don't land on it and compete with the components that run the whole cluster.
Whether you ever see the control plane at all depends on who manages it. Self-managed clusters built with kubeadm run every control-plane component as an ordinary-looking Pod in the kube-system namespace, visible to kubectl get pods like anything else. Managed offerings — EKS, GKE, AKS — run the control plane in infrastructure you don't have SSH access to and never see with kubectl at all; you get an API endpoint and a bill, and the cloud vendor owns everything behind it. Both are "Kubernetes architecture" in the sense this page describes — only who operates the five boxes on the left of the diagram below changes.
kube-apiserver: the one front door
☺ Like you're 10: The apiserver is the front-desk clerk everyone has to go through — nobody else is allowed to walk into the archive room and change the master script themselves.
kube-apiserver is a stateless HTTP server that implements the Kubernetes API. Every read and every write in the cluster — a person running kubectl, the scheduler, a controller, a kubelet, another cluster's federation tooling — goes through it, and it is the only component with a client connection to etcd. Nothing else, not even kube-scheduler, is allowed to read or write cluster state directly; that single choke point is what makes the rest of the architecture enforceable.
A request passing through kube-apiserver goes through the same pipeline every time: authentication (who are you — client certificate, bearer token, OIDC), authorization (are you allowed to do this — almost always RBAC on a modern cluster, see RBAC & admission control), admission control (built-in and webhook plugins that can mutate or outright reject an otherwise-valid object — NodeRestriction, ResourceQuota, a policy engine like Kyverno or OPA Gatekeeper), and finally schema validation against the object's OpenAPI definition. Only an object that survives all four stages ever reaches etcd. Because kube-apiserver keeps no state of its own between requests, it's trivial to scale horizontally — production clusters run several replicas behind a load balancer purely for availability, with no coordination needed between them.
kubectl exec, kubectl logs, and kubectl port-forward feel like they talk straight to a node, but they don't — kubectl never opens a connection to anything but kube-apiserver. For those three commands specifically, apiserver turns around and opens its own connection to the target kubelet's HTTPS API (port 10250) and proxies the response back. If that proxied connection is what's failing, "kubectl exec hangs" can mean a network-policy or firewall problem between the control plane and that one node, not a problem with the Pod at all.
etcd: the cluster's one source of truth
☺ Like you're 10: etcd is the filing cabinet with every fact about the cluster in it, and the clerk (apiserver) is the only one with a key.
etcd is a distributed, strongly-consistent key-value store, and it holds every object in the cluster — every Pod, Deployment, Secret, and Node — as a key under a path like /registry/pods/default/checkout-api-7d4f9. It reaches consistency across its members using the Raft algorithm: a write is only acknowledged once a majority of etcd members have durably recorded it, which is exactly why etcd runs with an odd number of members — three tolerates one member's loss, five tolerates two, and an even number buys you nothing extra while adding a member that can vote in ties. Because every write requires a majority round-trip and a disk fsync, etcd is unusually sensitive to slow storage; production guidance is a dedicated fast SSD, low network latency between members, and ideally etcd on its own machines separate from the rest of the control plane once a cluster gets large.
etcd is also the cluster's single point of total failure in a way nothing else is: lose a majority of etcd members permanently and you lose the cluster's entire memory of what should exist, even though the Pods that were already running keep running for a while. That's why etcdctl snapshot save — and practicing the matching snapshot restore — is genuinely operational, not academic, and shows up directly in CKA's Cluster Architecture, Installation & Configuration domain.
# On a kubeadm cluster, control-plane components are ordinary Pods in kube-system
kubectl get pods -n kube-system -o wide
# The kubelet on a control-plane node starts them straight from static manifests
# on disk — no scheduler involved, because at bootstrap there's nothing to
# schedule onto yet.
ls /etc/kubernetes/manifests/
# etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
# etcd's own health, queried through its client port on that node
kubectl -n kube-system exec etcd-control-plane-1 -- \
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health
# A real backup — the single most important control-plane habit to automate
etcdctl snapshot save /backup/etcd-$(date +%F).dbOn a self-managed cluster, control-plane components are themselves just Pods — but a special kind. The kubelet on a control-plane node reads YAML files straight out of /etc/kubernetes/manifests/ and starts them as static Pods, without ever asking kube-scheduler for a decision. That has to be true, because at the moment etcd and kube-apiserver first start up, there is no scheduler running yet to ask. Managed Kubernetes (EKS, GKE, AKS) hides this entirely — the cloud vendor's own infrastructure runs the equivalent of this bootstrap for you, which is exactly why you can't kubectl get pods -n kube-system and find a kube-apiserver Pod sitting there on those platforms.
kube-scheduler: deciding where a Pod runs
☺ Like you're 10: The scheduler is the director picking which soundstage shoots a scene — it never picks up a camera itself, it just writes the stage number on the call sheet.
kube-scheduler watches kube-apiserver for Pods whose .spec.nodeName is still empty — "unscheduled" Pods — and runs each one through two phases: filtering, which throws out every node that can't possibly work (not enough free CPU or memory for the Pod's requests, a taint the Pod doesn't tolerate, a node selector or required affinity rule that doesn't match, a hostPort already in use), and scoring, which ranks the nodes that survive filtering by things like how evenly resources would end up spread and how far the Pod sits from others it should stay near or away from. The scheduler then does exactly one thing with the winner: it writes a Binding back through kube-apiserver, setting nodeName. It never contacts a node directly, never starts a container, and has no idea whether the Pod it just placed will actually come up healthy — that's someone else's job, covered in full in Scheduling & Resource Management.
kube-controller-manager: the control loops
☺ Like you're 10: The floor manager doesn't do the filming — it just keeps walking the lot, comparing "what should be shooting" against "what's actually shooting," and fixing any gap it finds.
kube-controller-manager is a single binary that bundles dozens of independent control loops, each one watching kube-apiserver for one kind of object, comparing its recorded desired state against the observed actual state, and issuing further API calls to close any gap — the same reconciliation pattern the object model and the API & controller pattern cover in depth. A Deployment controller turns a Deployment into the right number of ReplicaSets; a ReplicaSet controller turns each of those into the right number of Pods; a Job controller tracks completions; an endpoint controller keeps Service membership current. The Node controller deserves a special mention: it watches the Lease object each kubelet renews as a heartbeat, marks a Node NotReady once heartbeats stop arriving past a grace period, and — after a further wait — starts evicting that node's Pods so they get rescheduled elsewhere, the mechanism underneath most "a node went dark and my Pods moved" incidents covered in the troubleshooting methodology page.
cloud-controller-manager: the seam to the cloud
☺ Like you're 10: This is the liaison who deals with the landlord — if your studio doesn't rent a lot from anybody, you don't need one.
Cloud-specific logic used to live inside kube-controller-manager itself, which meant every cloud vendor's code shipped inside core Kubernetes. cloud-controller-manager was split out specifically to end that: it's a separate binary that bundles only the controllers that need to call out to a cloud provider's own API — a node controller that checks whether a Node's backing VM still exists and removes the Node object if it doesn't, a route controller that programs cloud-network routing for Pod CIDRs where the cloud needs that, and a service controller that provisions an actual cloud load balancer the moment someone creates a Service of type LoadBalancer.
Unlike the other four components, cloud-controller-manager is genuinely optional. A bare-metal kubeadm cluster runs correctly with none at all — you simply lose the automatic cloud load balancer, which is exactly the gap tools like Cilium or MetalLB step into for on-prem clusters. On EKS, GKE, and AKS it runs as an invisible part of the managed control plane, quietly turning your Service objects into real load balancers in the background.
Node components: kubelet, kube-proxy, and the container runtime
☺ Like you're 10: The stage manager reads the call sheet and gets the cameras rolling; the mail router makes sure "Stage 3" always finds Stage 3; the crew with the actual equipment is who presses record.
Every node — control-plane or worker — runs the same three pieces, and none of them are Pods themselves; they're processes the operating system starts directly (typically as systemd units), because something has to exist before there's anything to run Pods with in the first place.
- kubelet registers its Node with kube-apiserver, then watches for any Pod whose
nodeNamenow matches its own — the Binding kube-scheduler wrote. For each one it drives the container runtime to pull images and start containers, continuously runs the Pod's liveness, readiness, and startup probes, and reports both Node and Pod status back to kube-apiserver. It also terminates the proxiedexec/logs/port-forwardconnections mentioned above. What kubelet is not is a controller in the etcd-reconciling sense — it only ever concerns itself with Pods bound to its own node, never the whole cluster. - kube-proxy runs on every node and watches kube-apiserver for
ServiceandEndpointSliceobjects, then programs that node's packet-forwarding rules — historically iptables, increasingly IPVS or eBPF, and on some clusters replaced entirely by the CNI in "kube-proxy-replacement" mode — so traffic sent to a Service's stable ClusterIP gets load-balanced to whichever backing Pods are currently healthy, wherever they've most recently been rescheduled. The full mechanics of that, plus the CNI plugin that gives every Pod its IP in the first place, live in Networking & the CNI. - Container runtime is the thing that actually creates the Linux namespaces and cgroups and starts a container process —
containerdor CRI-O today (Kubernetes dropped built-in support for talking to plain Docker, "dockershim," in version 1.24). It speaks the Container Runtime Interface (CRI), a gRPC API, to kubelet, and typically shells out to a lower-level OCI runtime likeruncto do the actual isolation work — the same namespaces-and-cgroups mechanism covered from the container side in DevOps' containers & orchestration page.
"The first time a Pod sat at Pending for three full minutes I was convinced kube-scheduler was broken. It wasn't — Pending just meant nothing had picked the Pod up yet. The second it went to ContainerCreating I relaxed too early, because that's kubelet and the runtime pulling an image, not a stalled scheduler at all. Three different words, three completely different components you'd need to go check, and 'my Pod is stuck' turned out to mean something different every single time I said it."
Putting it together: a Pod's journey through the architecture
☺ Like you're 10: One request touches almost every box on this page, in order, and the last box calls all the way back to the first one to say "done."
Run kubectl apply -f pod.yaml and here's everywhere that request actually goes before a container is running: kube-apiserver authenticates, authorizes, and admission-controls the request, then commits the new Pod object to etcd with no nodeName set. kube-scheduler, watching for exactly that, filters and scores the nodes and writes a Binding back through kube-apiserver, which persists the chosen nodeName to etcd. The kubelet on that specific node — watching kube-apiserver for Pods bound to itself — notices the new assignment and calls the container runtime over CRI to pull the image and start the container. Once it's up, kubelet reports the Pod's status back to kube-apiserver, which writes that final state to etcd too. Nine hops, one Pod, and etcd is the only thing any of them ever touched directly — always through apiserver.
Spin up a local cluster with kind or minikube, run kubectl get pods -n kube-system -o wide to see every control-plane component, then deliberately break one: kubectl cordon won't do it, so instead scale the scheduler's Deployment (on a kind cluster it runs as a static Pod, so move its manifest out of /etc/kubernetes/manifests/ temporarily) to zero. Create a brand-new Pod — it sits at Pending forever. Now check every Pod that was already running before you did that. They're untouched. That's the whole architecture in one experiment: the control plane decides what should run, but the data plane doesn't ask permission to keep something already running alive.
Notice what's structurally absent from the journey above: nowhere does a running container ask the control plane for permission to keep existing. If kube-apiserver disappears for ten minutes, every Pod that's already Running keeps serving traffic — kubelet just can't hear about new instructions, and can't report status, until apiserver comes back. That's precisely why control-plane high availability protects your ability to change the cluster, not the cluster's ability to keep serving traffic in the meantime — a distinction worth having exactly right before Troubleshooting, where "is this a control-plane outage or a workload outage" is usually the first fork in the road.
This page is the map, not the territory underneath it. Control Plane Internals walks the same five components at implementation depth — watch mechanics, leader election, the reconciler pattern in code — and if you want to see this whole architecture treated as the foundation you build an internal developer platform on top of, Platform Engineering's Kubernetes as the Platform Substrate goes further still. If Pod, Deployment, and Service are still unfamiliar words rather than architecture, The Object Model is the very next page, and covers them from Kubernetes' own declarative-API side.
Gizmo the Gremlin: Deployment's stuck mid-rollout and I'm impatient. Let's just SSH into the control-plane node and edit the object straight into etcd with etcdctl put — way faster than fighting kubectl.
Timmy the Turtle: Absolutely not. etcd doesn't know what a Deployment is — it just stores bytes under a key. Write straight to that key and you can corrupt the object badly enough that apiserver refuses to even read it back afterward.
Professor Owl: Every write has to go through kube-apiserver — that's the entire point of this architecture. It validates the object's schema, runs admission control, and only then commits to etcd. Skip that step and you've thrown away every guarantee the API ever gave you.
Benny the Beaver: And even if the write somehow "worked," kube-controller-manager wouldn't notice a thing happened until its next watch event fires — you'd have an object in etcd that doesn't match what's actually running. That's worse than stuck, that's silently wrong.
Foxy: Fine, fine. So what do we actually do when a rollout's stuck?
Professor Owl: kubectl rollout status, then describe on the Deployment and its ReplicaSet, then kubectl get events sorted by timestamp. The control plane already tells you exactly where the reconciliation loop is stuck, if you ask it the way it expects to be asked.
Timmy the Turtle: Which is also the only path that's still authenticated, authorized, and admission-controlled. The slow way is the only way that keeps working when three other people are depending on this cluster too.
1. Which control-plane component is the only one that ever reads or writes etcd directly, and why does that matter? 2. What's the difference between what kube-scheduler decides and what actually starts a container? 3. Name the three components every node runs and, in one sentence each, what job belongs to each one. 4. What was cloud-controller-manager split out of, and why is it the one control-plane component a bare-metal cluster can run without? 5. If kube-apiserver goes down for ten minutes, what happens to Pods that were already Running before it went down — and why?
Check your answers
- kube-apiserver. Every other component — kube-scheduler, kube-controller-manager, cloud-controller-manager, kubelet, kube-proxy, and kubectl itself — only ever talks to kube-apiserver, which is what makes authentication, authorization, admission control, and schema validation apply to literally every change made to the cluster's state.
- kube-scheduler only decides which node a Pod should run on and writes that decision back as a Binding through kube-apiserver — it never contacts a node or starts anything. The kubelet on the chosen node, working through the container runtime over the CRI, is what actually pulls the image and starts the container process.
- kubelet registers the node and drives the container runtime to run whatever Pods are bound to it, reporting status back to kube-apiserver; kube-proxy watches Services and EndpointSlices and programs that node's packet-forwarding rules so Service traffic reaches the right, currently-healthy Pods; the container runtime (containerd or CRI-O) is what actually creates the namespaces, cgroups, and container process itself.
- cloud-controller-manager was split out of kube-controller-manager to keep cloud-vendor-specific code out of core Kubernetes. It's optional because its controllers exist only to call a cloud provider's own API (provisioning load balancers, checking whether a backing VM still exists) — a bare-metal cluster has no such API to call, so it simply runs without one, typically substituting a tool like MetalLB for the load-balancer piece.
- They keep running, and keep serving traffic. kubelet just can't receive new instructions or report status back until kube-apiserver returns, because a running container never has to ask the control plane for permission to keep existing — the control plane governs what should run, not the data plane's ability to keep something already running alive.