Skip to content

Phase 4 · ORCHESTRATION & IAC

K8s architecture + kubectl with kind on WSL2

Day 67 of 90 ~55 min 0/20 in phase Builds on Day 66

By the end of today

  • Name every control-plane and node component and what each does
  • Run kubectl get, describe and apply against a local kind cluster
  • Explain how kubectl and namespaces route requests through the API server

The control plane, the nodes, and how kubectl drives them

Section 1 of 5 · ~3 min

Yesterday you saw why Kubernetes exists — it keeps a fleet of containers running the way you declared, rescheduling and restarting them without you watching. Today you meet the machine that does it, and the one command you’ll drive it with for the rest of Phase 4: kubectl.

A cluster splits in two. The control plane is the brain; the nodes are the muscle that runs your containers. Four control-plane parts are worth naming:

  • kube-apiserver — the front door. Every request, from you, a controller, or a node, is an HTTP call to it. kubectl never talks to anything else.
  • etcd — the cluster’s memory: a consistent key-value store holding all desired and observed state. Lose etcd and you lose the cluster.
  • kube-scheduler — decides which node a new Pod lands on, weighing free CPU, memory and any constraints you set.
  • kube-controller-manager — the reconcile loops. It compares “what you asked for” with “what exists” and closes the gap: a Deployment wants 3 replicas, 2 run, it creates one more.

Each node carries two agents: the kubelet takes Pod specs from the API server and makes the runtime start the containers, reporting health back; kube-proxy programs the node’s networking so a Service’s stable address reaches the right Pods.

Real world: The control plane is a warehouse’s dispatch office; the nodes are the forklift drivers on the floor. You hand dispatch an order sheet — the desired state — and they assign drivers and track every pallet. If a driver clocks out mid-shift, the office reassigns the load without you walking onto the floor. You never move a box yourself; you change the order sheet.

The loop that ties it together is declarative: you kubectl apply a YAML manifest describing the state you want, the API server writes it to etcd, and the scheduler and controllers work until reality matches. You never SSH to a node to “start the app” — you tell the API server what you want and the cluster converges. That is the whole shift away from docker run.

Kubernetes architecture: kubectl sends HTTP requests to the kube-apiserver, which reads and writes cluster state in etcd; the scheduler and controller-manager also talk only to the API server; each worker node runs a kubelet and kube-proxy that the API server directs. kubectl HTTP you control plane kube-apiserver etcd scheduler controller-manager worker node kubelet kube-proxy runs your Pods
kubectl talks only to the API server; the API server is the one door to etcd, the controllers, and the nodes.

kubectl is your single client. get lists resources (kubectl get pods) — add -o wide for node and IP columns, -o yaml for the full object. describe dumps one resource plus its recent Events, your first stop when something’s wrong. apply -f sends a manifest and creates or updates to match. Two flags scope everything: -n <namespace> picks the namespace — a virtual partition of the cluster, with system pieces in kube-system — and your context (kubectl config current-context) decides which cluster you hit, so a staging command never lands on prod.

A named example: Amazon EKS and Google GKE sell exactly this split. You get the nodes; they run and back up the control plane — the API server and etcd — so you never manage them yourself. Today you run the whole thing locally with kind (Kubernetes IN Docker): one kind create cluster packs a real API server, etcd, scheduler and kubelet into containers on your WSL2 box — no cloud, no bill, no waiting.

Hands-On Lab

Section 2 of 5 · ~4 min

Budget about 25 minutes. Open your WSL2 Ubuntu 24.04 terminal with Docker running (Phase 2) and kubectl and kind 0.24+ installed. Everything here runs locally on kind, so there’s no cloud account and nothing to bill. Pod name suffixes, IPs, ages, UIDs and the node name are unique to your cluster — yours will differ from the samples.

