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.
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.
Kubernetes control plane is running at
https://api.k8s.local:6443
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.
etcd_db_total_size_in_bytes 8.3MB
# On EKS: you cannot access etcd directly — AWS manages it
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.
Events:
Normal Scheduled 0s default-scheduler
"Successfully assigned prod/webapp-v2-bbb to node-01"
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.
pod deleted
ReplicaSet: desired=3, actual=2 → creating replacement
webapp-v2-zzz ContainerCreating ← controller acted in <1s
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.
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
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.
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)
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.
Container running + readiness probe passing. kube-proxy has registered this pod as a healthy backend for any matching Service.
pod/nginx created
$ kubectl get pod nginx -n prod
nginx 1/1 Running 0 30s
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.
webapp-v2-zzz 0/1 ContainerCreating 0 2s
# kube-proxy skips this pod — not ready yet
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.
mysql-0 1/1 Running 0 node-01
mysql-1 1/1 Running 0 node-02
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.
# 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!
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.
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
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.
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.
# 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!
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.
# 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.
--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.
--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.
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.