The previous lesson closed with a promise: you know what Kubernetes does —continuously maintain the state you declare to it— and now it is time to see how it does it. A cluster is not a magic box: it is six or seven processes with tightly defined responsibilities that always communicate through a single central point. Understanding that architecture is what will later let you diagnose problems instead of restarting things at random: when a pod is stuck in Pending, when a deployment makes no progress or when the cluster stops responding, the cause is almost always in one specific piece, and this lesson teaches you which. We will finish by following, step by step, everything that happens from the moment you run kubectl apply -f bookings-api.yaml until the bookings-api container is serving requests on a node.
Contents
- The big picture: control plane and worker nodes
- The control plane components
- The worker node components
- The reconciliation loop: the central idea
- A complete walkthrough of
kubectl apply - What happens when each piece fails
- High availability of the control plane
- The big picture: control plane and worker nodes
A Kubernetes cluster is split into two halves with very different roles:
- The control plane: the brain. It decides, records and watches. It does not run user applications (except on single-node clusters such as minikube).
- The worker nodes: the muscle. They run your application containers and report their state.
flowchart TB
subgraph CP["Control plane"]
API["kube-apiserver<br/>the single entry point"]
ETCD[("etcd<br/>state store")]
SCH["kube-scheduler<br/>decides which node"]
CM["kube-controller-manager<br/>reconciliation loops"]
CCM["cloud-controller-manager<br/>cloud integration"]
API --- ETCD
SCH --> API
CM --> API
CCM --> API
end
subgraph N1["Worker node 1"]
K1["kubelet"]
P1["kube-proxy"]
R1["containerd (CRI)"]
K1 --> R1
end
subgraph N2["Worker node 2"]
K2["kubelet"]
P2["kube-proxy"]
R2["containerd (CRI)"]
K2 --> R2
end
USER["kubectl / CI-CD"] --> API
K1 --> API
K2 --> API
P1 --> API
P2 --> API
Note the most important property of the diagram: every arrow goes through kube-apiserver. No component talks directly to another. The scheduler does not call the kubelet; the kubelet does not query etcd. They all read from and write to the API, and that API is the only thing that touches etcd. This star topology is what makes the system extensible: to add a new capability all you need is a process that watches the API and acts on it (that is a controller, and it is exactly what the operators in module 6 do).
- The control plane components
2.1. kube-apiserver
It is the REST front end of the entire cluster and the only component that writes to etcd. Everything that happens in Kubernetes is an HTTP request against this server.
Its responsibilities, in the order they are applied to each request:
- Authentication: who are you? (client certificate, token, OIDC…).
- Authorization: are you allowed to do this? This is where RBAC comes in (module 8).
- Admission control: modify or reject the object before storing it (for example, injecting default values, applying quotas or rejecting unsigned images).
- Validation of the object schema.
- Persistence in etcd.
- Notification of every subscribed component through the watch mechanism.
It is stateless and horizontally scalable: you can run three replicas behind a load balancer.
2.2. etcd
It is a distributed, consistent key-value database based on the Raft consensus algorithm. It stores absolutely all the cluster state: objects, configuration, secrets and observed state.
Points to remember:
- It is the single source of truth. If you lose etcd with no backup, you have lost the cluster (not the applications already running, but certainly their whole definition).
- It is deployed with an odd number of members (3 or 5) so that quorum can be formed.
- It is sensitive to disk latency: a dedicated SSD is recommended. It is the usual bottleneck in large clusters.
- Its backup (
etcdctl snapshot save) is the cluster's backup, and it is examinable material in the CKA (module 12).
2.3. kube-scheduler
Its job is one single thing, and it does it very well: deciding which node each newly created pod runs on. It continuously watches the API for pods with no nodeName assigned and, for each one, runs two phases:
| Phase | What it does | Example criteria |
|---|---|---|
| Filtering (predicates) | Discards the nodes where the pod cannot go | Is there enough CPU and memory? Does the node carry the nodeSelector labels? Does the pod tolerate the node's taints? Are the required ports free? |
| Scoring | Ranks the viable nodes and picks the best | Balanced resource distribution, affinity and anti-affinity, nodes that already have the image pulled, spreading across zones |
The outcome is not "start the container": it is simply writing the pod's spec.nodeName field through the API. The scheduler never contacts the node. The advanced criteria (affinity, taints, tolerations) are covered in module 6.
2.4. kube-controller-manager
It is a single binary that runs dozens of controllers in parallel, each with its own reconciliation loop. The ones that matter most for this course:
| Controller | What it watches | What it does |
|---|---|---|
| Deployment | Deployment objects | Creates and updates ReplicaSets to roll out new versions |
| ReplicaSet | ReplicaSet objects | Creates or deletes pods until the count matches replicas |
| Node | Node health | Marks nodes as NotReady and evicts their pods if they stop responding |
| Job / CronJob | Tasks | Creates task pods, and CronJobs launch them on a schedule |
| Endpoints / EndpointSlice | Services and pods | Keeps the list of healthy IPs behind each Service |
| ServiceAccount and token | Namespaces | Creates the default service accounts |
| PersistentVolume | PVCs and PVs | Binds claims to volumes |
When in module 2 you write a Deployment with replicas: 3 and see three pods appear, this is the process that created them.
2.5. cloud-controller-manager
It isolates everything that depends on the cloud provider, so that the Kubernetes core carries no AWS, Azure or GCP code. It takes care of:
- Nodes: asking the provider whether a virtual machine has really been deleted.
- Load balancers: when you create a
Serviceof typeLoadBalancer, this is what requests the real load balancer from the cloud (module 4). - Routes: configuring routing between nodes on the provider's network.
On minikube or kind it does not exist, and that is why a Service of type LoadBalancer keeps its external IP at <pending> unless you use minikube tunnel. It is a classic source of confusion that we will come back to.
- The worker node components
3.1. kubelet
It is the agent that runs on every node and the only one that actually starts containers. Its loop is:
- Ask the API which pods have been assigned to it (
spec.nodeName == this node). - Compare that with what is running on the machine.
- Ask the container runtime, over CRI, to start or stop whatever is needed.
- Mount volumes, inject ConfigMaps and Secrets, apply resource limits.
- Run the health probes (module 7) and restart failed containers.
- Continuously report the state of the node and its pods to the API.
Important: the kubelet only obeys the API, it takes orders from nobody else. And it only manages pods, not loose containers: if you start a container with docker run on a node, the kubelet ignores it.
3.2. kube-proxy
It implements the Service concept at network level on each node. It watches Services and their EndpointSlices and programs network rules (iptables or, on large clusters, IPVS) so that traffic sent to a service's virtual IP is spread across the healthy pods.
In its usual mode it is not a proxy in the data path: it writes kernel rules and steps aside. Some modern network plugins (Cilium in eBPF mode) replace it entirely. It is covered in module 4.
3.3. Container runtime and the CRI interface
The kubelet does not know how to create containers: it delegates to a runtime through the CRI (Container Runtime Interface), a standard gRPC API. The common runtimes:
| Runtime | Notes |
|---|---|
| containerd | The most widespread. It is the same engine Docker uses internally |
| CRI-O | Lightweight, aimed exclusively at Kubernetes. Common on OpenShift |
| Docker Engine | No longer supported directly; it needs the cri-dockerd adapter. dockershim was removed in 1.24 |
Below the runtime there is another level (runc) that actually talks to the kernel (namespaces and cgroups). You do not need that detail to operate a cluster, but it explains the phrase "Kubernetes no longer uses Docker": the images you build with Docker keep working perfectly because they follow the OCI standard.
3.4. Network plugin (CNI) and storage plugin (CSI)
Two more interfaces worth naming now:
- CNI (Container Network Interface): the plugin that assigns IPs to pods and lets them see each other across nodes. Calico, Cilium, Flannel. With no CNI installed, nodes stay
NotReady. Module 4. - CSI (Container Storage Interface): the plugin that provisions real disks for persistent volumes. Module 5.
- The reconciliation loop: the central idea
If you take only one idea away from this lesson, make it this one. Every Kubernetes controller runs the same pattern:
infinite loop:
desired_state <- read from the API (the spec field)
actual_state <- observe the world (existing pods, nodes, etc.)
if actual_state != desired_state:
perform the minimum actions to bring them closer
publish what was observed to the API (the status field)Practical consequences of this design, which explain many behaviours you will run into:
- You write
spec; the system writesstatus. Never edit astatusby hand. - It is level-based, not event-based. The controller does not react to "a pod was deleted": it compares the current picture with the desired one. That is why the system recovers even if a message is lost or a controller restarts.
- It is idempotent. Applying the same manifest ten times produces the same result as applying it once.
- Manual fixes do not last. If you delete a pod managed by a ReplicaSet, it comes back within seconds. To make it go away you must change the desired state.
- Failures are "eventually consistent". Time passes between declaring something and it being satisfied; that is why objects have conditions and events, and why
kubectl getsometimes shows intermediate states.
- A complete walkthrough of
kubectl apply
kubectl applyLet us follow a real Rutas Norte request. Assume this minimal file in the repository's k8s/ directory (the details of the format are covered in the lesson Objects, YAML Manifests and the Declarative Model):
# k8s/bookings-api.yaml
apiVersion: v1
kind: Pod
metadata:
name: bookings-api
namespace: rutas-norte-dev
labels:
app: bookings-api
app.kubernetes.io/part-of: rutas-norte
environment: dev
spec:
containers:
- name: api
image: registry.rutasnorte.example/bookings-api:2.4.0
ports:
- containerPort: 3000And you apply it:
Behind that single line of output, twelve steps have taken place.
sequenceDiagram
participant U as kubectl
participant A as kube-apiserver
participant E as etcd
participant S as kube-scheduler
participant K as kubelet (node-2)
participant C as containerd
U->>A: 1-2. POST /api/v1/namespaces/rutas-norte-dev/pods (TLS + credentials)
A->>A: 3. Authentication
A->>A: 4. Authorization (RBAC)
A->>A: 5. Admission (mutation and validation)
A->>E: 6. Write Pod object (no nodeName)
E-->>A: 7. Acknowledgement
A-->>U: 8. 201 Created
A-->>S: 9. Watch event: pending pod
S->>A: 10. Filtering + scoring -> binding to node-2
A->>E: Persist spec.nodeName=node-2
A-->>K: 11. Watch event: you have a new pod
K->>C: 12. CRI: pull the image and create the container
C-->>K: Container running
K->>A: Update status: Running
Step by step, in detail:
- kubectl reads your kubeconfig to work out which cluster to talk to, with which credentials and in which namespace.
- It converts the YAML to JSON and sends it as a
POST(orPATCHif the object already existed) over HTTPS to the resource endpoint. - Authentication. The apiserver validates your client certificate or token. If it fails:
error: You must be logged in to the server (Unauthorized). - Authorization. RBAC checks whether your user may
create podsin therutas-norte-devnamespace. If it fails:Error from server (Forbidden). - Admission control. The admission controllers run in two rounds: first the mutating ones (they add defaults, the ServiceAccount, limits from a LimitRange…) and then the validating ones (ResourceQuota, Pod Security Standards, your own webhooks). Any of them can reject the request.
- Write to etcd. The object is stored. At this moment the pod already exists as an object, even though no container is running. Its state is
Pendingand itsspec.nodeNameis empty. - etcd acknowledges the write through Raft consensus.
- The apiserver replies to kubectl with
201 Created. Your command ends here:kubectl applydoes not wait for the container to start. - The scheduler is notified through its watch on pods with no node assigned.
- The scheduler decides. It filters out nodes without enough resources or otherwise incompatible, scores the rest, picks
node-2and writes the binding to the API. The pod is stillPending, but it now has a destination. - The kubelet on
node-2finds out through its own watch and takes over. - The kubelet executes: it asks the runtime, over CRI, to pull
registry.rutasnorte.example/bookings-api:2.4.0(stateContainerCreating, orImagePullBackOffif the registry refuses the pull), creates the network sandbox by requesting an IP from the CNI plugin, mounts volumes and secrets, and starts the container. It then reports to the API:status.phase = Running.
From then on, the kubelet watches the container permanently and the reconciliation loop stays alive until the object is deleted.
- What happens when each piece fails
This table is pure gold for troubleshooting incidents, and a recurring question in interviews and certifications.
| Failed component | What STOPS working | What KEEPS working |
|---|---|---|
| kube-apiserver | All of kubectl, all controllers, every state change |
Pods already running keep serving traffic; kube-proxy keeps its rules |
| etcd (no quorum) | The API becomes read-only or fails; no change is persisted | Same as above: applications keep serving |
| kube-scheduler | New pods stay Pending forever |
Pods already assigned start and run normally |
| kube-controller-manager | Replicas are not recreated, rollouts do not progress, endpoints are not updated | Existing pods stay alive |
| cloud-controller-manager | No new load balancers are created and deleted nodes are not cleaned up | The rest of the cluster works |
| kubelet on one node | That node becomes NotReady; after ~5 min its pods are recreated on other nodes |
The containers on that node may keep running for a while, but unsupervised |
| kube-proxy on one node | Traffic towards Services from that node stops being routed correctly | The other nodes route correctly |
| CNI plugin | New pods get no IP; they stay in ContainerCreating |
Existing pods keep their networking |
The operational takeaway is reassuring: the control plane is the brain, not the heart. If it goes down, the cluster stops changing, but it does not stop serving. This is a deliberate design and it explains why a control plane outage is serious but rarely a service outage.
- High availability of the control plane
A practice cluster has a single control node. A production cluster, like the one Rutas Norte will want for its rutas-norte-pro namespace, must tolerate losing one control plane machine.
The standard layout:
- 3 control plane nodes (an odd number, because of etcd quorum).
- kube-apiserver replicated across all three, behind a load balancer or a
keepalivedsetup with a virtual IP. Being stateless, it scales active-active. - etcd with 3 members. It tolerates losing 1. With 5 members it tolerates 2. With 2 members it tolerates none: never use an even number.
- scheduler and controller-manager active-passive. All three run, but only one acts: they elect a leader through a
Leaseobject in the API. Only one must reconcile, otherwise they would create duplicate pods. - Stacked or external etcd: stacked (etcd on the same control machines) is the usual and simplest option; external isolates the failure and is used on large clusters.
flowchart LR
LB["Load balancer<br/>or virtual IP"]
subgraph CP1["control-1"]
A1["apiserver"]
E1[("etcd")]
S1["scheduler (leader)"]
end
subgraph CP2["control-2"]
A2["apiserver"]
E2[("etcd")]
S2["scheduler (standby)"]
end
subgraph CP3["control-3"]
A3["apiserver"]
E3[("etcd")]
S3["scheduler (standby)"]
end
LB --> A1
LB --> A2
LB --> A3
E1 <--> E2
E2 <--> E3
E1 <--> E3
On top of that, in a managed cluster (EKS, AKS, GKE, module 10-06) the provider operates all of this for you and you never even see the control plane machines: you pay a fee and only administer the worker nodes. For most companies, Rutas Norte included, that is the sensible option.
Common Mistakes and Tips
- Believing the scheduler starts containers. It does not: it only writes a node name. The kubelet is what starts things. If a pod is
Pending, the problem belongs to the scheduler (it fits on no node); if it isContainerCreatingorImagePullBackOff, the problem belongs to the kubelet or the runtime. - Thinking
kubectl applywaits for the application to be ready. It returns as soon as the object is stored in etcd. To wait for real, usekubectl rollout statusorkubectl wait. - Changing resources by hand on a node. Stopping a container with
crictlordockerachieves nothing: the kubelet recreates it. You always act through the API. - Forgetting the etcd backup. It is the only component whose data is irreplaceable. Without a snapshot, a disaster in etcd means rebuilding the cluster from the manifests (one more reason to keep them all in Git).
- Running 2 etcd members "for redundancy". It makes availability worse: with 2 members quorum is 2, so losing one blocks the cluster. Always an odd number.
- Troubleshooting tip: faced with any problem, walk the chain in order — does the object exist in the API? (
kubectl get) → does it have a node assigned? (kubectl get pod -o wide) → what does the kubelet say? (kubectl describe pod, Events section). That order replicates exactly the walkthrough in section 5.
Exercises
Exercise 1: Pinpointing the responsible piece
For each symptom observed in the Rutas Norte cluster, state which component is the prime suspect and why:
- The
bookings-apipod has beenPendingfor 10 minutes anddescribesays0/3 nodes are available: insufficient memory. kubectl get podsreturnsUnable to connect to the server: dial tcp ... connection refused.- A
web-storepod managed by a ReplicaSet is deleted by hand and is not recreated, but the rest of the cluster responds fine. - The
notifications-workerpod has beenContainerCreatingfor 5 minutes with the eventfailed to pull image ... unauthorized. - A node shows up as
NotReadyand a few minutes later its pods appear on other nodes.
Exercise 2: Rebuilding the walkthrough
Put these eight steps, which are out of order, back in sequence, and point out at which of them the pod goes from Pending to ContainerCreating:
- a) The kubelet asks the runtime to create the container over CRI.
- b) The apiserver writes the object to etcd.
- c) RBAC checks the user's permissions.
- d) The scheduler writes
spec.nodeName. - e) kubectl sends the HTTPS request.
- f) The admission controllers run.
- g) The apiserver replies
201 Created. - h) The kubelet receives the watch event.
Exercise 3: Designing for high availability
Rutas Norte wants a self-managed cluster for rutas-norte-pro that tolerates losing an entire control plane machine. State: how many control nodes you would use, how many etcd members, how the apiserver is reached, in which mode the scheduler and controller-manager work, and explain in two lines what would happen to the web store during the 3 minutes when the control plane was completely down.
Solutions
Solution 1
- kube-scheduler (together with node capacity). The pod was created correctly in the API, but no node passes the filtering phase for lack of memory. The fix is to reduce the pod's
requestsor add capacity. - kube-apiserver (or the network to it).
connection refusedmeans kubectl does not even get as far as authenticating: the process is not listening or the kubeconfig endpoint is wrong. - kube-controller-manager. The ReplicaSet controller is what recreates replicas; if everything else works but nothing reconciles, the suspect is the controller-manager (or its leader election).
- kubelet and container runtime, with the root cause in the registry credentials. The pod already has a node assigned; the failure is in pulling the image (the
imagePullSecretis missing). - The kubelet on that node (or the node's network). Once it stops reporting, the node controller marks it
NotReadyand, after the tolerance period, evicts its pods so they are recreated elsewhere. Expected behaviour, not a fault.
Solution 2
Correct order: e → c → f → b → g → d → h → a.
- e) HTTPS request
- c) authentication and RBAC authorization
- f) admission (mutation and validation)
- b) write to etcd → the pod exists in
Pending - g)
201 Createdreply to kubectl - d) the scheduler assigns a node → still
Pending, but now with a destination - h) the kubelet receives the event
- a) the kubelet creates the container → here the pod moves to
ContainerCreatingand then toRunning
Solution 3
- 3 control plane nodes and 3 etcd members (stacked topology): with a quorum of 2, losing 1 machine is tolerated.
- Access to the apiserver through a load balancer or virtual IP (
keepalived+ HAProxy) spreading requests across the three replicas; the kubeconfig points at that single endpoint, not at a specific machine. - kube-scheduler and kube-controller-manager active-passive, with leader election through a
Lease: all three processes run, but only the leader reconciles, to avoid duplicating actions. - During those 3 minutes without a control plane, the web store keeps working normally: the pods are already running on the nodes and kube-proxy keeps its network rules. What does not happen in those 3 minutes is change: you cannot deploy versions, scale, recreate failed pods or run
kubectl.
Conclusion
A Kubernetes cluster is a set of processes with very strict responsibilities that communicate exclusively through the kube-apiserver, with etcd as the single source of truth. The control plane decides (apiserver, scheduler, controller-manager, cloud-controller-manager) and the nodes execute (kubelet, kube-proxy, runtime over CRI). The conceptual glue is the reconciliation loop: compare the desired state you declare with the observed actual state and act until they match, continuously and idempotently. With that mental map you can already reason about incidents instead of guessing, and you know why a control plane outage stops changes but not the service.
We have the architecture; what we lack is the vocabulary. In the coming lessons, terms such as pod, ReplicaSet, Service, PVC or namespace will appear constantly, and it pays to have a clear map of what each one is and how they relate to each other before using them. That is what the next lesson sets out: Key Concepts and Terminology.
Kubernetes Course
Module 1: Introduction to Kubernetes
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
