10.12 - Core Concepts: The Mental Model
Kubernetes is a declarative control loop. You declare desired state (a manifest), the control plane reconciles reality toward it forever. There are no “start this container” commands; there are desired-state objects that controllers watch and enforce.
The two planes
Control plane (runs on “server” nodes):
- API server: the only thing you ever talk to. Everything goes through
kubectl -> API server(REST, port 6443). It validates requests and stores state in etcd (k3s: SQLite by default, etcd for HA). - Scheduler: picks which node each new pod lands on (filters + scores).
- Controller manager: runs all the built-in controllers (deployment, replicaset, namespace, serviceaccount, endpoint, etc.).
Data plane (runs on every node, including “server” nodes):
- kubelet: the node agent. Watches for pods assigned to its node, talks to the container runtime, reports status.
- Container runtime: k3s bundles containerd.
- kube-proxy: implements Service networking (iptables/IPVS rules for ClusterIP, NodePort).
- Plus the CNI plugin (Flannel in k3s) for pod-to-pod networking.
The objects you will use all week
Pod: the smallest unit. One or more containers sharing a network namespace and volume mounts. Pods are EPHEMERAL: assume any pod can die at any moment. You almost never create a bare pod; you create a controller that makes pods for you.
Controller (workload): a reconciliation loop watching a label selector.
- Deployment: stateless apps. N replicas, rolling updates, rollbacks, scaling. The workhorse. Owns ReplicaSets.
- ReplicaSet: the thing that actually makes/removes pods, owned by a Deployment. You rarely touch it directly.
- StatefulSet: stable identity and storage per pod (db-0, db-1, …). Day 5.
- DaemonSet: exactly one pod per node (metrics collectors, log shippers).
- Job / CronJob: run-to-completion work.
Service: stable network endpoint in front of a changing set of pods. Selects pods by label, load-balances among them, gets a stable ClusterIP + DNS name.
- ClusterIP (default): internal only.
- NodePort: ClusterIP + a port in 30000-32767 on every node.
- LoadBalancer: asks the platform for an external LB (k3s: servicelb, or MetalLB). Disabled in this setup, see 10.11.
- Headless: no ClusterIP; returns pod IPs directly (StatefulSets).
Ingress: HTTP-level routing (host/path -> Service). In k3s that is Traefik, disabled for week 1; see 10.41.
Other objects: Namespace (scoping), ConfigMap/Secret (config, day 4), PV/PVC/StorageClass (storage, day 5), HorizontalPodAutoscaler (scaling), Role/RoleBinding/ServiceAccount (RBAC, beyond this week).
The declarative loop, with a worked example
Manifest says: kind: Deployment, replicas: 3, image: nginx, selector:
app=web.
kubectl apply -f-> API server validates, writes to etcd.- Deployment controller sees “3 replicas wanted, 0 exist”, creates a ReplicaSet (with a new revision).
- ReplicaSet controller creates 3 Pod objects.
- Scheduler assigns each pod to a node.
- kubelet on that node pulls the image, starts the container via containerd, reports Ready.
- kube-proxy programs Service endpoints to the new pod IPs.
Now kill a pod: kubectl delete pod <name>. ReplicaSet notices 2 of 3 and
immediately creates a replacement. THIS is the core idea: nobody “restarts”
anything. Desired state is reconciled. Docker’s restart: always is a
degenerate case of this.
Labels and selectors: the glue
Everything is glued together by labels, not by names or IDs.
kubectl get pods --show-labels
kubectl get pods -l app=web # label selector
kubectl get pods -l 'app in (web,api),tier!=db'
kubectl label pod <name> env=prod # add/change labels live
kubectl label pod <name> env- # remove
Service -> pods: the Service’s selector must match the pods’ labels. Deployment -> pods: the Deployment’s selector must match its template labels (and the selector is immutable after creation, so pick it carefully).
Namespaces: logical partitions
kubectl get namespaces
kubectl create namespace lab
kubectl -n lab get pods # scope a command to a namespace
kubectl get all -n lab
- Default objects: kube-system (control plane), kube-public, kube-node-lease, default (yours).
- DNS names are namespace-scoped:
svc-name.namespace.svc.cluster.local. kubectl config set-context --current --namespace=labmakes lab your default for the rest of the week. Use separate namespaces for challenges to keep cleanup easy:kubectl delete namespace labremoves everything in it.
kubectl basics: the only verbs you need this week
kubectl get <resource> # list (add -o wide / -o yaml / -o json)
kubectl describe <resource> <name> # rich detail + events, the debug tool
kubectl apply -f file.yaml # create/update desired state
kubectl delete <resource> <name> # delete (controllers reconcile!)
kubectl logs <pod> [--previous] # stdout (previous = crashed container)
kubectl exec -it <pod> -- bash # enter a container
kubectl port-forward <pod|svc|deploy> <host>:<port>
kubectl explain <resource>.<field> # offline API docs
kubectl get all does NOT show everything (no PVCs, Ingresses, etc.). Use
kubectl api-resources to see every resource kind.
The three most common mental traps
- “I restarted the container”: there is no restart. You edited desired
state (or the controller replaced a dead pod). The pod is a different
object now.
kubectl get podsshows the new one with a new name/UID. - “I edited the YAML but nothing changed”: you must re-apply or use
kubectl edit/kubectl patch. Editing a running pod’s manifest does nothing; the API server stores what you last applied. - “I deleted the pod, why is it back”: a controller (Deployment) owns it. Delete the Deployment, or scale it to 0.
Terminology quick map (k3s vs vanilla)
| k3s name | Standard name |
|---|---|
| server node | control-plane node (runs kubelet too, by default) |
| agent node | worker node |
| –disable traefik | disable default ingress controller |
| servicelb | k3s’s built-in LoadBalancer provider |
| local-path provisioner | StorageClass “local-path” for PVCs |
Next: 10.21 - Week 1 Plan for the schedule, then 10.22 - Challenges to start practicing.