🗺 Live Cluster Visual — Every Component Explained

What is each item in the cluster screenshot, why it exists, the command that creates it, and what breaks without it.

Live Kubernetes Cluster — the screenshot you asked about
Control Plane · K8S V1.29 (EKS-Managed)
api-server etcd scheduler controller-mgr
ns: prod
deploy/webapp 6/6 sts/mysql svc/nginx-svc 🌐 svc/webapp-lb cm/app-config 🔐 secret/db-creds
⊙ node-01v1.29.0
nginx webapp-v2-bbb webapp-v2-zzz webapp-v2-d2 mysql-0
⊙ node-02v1.29.0
webapp-v2-ccc webapp-v2-d1 webapp-v2-d3 mysql-1
ns: kube-system
ds/fluent-bit (2 pods · 2 nodes)

CONTROL PLANE · K8S V1.29 (EKS-MANAGED)

The brain of Kubernetes. Four processes that make every scheduling, storage, and reconciliation decision. On EKS, AWS runs these — you never touch them directly.

🚪
api-server
Glowing cyan — active right now

Why it exists

Every kubectl command, every SDK call, every controller watch — all go through the API server. It is the single authoritative front door. Nothing changes in the cluster without going through it.

Without it

Zero visibility. kubectl get pods fails immediately. No deployments, no scaling, nothing. The cluster is a black box.

$ kubectl cluster-info
Kubernetes control plane is running at
https://api.k8s.local:6443
Analogy: The API server is the hotel front desk. Every guest request (kubectl) goes through it. The hotel still stands without the desk, but nobody can check in, check out, or know which rooms are occupied.
🧠
etcd
The cluster's memory

Why it exists

Distributed key-value store. Holds the complete desired state of every object — every Pod spec, every Service, every ConfigMap. The API server reads and writes here for every operation.

Without it

The cluster loses all memory instantly. No record of what pods should exist, what namespaces were created, what deployments were applied. On EKS, AWS replicates etcd across 3 AZs so this never happens.

$ kubectl get --raw /metrics | grep etcd
etcd_db_total_size_in_bytes 8.3MB
# On EKS: you cannot access etcd directly — AWS manages it
Analogy: etcd is the hotel's booking database. Without it, the front desk has no record of any reservation. On EKS: it's the cloud-backed database AWS runs in 3 data centres simultaneously.
📍
scheduler
Pod placement engine

Why it exists

When a Pod is created, it starts as Pending. The scheduler's job is to find the best node. It runs two passes: predicates (filter ineligible nodes) then priorities (score eligible ones). Picks the winner.

Without it

Every pod stays Pending forever. No container ever starts. The cluster has workers but nothing to tell them what to run.

$ kubectl describe pod webapp-v2-bbb -n prod
Events:
Normal Scheduled 0s default-scheduler
"Successfully assigned prod/webapp-v2-bbb to node-01"
Scheduler decision flow
Pending Pod webapp-v2-zzz cpu:250m req Predicates (filter) node-01 ✓ node-X ✗ no CPU node-02 ✓ Priorities (score) node-01: 82 pts node-02: 71 pts → node-01 wins Pod scheduled here
🔄
controller-manager
The reconciliation engine

Why it exists

Runs control loops. Every second it checks: does actual state match desired state? If not, it acts. The ReplicaSet controller sees 2 pods when you wanted 3 → creates 1 more. This loop never stops.

Without it

Pods that crash stay dead. Deployments can't roll out. StatefulSets can't order their pods. Self-healing stops entirely.

$ kubectl delete pod webapp-v2-bbb -n prod # simulate crash
pod deleted
ReplicaSet: desired=3, actual=2 → creating replacement
webapp-v2-zzz ContainerCreating ← controller acted in <1s
Analogy: A hotel manager who walks the floors all night. If a room service tray is missing, they put one back. If a light bulb is out, they replace it. Desired state = hotel policy. Actual state = what the manager finds on the walk.

ns: prod — logical isolation

A virtual cluster inside your physical cluster. Everything inside the dashed orange border lives in the prod namespace and is isolated from default and kube-system.

Why it exists

