Chapter 6.2: Container Orchestration (Kubernetes)
Part 1: The "Why" - What is Orchestration?
In the last chapter, we learned how to use Docker. Docker is a revolutionary tool for building, shipping, and running a single container. With docker-compose, we can even run a few related containers on a single machine. This is perfect for local development.
But what happens in **production**?
What happens when your website gets 10 million users and you need to run **500 copies** of your web server container?
What happens if one of those 500 containers crashes at 3 AM?
How do you update all 500 containers to a new version with zero downtime?
This is the problem of **Container Orchestration**. You cannot manually ssh into 50 servers and run docker run .... You need a "manager" or "orchestrator" to do it for you.
What is Kubernetes (K8s)?
Kubernetes** (also known as **K8s** - "K" + 8 letters + "s") is the undisputed king of container orchestration. It is an open-source platform originally developed by Google (based on their internal "Borg" system) that *automates* the deployment, scaling, and management of containerized applications.
Kubernetes is your "operating system for the cloud." You give it your "desired state," and it works 24/7 to make it a reality.
- You tell K8s: "I want 5 copies of my
my-web-app:v1image running at all times." - K8s makes it happen: It finds 5 servers with free space and starts the 5 containers.
- A server crashes: K8s detects this. It sees only 4 containers are running. It automatically finds a new, healthy server and starts the 5th container there.
- You release
v2: You tell K8s, "Update this app tomy-web-app:v2." K8s performs a **rolling update**: it carefully starts 1 container of v2, waits for it to be healthy, then stops 1 container of v1. It repeats this until all 5 containers are safely updated, all with zero downtime for your users.
This is the power of orchestration. It's a self-healing, auto-scaling system for your applications.
Part 2: Kubernetes Architecture (The 10,000-foot View)
A Kubernetes setup is called a **Cluster**. A cluster is a group of servers (called **Nodes**) that are all working together. The cluster is split into two parts: the "brain" and the "muscle."
1. The Control Plane (The "Brain")
This is the "master" server (or servers) that manages the entire cluster. It makes all the decisions. It contains four key components:
kube-apiserver: This is the **front-door** and *only* way to talk to the cluster. Every command you run (usingkubectl) goes to the API server. It's the "waiter" (API) for the whole cluster.etcd:** This is the **database/memory** of the cluster. It's a simple, reliable key-value store. *All* configuration and state (e.g., "we should have 5 containers") is stored inetcd. If this database corrupts, your entire cluster is dead.kube-scheduler:** This is the "placer." It watches for new containers that need to be run. Its only job is to decide *which* Worker Node is the best fit for that container (e.g., based on available CPU/RAM). It *decides*, it doesn't *run* it.kube-controller-manager:** This is the "watcher" or "thermostat." It runs "controller loops" that constantly check the *desired state* (frometcd) against the *current state* (what's actually running). If it sees a difference (e.g., "desired=5, current=4"), it takes action to fix it.
2. The Worker Nodes (The "Muscle")
These are the servers (VMs or physical) that do the *actual work* of running your application containers. Each Worker Node has three key components:
kubelet: This is the **"agent"** that runs on every single worker. It's the "manager" of that one node. It talks to thekube-apiserverto get its instructions (e.g., "Scheduler said you need to run this container") and then tells the Container Runtime what to do.Container Runtime:** The tool that *actually* runs the containers. This can be **Docker**, but more commonly it's a lighter-weight runtime like **containerd** (which Docker itself is built on).kube-proxy:** This is the "network manager" for the node. It's a simple network proxy that handles all the complex network rules (like routing traffic from a Service to the correct container).
Part 3: The Kubernetes Object Model (YAML)
How do you tell the kube-apiserver what you want? You don't send an email. You send a **YAML** file.
In Kubernetes, *everything* is an **Object** (or "Resource"). A container, a deployment, a network rule... it's all an object defined in YAML. This is the **"declarative"** model. You write a YAML file that describes your "desired state" and send it to the API server.
Every Kubernetes YAML file has **four** required fields:
# 1. apiVersion: Which K8s API to use (e.g., v1, apps/v1)
apiVersion: v1
# 2. kind: What *kind* of object is this?
kind: Pod
# 3. metadata: Data *about* the object (its name, labels)
metadata:
name: my-first-pod
labels:
app: my-app
# 4. spec: The "desired state" - the *real* configuration
spec:
containers:
# ... (spec for the Pod goes here)
We will now learn the most important kinds of objects.
Part 4: Core Workloads (Running Your App)
These objects are responsible for running your containers.
1. `Pod` (The Smallest Unit)
A **Pod** is the *smallest, most basic deployable unit* in Kubernetes. A Pod is **not** a container. It is a **wrapper around one or more containers**.
A Pod represents a single, co-located "instance" of your app. All containers inside one Pod share the same network (IP address) and storage (Volumes). 99% of the time, your Pod will just have **one container** in it (e.g., your web app).
You almost *never* create a Pod by itself. Why? Because if the Pod crashes, it's dead. K8s won't restart it. Instead, you use a `Deployment` to manage your Pods.
pod.ymlapiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx-container
image: nginx:1.25
ports:
- containerPort: 80
2. `ReplicaSet` (The "Ensurer")
A ReplicaSet's job is simple: to ensure that a specified number of "replicas" (copies) of a Pod are running at all times.
You tell it: replicas: 3. It will create 3 Pods. If you manually delete one, the ReplicaSet's controller will see "desired=3, current=2" and *immediately* create a new one.
You almost *never* create a ReplicaSet by itself either. You use a `Deployment`.
3. `Deployment` (The "Manager" - CRITICAL)
This is the object you will use **99% of the time** to deploy your app. A Deployment is a high-level manager that controls `ReplicaSet`s, which in turn control `Pod`s.
The Deployment adds the one feature ReplicaSets don't have: **rolling updates**.
When you edit a Deployment (e.g., change image: v1 to image: v2), the Deployment performs a controlled rolling update:
- It creates a *new* ReplicaSet for v2.
- It scales up the v2 ReplicaSet (e.g., to 1 Pod).
- It waits for that v2 Pod to be "healthy."
- It scales down the *old* v1 ReplicaSet (e.g., to 2 Pods).
- It repeats this process, one-by-one, until all Pods are v2 and the v1 ReplicaSet is at 0.
This ensures you have zero downtime during an update.
Example: A Full Deployment YAML
deployment.ymlapiVersion: apps/v1 # Note the different apiVersion
kind: Deployment
metadata:
name: my-nginx-deployment
spec:
# 1. We want 3 copies (replicas) of our app
replicas: 3
# 2. The 'selector' tells the Deployment which Pods it "owns"
# This MUST match the Pods' labels
selector:
matchLabels:
app: my-nginx
# 3. This is the 'template' for creating the Pods
template:
metadata:
# 4. The Pods' labels (must match the selector)
labels:
app: my-nginx
spec:
containers:
- name: nginx-container
image: nginx:1.25
ports:
- containerPort: 80
Part 5: Networking (Services & Ingress)
We have a problem. We have 3 Nginx Pods running. But Pods are ephemeral:
1. If a Pod crashes, it gets a **new IP address** when it restarts.
2. If a `Backend` app wants to talk to a `Database` app, which of the 3 `Database` Pod IPs should it talk to?
We need a **stable address** for a group of Pods. This is what a **Service** is.
1. `Service` (The "Internal Load Balancer")
A **Service** is a K8s object that provides a **single, stable IP address** and **DNS name** for a group of Pods (e.g., all Pods with the label app: my-nginx). It acts as an internal load balancer, distributing traffic to all the healthy Pods behind it.
Example: A Service for our Nginx Deployment
service.ymlapiVersion: v1
kind: Service
metadata:
name: my-nginx-service
spec:
# 1. This 'selector' finds the Pods to send traffic to.
# It MUST match the Pods' labels (app: my-nginx)
selector:
app: my-nginx
# 2. 'ports' defines how traffic is routed
ports:
- protocol: TCP
port: 80 # The Service's port (what other Pods connect to)
targetPort: 80 # The container's port (from the Dockerfile)
# 3. 'type' (discussed below)
type: ClusterIP
Now, any *other* Pod in your cluster can simply connect to http://my-nginx-service and Kubernetes will automatically route their request to one of the 3 healthy Nginx Pods.
Service Types (How to Expose Your App)
ClusterIP(Default): Exposes the Service *only* on an internal IP inside the cluster. This is perfect for databases or backend APIs that should *not* be open to the internet.NodePort: Exposes the Service on a static port (e.g., 30080) on *every single Worker Node*. This is good for testing, as you can go tohttp://[Any_Node_IP]:30080.LoadBalancer(The Cloud Way): This is the one you use in production. When you set this type, Kubernetes *automatically* tells your cloud provider (AWS, GCP) to provision a **real, external Load Balancer** (like an AWS ELB). This gives you a *public IP address* for your app, which you can point your domain (codewithmsmaxpro.me) to.
2. `Ingress` (The "Smart Router")
A LoadBalancer service is expensive (you pay for a new public IP for *every* app). What if you have 10 apps on your cluster? You don't want 10 Load Balancers.
An **Ingress** is a Layer 7 "smart router" that sits in front of *all* your services. You create **one** LoadBalancer for your Ingress, and then the Ingress routes traffic to the correct *internal* ClusterIP service based on the URL.
Example: An Ingress for Two Apps
ingress.ymlapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
spec:
rules:
# Rule 1: Send 'codewithmsmaxpro.me' to the blog service
- host: "codewithmsmaxpro.me"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-blog-service
port:
number: 80
# Rule 2: Send 'api.codewithmsmaxpro.me' to the API service
- host: "api.codewithmsmaxpro.me"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-api-service
port:
number: 8080
Part 6: ConfigMaps, Secrets, and Storage
`ConfigMap` (For Non-Secret Data)
You should *never* hard-code configuration (like a DATABASE_URL) into your Docker image. To change it, you'd have to rebuild the entire image.
A **ConfigMap** is a K8s object that stores non-sensitive configuration data as key-value pairs. You can then "inject" this data into your Pods as environment variables or files.
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
data:
DATABASE_URL: "postgres://user@db-service"
LOG_LEVEL: "DEBUG"
`Secret` (For Sensitive Data)
A **Secret** is *exactly* like a ConfigMap, but it's used for sensitive data (API keys, database passwords). The data is stored in **Base64** encoding (which is *not* encryption, just obsfucation). Its main power is that you can apply stricter access control (RBAC) to it.
# Create a secret from the command line (safer)
$ kubectl create secret generic my-db-secret \
--from-literal=DB_PASSWORD='super-secret-pass-123'
How to Use ConfigMaps/Secrets in a Pod:
deployment.yml (Updated)apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api-deployment
spec:
... (replicas, selector) ...
template:
... (metadata) ...
spec:
containers:
- name: api-container
image: my-api:v1
# Inject the data as Environment Variables
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: my-app-config # Name of the ConfigMap
key: LOG_LEVEL
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: my-db-secret # Name of the Secret
key: DB_PASSWORD
`PersistentVolume` (Persistent Storage)
Just like Docker, a Pod's filesystem is **ephemeral**. If a Pod (like your database) crashes and restarts, all its data is GONE.
To save data permanently, we use **PersistentVolumes**. The concept is a bit complex:
- PersistentVolume (PV): The "storage" itself. This is the actual physical (or cloud) disk (e.g., an AWS EBS Volume). A cluster admin usually creates this.
- PersistentVolumeClaim (PVC):** The "request" for storage. A developer's Pod *claims* (requests) storage.
- StorageClass:** The "automatic" way. You just ask for "10GB of fast-ssd," and the StorageClass automatically provisions a PV for you.
Example: A Pod (Database) requesting storage
db-deployment.yml# 1. Create the "request" (PVC)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-db-data-claim
spec:
accessModes:
- ReadWriteOnce # Can be mounted by one Pod at a time
resources:
requests:
storage: 5Gi # I need 5 Gigabytes of space
storageClassName: "standard-ssd" (Tells the cloud provider to make an SSD)
--- # (This just separates YAML files)
# 2. Create the Deployment that *uses* the claim
apiVersion: apps/v1
kind: Deployment
...
spec:
template:
...
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
# 3. Mount the volume into the container
volumeMounts:
- name: db-storage
mountPath: /var/lib/postgresql/data # Postgres's data path
# 4. Tell the Pod *which* PVC to use for "db-storage"
volumes:
- name: db-storage
persistentVolumeClaim:
claimName: my-db-data-claim
Part 7: Practical Management (`kubectl`)
kubectl is the **command-line tool** for talking to your Kubernetes cluster's API server. This is your most important tool, similar to the git or docker command.
The 4 Core Verbs: `apply`, `get`, `describe`, `delete`
kubectl apply -f [FILENAME]
This is the **#1 command**. It takes a YAML file and tells the API server: "Make the world look like this file." It is used to *create* and *update* objects.
# Create or update all objects defined in my deployment.yml file
$ kubectl apply -f deployment.yml
deployment.apps/my-nginx-deployment created
kubectl get [OBJECT]
This checks the *status* of your objects.
# Get all pods
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx-deployment-7f5d7d5f4f-abcde 1/1 Running 0 5m
my-nginx-deployment-7f5d7d5f4f-ghijk 1/1 Running 0 5m
my-nginx-deployment-7f5d7d5f4f-xyz12 1/1 Running 0 5m
# Get all services and deployments
$ kubectl get service,deployment
kubectl describe [OBJECT] [NAME]
This is your **#1 debugging tool**. If your Pod is *not* Running (e.g., it says CrashLoopBackOff or Pending), you use describe to find out *why*.
# Find out why my pod is broken
$ kubectl describe pod my-nginx-deployment-7f5d7d5f4f-abcde
Name: my-nginx-deployment-7f5d7d5f4f-abcde
Namespace: default
Status: Pending
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m default-scheduler 0/3 nodes are available: 3 Insufficient cpu.
The "Events" section at the bottom tells you the problem: "Insufficient cpu" (your servers are full).
kubectl delete -f [FILENAME]
Deletes all objects defined in the YAML file.
$ kubectl delete -f deployment.yml
deployment.apps "my-nginx-deployment" deleted
Other Essential `kubectl` Commands
# Show logs from a pod (like 'docker logs')
$ kubectl logs my-nginx-deployment-7f5d7d5f4f-abcde
# Follow the logs in real-time
$ kubectl logs -f my-nginx-deployment-7f5d7d5f4f-abcde
# Get a shell *inside* a running container (like 'docker exec')
$ kubectl exec -it my-nginx-deployment-7f5d7d5f4f-abcde -- /bin/bash
Part 8: Local Development (Minikube)
How do you learn all this without paying for an AWS cluster? You run Kubernetes on your laptop using **Minikube**.
Minikube is a tool that spins up a *single-node* Kubernetes cluster inside a VM or Docker container on your local machine. It's a fully functional, real K8s cluster for development and testing.
How to Use Minikube
- **Install:** Follow the official Minikube installation guide for your OS.
- Start your cluster:**
# This will download the K8s components and start the cluster $ minikube start - Point `kubectl` to it:**
$ minikube kubectl -- config use-context minikube - You're ready!** You can now use all the
kubectlcommands.$ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready control-plane 5m v1.28.3 - Stop the cluster:**
$ minikube stop