Kubernetes Core Architecture: Cluster, Pod, Deployment, and Declarative Management

DevOps Learning Notes

Using kind to create a local K8s cluster, understanding Cluster, Node, Pod, and Deployment relationships from scratch, and the core mindset of K8s declarative management

Cluster Composition and Node Roles

A K8s cluster consists of two roles:

  • Control Plane: the brain, responsible for decisions (scheduling, monitoring, storing state)
  • Worker Node: the hands and feet, responsible for actually running containers

A typical production cluster:

1
2
3
4
5
6
7
8
Cluster
โ”œโ”€โ”€ Control Plane Node 1  โ”
โ”œโ”€โ”€ Control Plane Node 2  โ”œโ”€โ”€ usually 3 nodes for HA (high availability)
โ”œโ”€โ”€ Control Plane Node 3  โ”˜
โ”œโ”€โ”€ Worker Node 1  โ”
โ”œโ”€โ”€ Worker Node 2  โ”œโ”€โ”€ scales with workload, could be tens to hundreds
โ”œโ”€โ”€ Worker Node 3  โ”‚
โ””โ”€โ”€ ...             โ”˜

Control Plane runs 3 nodes because etcd (the database storing all cluster state) needs an odd number for consensus โ€” if one goes down, the remaining two can still vote for a leader, and the cluster continues operating.

Brain Components

Component Role
API Server Entry point for all operations. kubectl communicates with it
etcd Distributed key-value database, stores the entire cluster’s state
Scheduler Decides which Node a Pod runs on
Controller Manager Runs various controllers (Deployment, ReplicaSet, etc.)

Workload-Machine Daemons

Component Role
kubelet Agent on each Node, ensures Pods are actually running
kube-proxy Maintains network rules (iptables/ipvs), enables Service routing to Pods
container runtime What actually runs containers (containerd)

Spinning Up a Test Group Locally

kind (Kubernetes IN Docker) runs the entire K8s cluster inside a Docker container, for learning and testing.

1
kind create cluster --name devops-lab

What this command does:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
kind create cluster
 โ”œโ”€โ”€ pull kindest/node Docker image
 โ”œโ”€โ”€ start a Docker container (this is your Node)
 โ”œโ”€โ”€ run the full K8s stack inside the container:
 โ”‚   โ”œโ”€โ”€ etcd
 โ”‚   โ”œโ”€โ”€ API Server
 โ”‚   โ”œโ”€โ”€ Scheduler
 โ”‚   โ”œโ”€โ”€ Controller Manager
 โ”‚   โ”œโ”€โ”€ kubelet
 โ”‚   โ”œโ”€โ”€ kube-proxy
 โ”‚   โ””โ”€โ”€ CoreDNS
 โ””โ”€โ”€ configure kubectl context

kind’s special feature: one Docker container plays both Control Plane + Worker, so kubectl get nodes will only show one Node.

Connection Target Selection in kubeconfig

kubectl uses context to know which cluster to connect to. After kind create completes, it automatically sets kind-devops-lab as the active context.

1
2
kubectl config current-context
# kind-devops-lab

If you have multiple clusters simultaneously (e.g., kind + AWS EKS), you need to use --context to specify.

Making Local Images Available to the Cluster

The container runtime inside kind (containerd) is isolated from the local Docker daemon. Images built with local docker build are not visible to kind.

1
kind load docker-image go-api:0.0.2 --name devops-lab
1
2
3
4
Local Docker daemon                kind Node's containerd
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  go-api:0.0.2    โ”‚ โ”€โ”€transferโ”€โ†’ โ”‚  go-api:0.0.2    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

kind load only transfers the image, it doesn’t start anything. Only when kubectl apply runs will kubelet use this image to create a container.

You can verify the image is in the Node with crictl:

1
docker exec devops-lab-control-plane crictl images | grep go-api

crictl is a CLI for interacting with containerd, just like the docker command is a CLI for interacting with the Docker daemon.

Smallest Schedulable Workload Primitive

A Pod is a wrapper K8s adds on top of containers.

1
2
Docker world:     Container
K8s world:        Pod โ†’ wraps 1 or more Containers

Containers in the same Pod share network (same IP, communicate via localhost) and storage. In most cases, it’s one Pod one container.

Why K8s Wraps Containers in Pods

K8s’s smallest management unit is Pod, not container:

  • Scheduling: Scheduler places the entire Pod onto a Node
  • IP: Assigned to the Pod, containers within the same Pod share it
  • Lifecycle: When a Pod dies, all containers inside die together

Workload Specification Sample

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Pod
metadata:
  name: go-api
  labels:
    app: go-api         # label, used by Service to find this Pod
spec:
  containers:
    - name: go-api
      image: go-api:0.0.2
      ports:
        - containerPort: 8080
1
2
kubectl apply -f k8s/pod.yaml
kubectl describe pod go-api

From kubectl apply to Running Container

The Events section of kubectl describe pod records the complete process:

1
2
3
4
5
Events:
  Scheduled  โ†’ default-scheduler  โ†’ Successfully assigned default/go-api to devops-lab-control-plane
  Pulled     โ†’ kubelet             โ†’ Container image "go-api:0.0.2" already present on machine
  Created    โ†’ kubelet             โ†’ Container created
  Started    โ†’ kubelet             โ†’ Container started

Mapped to component collaboration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kubectl apply
 โ†“
API Server: receives request, stores in etcd
 โ†“
Scheduler: decides which Node โ†’ devops-lab-control-plane     โ† Scheduled
 โ†“
kubelet (on that Node):
 โ”œโ”€โ”€ pull image โ†’ already present (loaded via kind load)      โ† Pulled
 โ”œโ”€โ”€ create container                                         โ† Created
 โ””โ”€โ”€ start container                                          โ† Started