# 1. Confirm the tools are present. kind and kubectl are separate binaries.
kind version && kubectl version --client
# Output (your versions will differ):
# kind v0.24.0 go1.22.6 linux/amd64
# Client Version: v1.31.0
# Kustomize Version: v5.4.2
# 2. Create a local single-node cluster. kind runs the control plane inside a Docker container.
kind create cluster
# Output (takes ~30s; the last line sets your kubectl context):
# Creating cluster "kind" ...
#  ✓ Ensuring node image (kindest/node:v1.31.0) 🖼
#  ✓ Preparing nodes 📦
#  ✓ Writing configuration 📜
#  ✓ Starting control-plane 🕹️
#  ✓ Installing CNI 🔌
#  ✓ Installing StorageClass 💾
# Set kubectl context to "kind-kind"
# 3. Ask the API server where it lives. This is the fastest "is my cluster up?" check.
kubectl cluster-info
# Output (the port is random per cluster — yours will differ):
# Kubernetes control plane is running at https://127.0.0.1:44xxx
# CoreDNS is running at https://127.0.0.1:44xxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
# 4. Confirm which cluster kubectl is pointed at BEFORE you run anything real.
kubectl config current-context
# Output:
# kind-kind
# 5. List the nodes. -o wide adds internal/external IP, OS image, kernel version and runtime columns.
kubectl get nodes -o wide
# Output (NAME, IP and AGE will differ):
# NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE              KERNEL-VERSION                       CONTAINER-RUNTIME
# kind-control-plane   Ready    control-plane   90s   v1.31.0   172.18.0.2    <none>        Debian GNU/Linux 12   5.15.167.4-microsoft-standard-WSL2   containerd://1.7.18
# 6. List Pods in the default namespace. A fresh cluster has none of YOUR workloads yet.
kubectl get pods
# Output (this is expected, not an error — default is empty):
# No resources found in default namespace.
# 7. Now list Pods across ALL namespaces with -A. The cluster's own machinery appears.
kubectl get pods -A
# Output (suffixes and ages differ; ~9 Pods across two namespaces — kube-system and local-path-storage):
# NAMESPACE            NAME                                         READY   STATUS    RESTARTS   AGE
# kube-system          coredns-7db6d8ff4d-abcde                     1/1     Running   0          2m
# kube-system          coredns-7db6d8ff4d-fghij                     1/1     Running   0          2m
# kube-system          etcd-kind-control-plane                      1/1     Running   0          2m
# kube-system          kindnet-abcde                                1/1     Running   0          2m
# kube-system          kube-apiserver-kind-control-plane            1/1     Running   0          2m
# kube-system          kube-controller-manager-kind-control-plane   1/1     Running   0          2m
# kube-system          kube-proxy-x9k2p                             1/1     Running   0          2m
# kube-system          kube-scheduler-kind-control-plane            1/1     Running   0          2m
# local-path-storage   local-path-provisioner-6f5d79df6-abcde       1/1     Running   0          2m
# 8. Focus one namespace with -n. kube-system is where the control plane runs on kind.
kubectl get pods -n kube-system
# Output (trimmed to a few representative rows; scoping with -n drops the NAMESPACE column):
# NAME                                         READY   STATUS    RESTARTS   AGE
# etcd-kind-control-plane                      1/1     Running   0          2m
# kube-apiserver-kind-control-plane            1/1     Running   0          2m
# kube-scheduler-kind-control-plane            1/1     Running   0          2m
# 9. describe one component Pod — read past the labels to the Events at the bottom.
kubectl describe pod -n kube-system etcd-kind-control-plane
# Output (trimmed; the UID and image digest are yours alone):
# Name:                 etcd-kind-control-plane
# Namespace:            kube-system
# Priority:             2000001000
# Node:                 kind-control-plane/172.18.0.2
# Status:               Running
# ...
# Events:               <none>   (nothing has gone wrong since it started)
# 10. See the raw object the API server holds. -o yaml is what apply reads back.
kubectl get pod -n kube-system kube-apiserver-kind-control-plane -o yaml | head -n 12
# Output (head -n 12; kubectl serializes map keys alphabetically, so annotations sort first — name and namespace come after labels and fall past line 12):
# apiVersion: v1
# kind: Pod
# metadata:
#   annotations:
#     kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 172.18.0.2:6443
#     kubernetes.io/config.hash: 8d1f9e3c7a5b2d4e6f0a1c3b5d7e9f2a
#     kubernetes.io/config.mirror: 8d1f9e3c7a5b2d4e6f0a1c3b5d7e9f2a
#     kubernetes.io/config.seen: "2026-07-12T09:14:22Z"
#     kubernetes.io/config.source: file
#   creationTimestamp: "2026-07-12T09:14:25Z"
#   labels:
#     component: kube-apiserver
# 11. List the namespaces themselves. default is empty; the other four hold system pieces.
kubectl get namespaces
# Output (AGE differs):
# NAME                 STATUS   AGE
# default              Active   3m
# kube-node-lease      Active   3m
# kube-public          Active   3m
# kube-system          Active   3m
# local-path-storage   Active   3m

Read those last screens back: a single kind create cluster gave you a genuine API server, etcd, scheduler and kubelet — the exact components from the concept — all Running in kube-system, with kind’s storage provisioner over in local-path-storage, while default sits empty and waiting. Tomorrow you fill it. Leave the cluster up; you’ll deploy into it next.

Common Errors & Fixes

Section 3 of 5 · ~2 min

These are the mistakes that trip people up on their first kind cluster on WSL2. Read the error text slowly — parsing it is the skill.

Common error: Running kind create cluster while the Docker daemon is down — kind has nothing to build the cluster inside:

ERROR: failed to create cluster: failed to list nodes: command "docker ps ..." failed with error: exit status 1
Command Output: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

Why: kind runs each Kubernetes node as a Docker container, so it drives the Docker daemon to create them. If the daemon isn’t started — common on a fresh WSL2 shell — kind fails before it creates anything. This is a Docker problem, not a Kubernetes one.

Fix: Start Docker (sudo service docker start on WSL2, or launch Docker Desktop), confirm with docker ps, then re-run kind create cluster.