Without namespaces, every team's pods, services, and configs live in the same flat space. Names collide. A junior dev's test pod deletes a production config. RBAC becomes impossible to scope. Namespaces enforce boundaries.

Without namespaces

All 100 teams' resources mixed in default. No isolation. Network Policies can't scope to a team. Billing is impossible to attribute. Service names like mysql collide across teams.

$ kubectl create namespace prod
namespace/prod created

$ kubectl apply -f app.yaml -n prod
# All resources created inside prod namespace

$ kubectl get all -n prod
NAME READY STATUS
pod/nginx 1/1 Running
pod/webapp-v2-bbb 1/1 Running
Namespace isolation
One Cluster — Three Namespaces Kubernetes Cluster ns: prod deploy/webapp svc/nginx-svc 🔐 secret/db-creds ns: staging deploy/webapp (same name — no clash) ns: kube-system ds/fluent-bit svc/kube-dns K8s system pods cannot see each

node-01 + node-02 — the compute machines

EC2 instances (on EKS) that actually run your containers. The orange border means they're managed by you (data plane). Each runs kubelet, kube-proxy, and the container runtime.

Why you need TWO nodes

Single node = single point of failure. If node-01 crashes, ALL pods on it die. With node-02, the scheduler spreads pods across both. A node failure takes down ~half your pods, not all. On EKS, the managed node group Auto Scaling Group automatically replaces a failed node.

$ kubectl get nodes -o wide
NAME STATUS VERSION INTERNAL-IP
node-01 Ready v1.29.0 10.0.0.10
node-02 Ready v1.29.0 10.0.0.11

$ kubectl describe node node-01 | grep -A5 Allocatable
Allocatable:
cpu: 1900m ← 1.9 cores available for pods
memory: 3532Mi ← 3.5 GB available for pods
# (2000m total minus ~100m reserved for system)
What runs inside each node
node-01 (EC2 t3.medium) kubelet manages pod lifecycle kube-proxy iptables for services containerd (container runtime) nginx ✓ ready webapp-v2-bbb ✓ ready mysql-0 StatefulSet node-02 (EC2 t3.medium) kubelet manages pod lifecycle kube-proxy iptables for services containerd (container runtime) webapp-v2-ccc ✓ ready webapp-v2-d1 ✓ ready mysql-1 StatefulSet

nginx · webapp-v2-bbb · webapp-v2-zzz · webapp-v2-d2

The actual running containers. Each chip = one pod = one or more containers sharing the same network namespace. Colour tells you the state instantly.

nginx
Green glow = Ready ✓

Container running + readiness probe passing. kube-proxy has registered this pod as a healthy backend for any matching Service.

$ kubectl apply -f nginx-pod.yaml
pod/nginx created
$ kubectl get pod nginx -n prod
nginx 1/1 Running 0 30s
webapp-v2-zzz
Blue = New / Starting

Just created by the ReplicaSet controller after a pod crash. Image is pulling or container is starting. Not yet ready — Service won't send traffic here yet.

$ kubectl get pod webapp-v2-zzz -n prod
webapp-v2-zzz 0/1 ContainerCreating 0 2s
# kube-proxy skips this pod — not ready yet
mysql-0
Purple = StatefulSet pod

Stable ordered name — mysql-0 always on the same PVC. Won't be replaced by a random hash like Deployment pods. Its identity (name + storage) persists across restarts.

$ kubectl get pods -n prod -l app=mysql
mysql-0 1/1 Running 0 node-01
mysql-1 1/1 Running 0 node-02
Pod names from Deployments have random hashes (webapp-v2-bbb) — they change on every restart. StatefulSet pod names are ordered and stable (mysql-0, mysql-1). This is the key Deployment vs StatefulSet distinction.

deploy/webapp 6/6 — the workload controller

6/6 means: 6 pods desired, 6 pods ready. The Deployment manages a ReplicaSet which manages the pods. It enables rolling updates, rollbacks, and self-healing.

Why it exists

Without a Deployment, you'd manually create each pod. When one crashes, it stays dead. No rolling update exists. You'd have to delete all pods and recreate manually for every version change — guaranteed downtime.

$ kubectl apply -f webapp-deploy.yaml
# webapp-deploy.yaml:
# kind: Deployment, spec.replicas: 6
# spec.template.spec.containers: [{image: webapp:v2}]
deployment.apps/webapp created