Pods Disappear Without a Controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kubectl get pod go-api -o wide
# IP: 10.244.0.5

kubectl delete pod go-api
kubectl get pods
# No resources found โ€” gone forever

kubectl apply -f k8s/pod.yaml
kubectl get pod go-api -o wide
# IP: 10.244.0.6 โ€” IP changed

Two key observations:

  • Deleting a Pod, nothing will recreate it for you
  • Re-applying, the Pod gets a different IP

This is why you don’t use bare Pods in production โ€” you need a Deployment to manage them.

Kubernetes Tenant Scope Versus OS Isolation Primitives

Same name, completely different things:

  • Linux Namespace (from Phase 1 content): kernel-level isolation mechanism (PID, Network, Mount…)
  • K8s Namespace: logical grouping, like folders classifying resources in a cluster
1
2
3
kubectl get namespaces
# default        โ† your Pod lives here
# kube-system    โ† K8s internal components

ReplicaSet Driver: Autonomous Replica Guardian

Deployment solves two problems with bare Pods: they’re gone when deleted, and you can’t do zero-downtime updates.

Owner Chain: Deployment โ†’ ReplicaSet โ†’ Pod

1
2
3
4
5
Deployment (you define: I want 3 go-api instances)
 โ””โ”€โ”€ ReplicaSet (auto-created, maintains the count)
      โ”œโ”€โ”€ Pod 1
      โ”œโ”€โ”€ Pod 2
      โ””โ”€โ”€ Pod 3

You only manage the Deployment. ReplicaSet and Pods are managed automatically.

Pod Manifest Referencing the Registry

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-api
spec:
  replicas: 3                    # always maintain 3 Pods
  selector:
    matchLabels:
      app: go-api                # identifies which Pods belong to this Deployment
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                # at most 1 extra Pod during update
      maxUnavailable: 0          # no Pod downtime allowed (zero-downtime)
  template:                      # Pod template
    metadata:
      labels:
        app: go-api
    spec:
      containers:
        - name: go-api
          image: go-api:0.0.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 50m           # minimum 0.05 CPU cores
              memory: 32Mi       # minimum 32MB memory
            limits:
              cpu: 100m          # maximum 0.1 CPU cores
              memory: 64Mi       # maximum 64MB memory

Everything under template is the spec from the earlier pod.yaml, now wrapped inside the Deployment.

Auto-Generated Naming Convention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kubectl get deployment
# go-api

kubectl get rs
# go-api-668dcc5dd                   โ† Deployment name + hash

kubectl get pods
# go-api-668dcc5dd-72s9z             โ† ReplicaSet name + random suffix
# go-api-668dcc5dd-fbz2q
# go-api-668dcc5dd-zbk6l

You can see the Deployment โ†’ ReplicaSet โ†’ Pod hierarchy from the names alone.

Convergence Toward Declared Intent

1
2
3
4
5
kubectl delete pod go-api-668dcc5dd-72s9z
kubectl get pods
# go-api-668dcc5dd-5gcng   โ† brand new, AGE is seconds
# go-api-668dcc5dd-fbz2q   โ† unchanged
# go-api-668dcc5dd-zbk6l   โ† unchanged

Completely different from bare Pods โ€” delete one, a replacement is created immediately:

1
2
3
4
5
6
7
one Pod deleted โ†’ actual count = 2
 โ†“
ReplicaSet detects: desired=3, actual=2, short by 1
 โ†“
auto-creates a new Pod to compensate
 โ†“
actual count back to 3

This loop runs continuously. No matter how a Pod dies โ€” manual deletion, crash, Node failure โ€” it will be replaced.

Zero-Downtime Image Changes

When updating the image version, the Deployment manages two ReplicaSets simultaneously:

1
2
3
4
5
6
7
Deployment
 โ”œโ”€โ”€ ReplicaSet v1 (old version, scaling down)
 โ”‚    โ””โ”€โ”€ Pod (old)
 โ””โ”€โ”€ ReplicaSet v2 (new version, scaling up)
      โ”œโ”€โ”€ Pod (new)
      โ”œโ”€โ”€ Pod (new)
      โ””โ”€โ”€ Pod (new)

maxSurge: 1, maxUnavailable: 0 means: first create the new Pod and confirm Ready, then kill the old Pod โ€” achieving zero downtime.

Production Workflows Use Deployments Directly

In real work, you write Deployments directly:

1
kubectl apply -f k8s/deployment.yaml

Deployment automatically creates ReplicaSet and Pods. No need to write a separate pod.yaml.

Desired-State Configuration Model

K8s core mindset โ€” you write YAML describing “the state I want”, and K8s makes it happen:

1
2
3
4
5
6
7
write YAML (desired state)
 โ†“
kubectl apply
 โ†“
K8s continuously ensures actual state = desired state
 โ†“
need changes โ†’ edit YAML โ†’ apply again

This is the same mindset as Terraform and Docker Compose, all belonging to Infrastructure as Code (IaC):

Tool What It Manages
Terraform Cloud resources (EC2, RDS, VPC…)
Docker Compose Local multiple containers
K8s YAML Application deployment in cluster

YAML files go into git, changes are trackable, all infrastructure is defined and managed through code.

Management Commands for the Test Group

A kind cluster is just a Docker container, manageable directly with Docker:

1
2
3
4
5
6
7
8
# pause (state preserved, resumes on next start)
docker stop devops-lab-control-plane

# resume
docker start devops-lab-control-plane

# destroy completely (everything gone, needs full rebuild)
kind delete cluster --name devops-lab

kind doesn’t use Docker Compose. kind creates containers itself using the Docker API.

References