How you’d spot it in prod: Any CI job that provisions an ephemeral kind cluster and fails at creation with “Cannot connect to the Docker daemon” has a runner whose Docker service isn’t up — fix the runner, not the cluster config.

Common error: kubectl printing a connection error even though kind create cluster succeeded:

The connection to the server localhost:8080 was refused - did you specify the right host or port?

Why: kubectl fell back to its built-in default of localhost:8080 because it found no working context — usually a KUBECONFIG pointing at the wrong file, or a context that was switched away from kind-kind. The API server is fine; kubectl just isn’t aimed at it.

Fix: Point kubectl back at the cluster: kubectl config use-context kind-kind, then verify with kubectl config current-context. If the context is missing entirely, kind export kubeconfig rewrites it.

How you’d spot it in prod: A localhost:8080 refusal on a machine that has never run a local cluster means an unset or stale kubeconfig — the kubectl command is talking to nothing, so check the context before you suspect the cluster.

Common error: A typo in the resource type, so kubectl asks the API server for something that doesn’t exist:

error: the server doesn't have a resource type "deploments"

Why: kubectl doesn’t guess. It asks the API server which resource types exist, and deploments (missing an y) isn’t one — the real type is deployments. The server reports the type as unknown rather than silently correcting it.

Fix: Retype the resource name — kubectl get deployments — or use the short name deploy. Run kubectl api-resources to see every valid type and its short aliases.

How you’d spot it in prod: A script failing with “the server doesn’t have a resource type” is almost always a typo or a Custom Resource whose CRD isn’t installed on that cluster. Check the spelling against kubectl api-resources before assuming the cluster is broken.

Kubernetes Architecture Interview Questions

Section 4 of 5 · ~1 min

Cover the answers below and say your own version out loud first — name the four control-plane components, then walk what happens on a kubectl apply, before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~30 more minutes today:

  • 5 min — Run kubectl explain pod and then kubectl explain pod.spec.containers to see the API server describe its own object schema field by field — the same schema apply validates against.
  • 10 min — Run kubectl get pods -A -o wide and notice every control-plane Pod sits on the one kind-control-plane node; then kubectl logs -n kube-system kube-apiserver-kind-control-plane | tail to watch the front door narrate the requests it’s serving.
  • 15 min — Read the official Kubernetes Components page and match each box on it to a Pod you saw in kube-system — the diagram in the concept is a simplified version of it.
What are the components of the Kubernetes control plane? Both

Four pieces. The kube-apiserver is the front door — every request, from me, a controller or a node, is an HTTP call to it, and nothing talks to anything else directly. etcd is the cluster's memory: a consistent key-value store holding the whole desired and observed state, which is why losing it loses the cluster. The kube-scheduler decides which node a new Pod lands on, weighing free CPU, memory and constraints. The kube-controller-manager runs the reconcile loops — it compares what you asked for against what exists and closes the gap, like creating a replacement when a Pod dies. Nodes then run kubelet and kube-proxy, but those four are the brain.

What actually happens when you run kubectl apply -f deployment.yaml? Both

kubectl sends the manifest as an HTTP request to the kube-apiserver, which authenticates me, validates the object, and writes the desired state into etcd — that's the whole of my part. From there controllers take over: the Deployment controller sees it wants three replicas and creates the Pods, the scheduler assigns each Pod to a node with room, and that node's kubelet tells the container runtime to pull the image and start the containers. Nothing ran the app because I said 'run' — I declared the state I wanted and the control plane converged on it. That declarative apply-and-reconcile loop is the core difference from typing docker run by hand.

What's the difference between the kubelet and kube-proxy on a node? Service

Both run on every node but do different jobs. The kubelet is the workload agent: it takes the Pod specs the API server assigns to its node, tells the container runtime to start those containers, and continuously reports their health back up. If a container dies, the kubelet is what notices and restarts it per the spec. kube-proxy is the networking agent: it programs the node's iptables or IPVS rules so a Service's stable virtual IP load-balances to the right backend Pods, wherever they live. Rough split — kubelet makes containers run, kube-proxy makes Service traffic reach them. Neither makes scheduling decisions; that's the control plane's job.

What is a namespace, and why do system components live in kube-system? Both

A namespace is a virtual partition of one physical cluster — a scope for names and a boundary for quotas and access control. Two teams can each have a Pod called web in separate namespaces without colliding, and I can grant RBAC or set resource quotas per namespace. Kubernetes ships a few by default: default is where your objects go when you don't specify one, and kube-system holds the cluster's own machinery — CoreDNS, kube-proxy, the CNI, and on a real cluster the control-plane Pods. Keeping system workloads there separates them from application workloads, so kubectl get pods in default looks empty on a fresh cluster until you deploy something. I scope commands with -n.

Mark Day 67 complete

Tomorrow you stop poking at system Pods and run your own — Pods and Deployments turn your app into workloads the cluster keeps alive.

Stuck on today’s lab? Ask in Mission 90 Q&A