$ kubectl rollout status deploy/webapp -n prod
deployment "webapp" successfully rolled out

$ kubectl set image deploy/webapp webapp=webapp:v3 -n prod
deployment.apps/webapp image updated
# Rolling: new RS scales up, old RS scales down — zero downtime

$ kubectl rollout undo deploy/webapp -n prod
deployment.apps/webapp rolled back ← instant!
Deployment → ReplicaSet → Pods hierarchy
Deployment/webapp replicas: 6, image: webapp:v2 ReplicaSet-v2 (6/6) maintained by controller-manager webapp-v2-bbb webapp-v2-ccc webapp-v2-zzz webapp-v2-d1 webapp-v2-d2 webapp-v2-d3 6 pods across 2 nodes — if controller-manager detects any missing, it creates replacements

sts/mysql — stable identity workload

mysql-0 always exists on node-01, mysql-1 on node-02. Each has its own PersistentVolumeClaim. Names survive restarts. Unlike Deployments where pods are interchangeable, here each pod is unique.

Why it exists

Databases need stable identity. mysql-0 is the primary. mysql-1 is the replica. If they had random names, the database replication config would break on every restart. StatefulSet preserves pod identity and its attached storage permanently.

What breaks with a Deployment for MySQL

After a restart, pod name changes from mysql-abc to mysql-xyz. The PVC binding is lost. The replica server can't find the primary by name. Data is gone if PVC wasn't bound. Total database corruption.

$ kubectl apply -f mysql-sts.yaml
statefulset.apps/mysql created

$ kubectl get pods -n prod -l app=mysql
mysql-0 1/1 Running 0 node-01 ← primary, starts FIRST
mysql-1 1/1 Running 0 node-02 ← replica, starts AFTER

$ kubectl get pvc -n prod
data-mysql-0 Bound 10Gi RWO ← mysql-0 owns this forever
data-mysql-1 Bound 10Gi RWO ← mysql-1 owns this forever
Deployment vs StatefulSet behaviour on pod restart
Deployment (stateless) Before restart webapp-abc webapp-def webapp-ghi After restart — NEW names, pods interchangeable ✓ webapp-xyz webapp-pqr webapp-mno StatefulSet (stateful) Before restart mysql-0 mysql-1 After restart — SAME names, same PVCs ✓ mysql-0 mysql-1 ↑ Deployment pods are interchangeable — StatefulSet pods are NOT Use StatefulSet for: MySQL · Kafka · Elasticsearch · Redis Cluster · ZooKeeper Use Deployment for: web frontends · APIs · workers · anything stateless

svc/nginx-svc (ClusterIP) + 🌐 svc/webapp-lb (LoadBalancer)

Pods die and restart with new IPs. Services give you a stable virtual IP + DNS name that never changes. kube-proxy routes traffic to healthy pods behind the scenes.

ClusterIP — internal only

Why it exists

Other pods in the cluster use nginx-svc.prod.svc.cluster.local to reach nginx. Without it, they'd need to hard-code pod IPs which change on every restart. ClusterIP is the backbone of all service-to-service communication.

$ kubectl apply -f nginx-svc.yaml
# type: ClusterIP, selector: {app: nginx}, port: 80
service/nginx-svc created

$ kubectl get svc nginx-svc -n prod
NAME TYPE CLUSTER-IP PORT(S)
nginx-svc ClusterIP 10.96.100.10 80/TCP

# From any pod in the cluster:
$ curl nginx-svc.prod.svc.cluster.local
Welcome to nginx!
ClusterIP is ONLY reachable from inside the cluster. You cannot curl it from your laptop. Not accessible from the internet.
🌐 LoadBalancer — internet exposed

Why it exists

On EKS, creating a LoadBalancer Service automatically provisions a real AWS NLB. The DNS name appears as EXTERNAL-IP. Traffic flows: internet → NLB → any worker node → kube-proxy iptables → pod. No manual load balancer config needed.

$ kubectl apply -f webapp-lb.yaml
# type: LoadBalancer, selector: {app: webapp}
service/webapp-lb created

