Chapter 44 of 47 · Under the hood

Kubernetes cluster architecture

You know the objects now: Pod, ReplicaSet, Deployment and Service. This chapter shows the machinery that makes them work. A cluster is a group of machines that work together to run your applications. Inside it there are two kinds of node: the control plane, which decides, and worker nodes, which run your containers. Everything you write is an API object that describes the state you want.

Kubernetes itselfwhat you work withwhat it is made oflit up in the walk-through

The map

Click a box to see what it does. Click it again to fold or unfold what is inside. With the keyboard, use the arrow keys to move and Enter to fold.

Text version of the map
  • Kubernetes — keeps your containers running the way you declared. You write down the state you want; Kubernetes keeps comparing it with what is actually running and fixes the difference, forever. kubectl version
    • Cluster — the set of machines (nodes) that Kubernetes manages as one computer. You never pick a machine for your app; you hand the app to the cluster. kubectl cluster-info
      • Control Plane — the brain. It stores the desired state, decides where things run and notices when reality drifts. In production it runs on its own machines and does not run your apps. kubectl get pods -n kube-system
        • API Server — the front door. Every kubectl command and every other component talks only to it. It checks who you are, validates the object and saves it. Nothing else talks to etcd. kubectl api-resources
        • Scheduler — finds Pods with no node yet and picks one for each, based on free CPU and memory, node labels, affinity and taints. It only decides; the kubelet does the starting. kubectl get events --field-selector reason=Scheduled
        • Controller Manager — runs the control loops: observe, compare with the desired state, act. The Deployment, ReplicaSet, Node and Job controllers all live here. This is what makes Kubernetes self-healing. kubectl get pods -n kube-system -l component=kube-controller-manager
        • Cloud-Controller-Manager — the loops that talk to your cloud provider: create a load balancer for a Service, notice a deleted VM, set up routes. Local clusters such as kind and minikube do not have one.
        • etcd — a consistent key-value store and the cluster's only source of truth. Every object you create ends up here. Lose etcd without a backup and you lose the cluster's state. kubectl get --raw /livez/etcd
      • Worker Node — a machine (VM or bare metal) that runs your Pods. Add more worker nodes and the cluster gets more room. kubectl get nodes -o wide
        • kubelet — the agent on every node. It watches the API server for Pods assigned to its node, asks the container runtime to start them, runs their health probes and reports status back. kubectl describe node <node-name>
        • Kube-Proxy — writes the network rules (iptables, IPVS or nftables) on each node so that traffic sent to a Service IP lands on one of its Pods. kubectl get pods -n kube-system -l k8s-app=kube-proxy
        • Container Runtime — pulls images and runs containers, usually containerd or CRI-O, through the Container Runtime Interface. Since Kubernetes 1.24 the kubelet does not talk to Docker Engine directly, but images you build with Docker run fine. kubectl get nodes -o wide # CONTAINER-RUNTIME column
    • Pod — the smallest thing Kubernetes runs: one or more containers that share one IP address and can share volumes. Pods are disposable; when one dies it is replaced, not repaired. kubectl get pods -o wide
      • Container — your image, running. The same container you know from Docker. Containers in one Pod reach each other on localhost. kubectl logs <pod> -c <container>
    • Service — a stable name and virtual IP in front of a changing set of Pods, chosen by label. Pods come and go with new IPs; the Service address stays the same. kubectl get svc
      • Service Types — how far the Service is reachable. Each type builds on the one before it.
        • ClusterIP — the default. A virtual IP reachable only from inside the cluster, plus a DNS name like web.default.svc.cluster.local. kubectl expose deployment web --port=80
        • NodePort — also opens the same port (30000–32767 by default) on every node, so <any-node-ip>:<port> reaches the Service. kubectl expose deployment web --type=NodePort --port=80
        • LoadBalancer — also asks the cloud for an external load balancer with a public address. The cloud-controller-manager creates it. kubectl expose deployment web --type=LoadBalancer --port=80
        • ExternalName — no Pods and no proxying: the Service name becomes a DNS alias (CNAME) for a name outside the cluster, such as a managed database. kubectl create service externalname db --external-name=db.example.com
    • Deployment — what you usually write for a stateless app: a Pod template plus how many copies you want. Changing the template triggers a rolling update. kubectl rollout status deployment/web
    • ReplicaSet — keeps exactly N identical Pods running. A Deployment creates a new ReplicaSet for each version, which is how rollbacks work. You rarely write one yourself. kubectl get rs
    • ConfigMap — plain configuration (URLs, flags, whole config files) kept outside the image and handed to Pods as environment variables or files. kubectl create configmap app-config --from-literal=LOG_LEVEL=info
    • Secret — like a ConfigMap, for passwords and keys. The values are only base64-encoded, not encrypted, unless encryption at rest is turned on, so limit who can read them. kubectl create secret generic db --from-literal=PASSWORD=change-me
    • Namespace — a folder for objects: names only have to be unique inside one, and quotas and access rules apply per namespace. It does not block network traffic by itself. kubectl get namespaces

Two kinds of node

Control planeWorker node
JobDecide and rememberRun containers
RunsAPI server, scheduler, controller manager, cloud controller manager, etcdkubelet, kube-proxy, container runtime, and your Pods
If it goes downRunning apps keep running, but nothing new is scheduled or healedIts Pods are recreated on other nodes
How manyUsually 3 for high availability (etcd needs a majority)As many as you need, from 1 to thousands
The one idea to keep. Components never call each other directly. They all watch the API server and react to changes in the objects stored there. That is why you can kill almost any piece and the cluster recovers.