$ kubectl get svc webapp-lb -n prod
EXTERNAL-IP: a1b2c3.elb.us-east-1.amazonaws.com

$ curl http://a1b2c3.elb.us-east-1.amazonaws.com
"hello from webapp v2"

# Traffic path: internet → NLB → node:NodePort → iptables → pod

cm/app-config — externalise your config

Separates configuration from the container image. The same Docker image runs in dev, staging, and prod by consuming different ConfigMaps. No rebuild needed when config changes.

Why it exists

Baking config into the image means rebuilding and redeploying for every environment change. A ConfigMap decouples config from code. Change LOG_LEVEL from debug to info? Update the ConfigMap, restart the pod. Zero image rebuild.

Without ConfigMap

Hardcoded DB_HOST=localhost in the image. Works in dev. Fails in prod. Build a new image for prod. Now you're maintaining two slightly different images for one app. Drift happens. Bugs appear only in prod.

$ kubectl create configmap app-config \
--from-literal=DB_HOST=postgres.prod \
--from-literal=LOG_LEVEL=info -n prod
configmap/app-config created

# Consume as env vars in pod spec:
# env:
# - name: DB_HOST
# valueFrom:
# configMapKeyRef:
# name: app-config
# key: DB_HOST

🔐 secret/db-creds — sensitive data (base64 ≠ encrypted)

Stores passwords, tokens, keys. The red badge + lock icon is a visual warning: Secrets are base64 encoded in etcd by default — anyone with kubectl access can decode them instantly. Enable KMS for real encryption.

⚠ The base64 trap (critical exam point)

base64 is encoding, NOT encryption. echo "cEBzc3cwcmQ=" | base64 -d gives you the password in plain text immediately. To truly protect secrets: enable KMS envelope encryption at the EKS cluster level, or use AWS Secrets Manager with ASCP.

Still better than hardcoding

Even without KMS, Secrets are better than env vars in a Dockerfile or committed plaintext. RBAC can restrict who can kubectl get secret. The secret isn't in version control. It's a foundation to build on.

$ kubectl create secret generic db-creds \
--from-literal=password=p@ssw0rd -n prod
secret/db-creds created

$ kubectl get secret db-creds -n prod -o yaml
data:
password: cEBzc3cwcmQ= ← base64, NOT encrypted!

$ echo "cEBzc3cwcmQ=" | base64 -d
p@ssw0rd ← decoded in 1 second

# Real fix: enable KMS on EKS + use AWS Secrets Manager

ds/fluent-bit (2 pods · 2 nodes)

Lives in kube-system, not prod. Kubernetes infrastructure namespace. Fluent Bit runs on EVERY node — one pod per node. Not 6 pods, not 3 pods. Exactly 1 per node, always. When node-02 joined, Fluent Bit was placed there automatically.

Why DaemonSet, not Deployment

Log collection must happen at the node level — it reads from /var/log/containers/ on the host filesystem. You can't do that with a Deployment because the scheduler might put both pods on one node, leaving the other node blind. DaemonSet guarantees coverage.

Without Fluent Bit DaemonSet

Pods on node-02 generate logs. Nobody reads them. When the pod restarts, those logs are gone forever. No logs in OpenSearch, no alarms in CloudWatch. Silent failures in production.

DaemonSets are NOT supported on AWS Fargate. You must use EC2 node groups if you need DaemonSets (Fluent Bit, node-exporter, VPC CNI, etc.).
$ kubectl apply -f fluent-bit-ds.yaml
daemonset.apps/fluent-bit created

$ kubectl get pods -n kube-system -l app=fluent-bit -o wide
NAME NODE STATUS
fluent-bit-n1ds node-01 Running ← auto-placed
fluent-bit-n2ds node-02 Running ← auto-placed when node-02 joined

# Add a 3rd node? DaemonSet places fluent-bit there automatically.
# Remove a node? Its DaemonSet pod is deleted automatically.
DaemonSet: 1 pod per node, automatically
node-01 fluent-bit (DaemonSet) app-pod-1 app-pod-2 reads /var/log/containers node-02 fluent-bit (DaemonSet) app-pod-3 mysql-1 node-03 (new) fluent-bit ← auto-created when node joins, DaemonSet places pod here automatically