Follow one kubectl apply

What happens between typing the command and a container running?

  1. You run kubectl apply -f web.yaml. kubectl sends the Deployment to the API server, which checks who you are and whether the object is valid.
  2. The API server saves the Deployment in etcd. At this point nothing is running. The cluster only knows what you want.
  3. Controllers react. The Deployment controller sees a Deployment without a ReplicaSet and creates one. The ReplicaSet controller sees 0 of 3 Pods and creates 3 Pod objects, still with no node.
  4. The scheduler places the Pods. It sees Pods with no node, scores the worker nodes, and writes the chosen node name into each Pod.
  5. The kubelet starts them. The kubelet on each chosen node sees a Pod assigned to it, tells the container runtime to pull the image and start the containers, then reports the Pod as Running.
  6. Traffic finds them. A Service whose selector matches the Pods' labels lists their IPs, and kube-proxy on every node updates its rules so the ClusterIP reaches them.

Every arrow in this story goes through the API server. The scheduler never calls the kubelet; it writes a field, and the kubelet notices.

The objects you write

A Deployment, a Service and a ConfigMap are enough to run most stateless apps:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3                 # the ReplicaSet keeps 3 Pods alive
  selector:
    matchLabels: { app: web }
  template:                   # the Pod template
    metadata:
      labels: { app: web }
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports: [{ containerPort: 80 }]
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP             # the default
  selector: { app: web }      # matches the Pod labels above
  ports: [{ port: 80, targetPort: 80 }]

The link between a Service and its Pods is only the label app: web. Get the label wrong and the Service has no endpoints, which is one of the most common "it does not work" bugs.

Try it yourself

You need Docker, kind and kubectl. Versions and names in the output will differ on your machine.

  1. Step 1 · Create a cluster

    kind runs each node as a Docker container. This cluster has a single node that is both control plane and worker.

    $ kind create cluster --name lab
    $ kubectl get nodes
    NAME                STATUS   ROLES           AGE   VERSION
    lab-control-plane   Ready    control-plane   40s   v1.31.0
  2. Step 2 · Find the control plane

    The components from the map are Pods in the kube-system namespace. There is no cloud-controller-manager on kind.

    $ kubectl get pods -n kube-system
    NAME                                        READY   STATUS
    coredns-7c65d6cfc9-6bz8q                    1/1     Running
    etcd-lab-control-plane                      1/1     Running
    kube-apiserver-lab-control-plane            1/1     Running
    kube-controller-manager-lab-control-plane   1/1     Running
    kube-proxy-4x2mv                            1/1     Running
    kube-scheduler-lab-control-plane            1/1     Running
  3. Step 3 · Deploy and look at the chain
    $ kubectl create deployment web --image=nginx:1.27 --replicas=3
    $ kubectl get deploy,rs,pods
    NAME                  READY   UP-TO-DATE   AVAILABLE
    deployment.apps/web   3/3     3            3
    
    NAME                             DESIRED   CURRENT   READY
    replicaset.apps/web-6d4cf56db6   3         3         3
    
    NAME                       READY   STATUS
    pod/web-6d4cf56db6-8kq2p   1/1     Running
    pod/web-6d4cf56db6-m7x4t   1/1     Running
    pod/web-6d4cf56db6-zt9wn   1/1     Running

    The Pod names carry the ReplicaSet's name, which carries the Deployment's name.

  4. Step 4 · Put a Service in front
    $ kubectl expose deployment web --port=80
    $ kubectl get endpointslices -l kubernetes.io/service-name=web
    NAME        ADDRESSTYPE   PORTS   ENDPOINTS
    web-x7k2d   IPv4          80      10.244.0.5,10.244.0.6,10.244.0.7
  5. Step 5 · Watch it heal

    Delete one Pod and watch the ReplicaSet create a replacement within seconds.

    $ kubectl delete pod web-6d4cf56db6-8kq2p
    $ kubectl get pods
    NAME                   READY   STATUS    AGE
    web-6d4cf56db6-m7x4t   1/1     Running   3m
    web-6d4cf56db6-zt9wn   1/1     Running   3m
    web-6d4cf56db6-qr5vb   1/1     Running   4s
    $ kind delete cluster --name lab

Recap

Quick check

You delete a Pod that belongs to a Deployment. Which components bring it back, and in what order?
Show answer

The ReplicaSet controller (in the controller manager) sees 2 of 3 Pods and creates a new Pod object. The scheduler gives it a node. The kubelet on that node starts its containers through the container runtime. All three only talk to the API server.

The control plane goes down for ten minutes. Does your website go down?
Show answer

No. Running Pods keep running and kube-proxy rules stay in place. But nothing changes either: no new deploys, no scaling, and a crashed Pod on a dead node is not rescheduled until the control plane is back.

Kubernetes cluster architecture: common questions

What is the difference between the control plane and a worker node?

The control plane stores the desired state and decides where Pods run. Worker nodes run the Pods, using the kubelet, kube-proxy and a container runtime.

What does the kubelet do?

It is the agent on every node. It watches the API server for Pods assigned to its node, starts their containers through the container runtime and reports their status.