Beginner · Kubernetes · Cloud Native

Kubernetes for Beginners ⎈

You know Docker. Now learn to orchestrate hundreds of containers across a cluster — with self-healing, auto-scaling, and zero-downtime deployments built in.

⭐ ⭐ ⭐ ⭐ ⭐
📚 Your Progress
0%
Lesson 1 of 9

⎈ Why Kubernetes?

You can run Docker containers on a single machine. But what happens when that machine isn't enough — or when it crashes at 3am? That's the problem Kubernetes was built to solve.

📋 Prerequisites: This course assumes you've completed the Docker beginner and intermediate courses. You should be comfortable with Docker images, containers, Compose, health checks, and resource limits. If not, start there.
🤔 What Docker Compose Can't Do

Docker Compose is excellent for running multi-container apps on one machine. But production systems have requirements it simply can't meet:

  • The machine crashes. All containers die. Compose has no way to restart them on another machine. Your app is down until someone manually intervenes.
  • Traffic spikes. You need 20 copies of your web server instead of 2. Compose can't automatically add more based on CPU load.
  • You need to update the app. Compose restarts containers, causing a brief outage. In production, zero downtime is non-negotiable.
  • You have 50 microservices. Managing 50 individual Compose files across 10 machines by hand is chaos.

Kubernetes solves every one of these problems — automatically, declaratively, and at any scale.

🍎 Real World Analogy

Imagine you run a restaurant chain. Docker Compose is like managing a single restaurant — you hire staff, set up the kitchen, and run service. It works great for one location.

Kubernetes is like being the corporate headquarters managing 50 locations simultaneously. You declare the standard: "Every location must have 3 chefs, 2 cashiers, and 1 manager on duty." Corporate monitors all 50 locations. If a chef quits (container crashes), headquarters automatically hires a replacement (reschedules the container). If a location burns down (node fails), the staff is redeployed to another location. You never have to manually intervene — the system self-heals.

⎈ What is Kubernetes?

Kubernetes (often written as K8s — 8 letters between K and s) is an open-source container orchestration platform originally built by Google and donated to the Cloud Native Computing Foundation (CNCF) in 2014. Google had been running containers at scale internally for a decade before that, using a system called Borg. Kubernetes is essentially a public version of those learnings.

K8s manages a cluster — a group of machines (physical or virtual) that it treats as a single pool of compute resources. You tell Kubernetes what you want (declarative), and it figures out how to make it happen (reconciliation loop). If reality ever drifts from what you declared, Kubernetes corrects it automatically.

💊

Self-Healing

Crashed containers are restarted automatically. Failed nodes are detected and their workloads rescheduled elsewhere.

📈

Auto-Scaling

Scale from 2 to 200 replicas based on CPU, memory, or custom metrics. Scale back down when traffic drops.

🔄

Rolling Updates

Deploy new versions with zero downtime. Automatically roll back if health checks fail.

📦

Bin Packing

Automatically schedules containers onto nodes to maximize resource utilization.

🛠️ Setting Up a Local Kubernetes Cluster

Before writing any K8s configuration, you need a cluster to practice on. For local development, the two best options are:

  • minikube — The official local K8s tool. Runs a single-node cluster inside a VM or Docker container. Best for learning. Install: brew install minikube (Mac) or download from minikube.sigs.k8s.io.
  • k3s — A lightweight K8s distribution (under 70MB) designed for resource-constrained environments and edge deployments. Great for a Raspberry Pi cluster or a cheap VPS.
  • Docker Desktop — If you already have Docker Desktop installed, there's a built-in K8s option in Settings → Kubernetes → Enable Kubernetes. One checkbox and you have a cluster.
Terminal — Getting Your First Cluster Running
# Start a local minikube cluster (uses Docker as the driver)
minikube start --driver=docker

# Verify the cluster is running and kubectl is connected
kubectl cluster-info

# See the single node in your local cluster
kubectl get nodes

# Output:
# NAME       STATUS   ROLES           AGE   VERSION
# minikube   Ready    control-plane   1m    v1.29.0

# kubectl is the command-line tool for talking to any K8s cluster.
# It's to Kubernetes what docker is to Docker.
# Every command follows the pattern:
#   kubectl <verb> <resource-type> [name]
#   kubectl get pods
#   kubectl describe deployment web
#   kubectl delete pod my-pod-abc123
📖
The Declarative Model Kubernetes is fundamentally declarative. You write YAML files that describe the desired state of your system — "I want 3 replicas of this web server running." You apply the file, and Kubernetes figures out the steps to get there. If a replica crashes, K8s notices the actual state (2 replicas) doesn't match the desired state (3) and creates a new one. This reconciliation loop runs constantly, keeping actual state in sync with desired state.
🧠 Quick Check!
What does "declarative" mean in the context of Kubernetes?
🧠 Quick Check!
You declare "I want 5 replicas of my app." Two crash. What does Kubernetes do?
Lesson 2 of 9

🏛️ Cluster Architecture

A Kubernetes cluster has two types of machines with completely different jobs. Understanding who does what is essential before you write a single line of YAML.

🗺️ The Two Kinds of Nodes

A node is just a machine — a physical server or a virtual machine — that Kubernetes manages. Every cluster has two roles:

  • Control Plane (formerly "master") — The brain. Makes all cluster-wide decisions. Runs the K8s management components. Your YAML goes here. You never run application workloads on the control plane in production.
  • Worker Nodes — The muscle. The machines where your actual application containers run. A production cluster has multiple worker nodes — typically 3 to hundreds, depending on scale.
┌─────────────────────────────────────────────────────────────┐ │ KUBERNETES CLUSTER │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ CONTROL PLANE NODE │ │ │ │ │ │ │ │ kube-apiserver etcd scheduler controller-mgr │ │ │ │ (front door) (DB) (assigns) (reconciles) │ │ │ └───────────────────────┬─────────────────────────────┘ │ │ │ kubectl talks to apiserver │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ WORKER NODE │ │ WORKER NODE │ │ WORKER NODE │ │ │ │ │ │ │ │ │ │ │ │ kubelet │ │ kubelet │ │ kubelet │ │ │ │ kube-proxy │ │ kube-proxy │ │ kube-proxy │ │ │ │ container rt. │ │ container rt. │ │ container rt. │ │ │ │ │ │ │ │ │ │ │ │ [pod] [pod] │ │ [pod] [pod] │ │ [pod] │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────┘
🧠 Control Plane Components

The control plane runs four key processes — you don't manage them directly, but knowing what they do explains how K8s works:

  • kube-apiserver — The single front door to the entire cluster. Every kubectl command, every internal component, every external tool communicates through the API server. It validates and stores requests.
  • etcd — A distributed key-value database. This is where the entire cluster state lives — every node, pod, deployment, secret, and config. It's the single source of truth. Backing up etcd is backing up your entire cluster.
  • kube-scheduler — When a new Pod needs to run, the scheduler decides which worker node it runs on. It evaluates resource requirements, node capacity, affinity rules, and dozens of other factors.
  • controller-manager — Runs a collection of controllers, each responsible for one type of resource. The ReplicaSet controller watches for dead pods and creates new ones. The Node controller watches for failed nodes. Controllers are the engines of self-healing.
💪 Worker Node Components

Each worker node runs three core processes:

  • kubelet — The agent that runs on every worker node. It receives instructions from the API server ("run this Pod") and tells the container runtime to start the containers. It also reports back health status for every Pod on that node.
  • kube-proxy — Manages the network rules on each node that allow traffic to reach Pods. It implements Kubernetes Services (covered in Lesson 5).
  • Container Runtime — The actual software that runs containers. In modern K8s this is containerd (the same runtime Docker uses under the hood). K8s doesn't use Docker directly — it uses the container runtime interface (CRI).
🗣️
How kubectl and K8s Communicate When you type kubectl get pods, here's the exact chain: kubectl reads your kubeconfig file (~/.kube/config) to find the cluster's API server address and your credentials → sends an authenticated HTTPS request to the kube-apiserver → apiserver queries etcd for the current pod list → returns the response → kubectl formats and prints it. Every single K8s operation goes through this chain.
Terminal — Exploring Your Cluster
# List all nodes in the cluster with details
kubectl get nodes -o wide

# Describe a specific node (see its capacity, conditions, pods running on it)
kubectl describe node minikube

# See ALL resources running across ALL namespaces
kubectl get all --all-namespaces

# The control plane components run as pods in the kube-system namespace
kubectl get pods -n kube-system

# Output includes:
# etcd-minikube                  ← the database
# kube-apiserver-minikube        ← the front door
# kube-controller-manager-...   ← the reconciler
# kube-scheduler-minikube        ← the placer
# coredns-...                    ← internal DNS for service discovery
# kube-proxy-...                 ← network rules agent
🧠 Quick Check!
Which control plane component stores ALL cluster state — every pod, node, secret, and config — and is the source of truth for the entire cluster?
🧠 Quick Check!
Which component on each worker node actually starts and stops containers by talking to the container runtime?
Lesson 3 of 9

📦 Pods — The Smallest Unit

In Docker you ran containers directly. In Kubernetes, you never deploy bare containers. Everything runs inside a Pod — K8s's fundamental unit of deployment.

🤔 What is a Pod?

A Pod is a wrapper around one or more containers that are tightly coupled and need to share resources. Every container in a Pod:

  • Shares the same network namespace — they all have the same IP address and can reach each other via localhost
  • Shares the same storage volumes — they can read and write to the same mounted volumes
  • Are always scheduled together on the same node — they live and die as a unit

The vast majority of Pods contain exactly one container. Multi-container Pods are used for specific patterns (sidecar, ambassador, adapter) where containers genuinely need to be colocated — for example, a log collector sidecar that ships logs from the main app container to a central aggregator.

🍎 Real World Analogy

If a container is a single person, a Pod is an apartment. Roommates (containers) in the same apartment share the same front door (IP address), share the same Wi-Fi (network), and have access to the same communal spaces (shared volumes). The building manager (Kubernetes) assigns apartments to floors (nodes) in the building (cluster). You can't move one roommate to a different floor without moving the whole apartment — Pods are atomic.

📄 Your First Pod YAML

Kubernetes resources are defined in YAML files. Every K8s YAML has four required top-level fields:

  • apiVersion — Which version of the K8s API this resource uses
  • kind — What type of resource this is (Pod, Deployment, Service, etc.)
  • metadata — Name, namespace, labels, and annotations
  • spec — The desired configuration of the resource
pod.yaml — A Simple Web Server Pod
apiVersion: v1
kind: Pod
metadata:
  name: my-web-pod
  labels:
    app: web          # Labels are key-value pairs. Services use them to find Pods.
    env: production
spec:
  containers:
    - name: web-server
      image: nginx:1.25-alpine    # Pin to exact version in production!
      ports:
        - containerPort: 80      # Informational only — doesn't expose the port
      resources:
        requests:               # Minimum guaranteed resources (for scheduling)
          memory: "64Mi"
          cpu: "100m"             # 100 millicores = 0.1 CPU
        limits:                  # Maximum allowed (container killed if exceeded)
          memory: "128Mi"
          cpu: "200m"
      livenessProbe:             # K8s restarts the container if this fails
        httpGet:
          path: /
          port: 80
        initialDelaySeconds: 10
        periodSeconds: 15
      readinessProbe:            # K8s only sends traffic when this passes
        httpGet:
          path: /
          port: 80
        initialDelaySeconds: 5
        periodSeconds: 10
❤️‍🔥 Liveness vs Readiness Probes

Kubernetes has two probe types that serve distinct purposes — understanding the difference is important:

  • Liveness Probe — "Is this container still alive?" If this fails, K8s kills and restarts the container. Use it to detect deadlocks or hung processes that need to be force-recycled.
  • Readiness Probe — "Is this container ready to receive traffic?" If this fails, K8s removes the Pod from all Service load balancers but does not restart it. Use it during startup (while the app is loading) or when temporarily overloaded.

A Pod can be alive (liveness passes) but not ready (readiness fails) — for example, a web server that's running but still loading its cache. Traffic is held back until it's ready.

Terminal — Working With Pods
# Apply the YAML file — create or update the resource
kubectl apply -f pod.yaml

# List all pods in the current namespace
kubectl get pods

# List pods with extra info (node, IP, status)
kubectl get pods -o wide

# Watch pod status in real time (great during deployment)
kubectl get pods -w

# Full details: events, probe results, resource usage, restart count
kubectl describe pod my-web-pod

# Stream logs from the container in the pod
kubectl logs -f my-web-pod

# Get a shell inside the pod (like docker exec -it)
kubectl exec -it my-web-pod -- sh

# Delete the pod
kubectl delete pod my-web-pod

# Quick tip: generate YAML without applying it (great for learning)
kubectl run test-pod --image=nginx --dry-run=client -o yaml
⚠️
You Almost Never Create Bare Pods in Production
If you create a Pod directly and the node it's running on fails, the Pod is gone forever — K8s won't reschedule it because nobody declared a desired count to maintain. In production, you always use a Deployment (next lesson), which manages Pods through a ReplicaSet and guarantees the desired count is always maintained. Bare Pods are for debugging and learning only.
🧠 Quick Check!
Your app is running but still warming up its caches and isn't ready to serve production traffic yet. Which probe type should fail during this window?
Lesson 4 of 9

🔁 Deployments — Managing Pods at Scale

Deployments are the workhorse of Kubernetes. They manage the full lifecycle of your Pods — creating them, scaling them, updating them with zero downtime, and rolling back when things go wrong.

🧱 The Deployment → ReplicaSet → Pod Hierarchy

When you create a Deployment, K8s actually creates a chain of resources:

  1. The Deployment defines the desired state: which image, how many replicas, how to update.
  2. The Deployment creates a ReplicaSet — a controller that ensures a specific number of identical Pods are always running.
  3. The ReplicaSet creates and manages the actual Pods.

When you do a rolling update, the Deployment creates a new ReplicaSet with the new image and gradually scales it up while scaling the old ReplicaSet down. This is how zero-downtime deploys work.

Deployment (desired state: 3 replicas of v1.0) │ ├── ReplicaSet v1 (manages 3 pods running v1.0) │ ├── Pod: web-abc11 [nginx:1.0] Running │ ├── Pod: web-abc12 [nginx:1.0] Running │ └── Pod: web-abc13 [nginx:1.0] Running After kubectl set image deployment/web nginx=nginx:1.1: Deployment (desired state: 3 replicas of v1.1) │ ├── ReplicaSet v1 (scaling DOWN) │ └── Pod: web-abc11 [nginx:1.0] Terminating │ └── ReplicaSet v2 (scaling UP) ├── Pod: web-def21 [nginx:1.1] Running ├── Pod: web-def22 [nginx:1.1] Running └── Pod: web-def23 [nginx:1.1] Running
deployment.yaml — A Production-Ready Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  replicas: 3                    # Run 3 identical Pods at all times
  selector:
    matchLabels:
      app: web                   # This Deployment manages Pods with label app=web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1               # Allow 1 extra Pod above the desired count during update
      maxUnavailable: 0         # Never reduce below desired count (zero-downtime!)
  template:                      # Pod template — this IS the Pod spec
    metadata:
      labels:
        app: web                 # Must match selector.matchLabels above!
    spec:
      containers:
        - name: web
          image: myapp:v1.2.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 15
Terminal — Managing Deployments
# Apply the deployment
kubectl apply -f deployment.yaml

# Check the rollout status in real time
kubectl rollout status deployment/web-deployment

# Scale to 10 replicas instantly
kubectl scale deployment web-deployment --replicas=10

# Update the container image (triggers a rolling update)
kubectl set image deployment/web-deployment web=myapp:v1.3.0

# Watch the rolling update happen in real time
kubectl get pods -w

# Oops — v1.3.0 is broken! Instantly roll back to the previous version
kubectl rollout undo deployment/web-deployment

# Roll back to a specific revision
kubectl rollout undo deployment/web-deployment --to-revision=2

# See rollout history (why SHA tags matter — you can trace each revision)
kubectl rollout history deployment/web-deployment

# Pause a rolling update mid-way (to canary test the new version)
kubectl rollout pause deployment/web-deployment
kubectl rollout resume deployment/web-deployment
🧠 Quick Check!
You set maxUnavailable: 0 in your rolling update strategy. What does this guarantee?
Lesson 5 of 9

🌐 Services — Stable Networking for Pods

Pods are mortal. Their IP addresses change every time they're restarted or rescheduled. A Service gives your application a permanent, stable address regardless of which Pods are backing it.

🤔 The Problem: Pod IPs Are Ephemeral

Every Pod gets its own IP address when it starts. But Pods are constantly being created and destroyed — crashes, updates, scaling events. If your frontend hardcodes the IP of a backend Pod, it breaks every time that Pod is rescheduled.

A Service is a stable virtual IP (called a ClusterIP) and DNS name that sits in front of a set of Pods. It uses label selectors to find its target Pods dynamically. Pods come and go — the Service IP never changes.

🗂️ The Four Service Types
TypeAccessible FromUse Case
ClusterIPInside the cluster onlyBackend-to-backend communication. The default and most common type.
NodePortOutside the cluster via <NodeIP>:<Port>Development/testing. Exposes a port on every node. Not for production.
LoadBalancerOutside the cluster via a cloud load balancerProduction external traffic. Provisions a real cloud LB (AWS ELB, GCP LB, etc.).
ExternalNameMaps a Service to an external DNS nameAccessing external services (databases, APIs) by a stable internal name.
service.yaml — ClusterIP and LoadBalancer Examples
# ── ClusterIP Service (internal only) ─────────────────────────
apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  type: ClusterIP           # Default — can be omitted
  selector:
    app: web               # Routes traffic to all Pods with label app=web
  ports:
    - protocol: TCP
      port: 80             # Port the Service listens on inside the cluster
      targetPort: 8080    # Port the Pods actually listen on

---
# ── LoadBalancer Service (external access via cloud LB) ────────
apiVersion: v1
kind: Service
metadata:
  name: web-loadbalancer
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
# On AWS/GCP/Azure, this creates a real cloud load balancer.
# kubectl get service web-loadbalancer will show the EXTERNAL-IP once provisioned.
🔍 Service Discovery via DNS

Every Service in Kubernetes automatically gets a DNS name inside the cluster, managed by CoreDNS. Any Pod can reach any Service using a predictable hostname — no hardcoded IPs, no service registries to configure manually.

The full DNS format is: <service-name>.<namespace>.svc.cluster.local

Within the same namespace, you can just use the service name: web-service. This is exactly how Docker Compose's service name DNS works — K8s just does it cluster-wide.

Service Discovery in Action frontend Pod wants to talk to the database Service Connection string in app code: postgresql://postgres-service:5432/mydb ↑ just the Service name — no IP needed CoreDNS resolves: postgres-service10.96.45.12 (stable ClusterIP) kube-proxy routes: 10.96.45.12:5432 → one of the healthy database Pods If a database Pod crashes and a new one starts with a new IP: Service ClusterIP stays the same → app keeps working ✓
Terminal — Working With Services
# Apply service definition
kubectl apply -f service.yaml

# List all services (note the CLUSTER-IP and EXTERNAL-IP columns)
kubectl get services

# Describe a service — shows which Pods it's routing to (Endpoints)
kubectl describe service web-service

# Test a ClusterIP service from inside the cluster
# (run a temporary debug pod, curl the service by name)
kubectl run debug --image=curlimages/curl -it --rm -- sh
# Inside the debug pod:
curl http://web-service/       # ← uses DNS to resolve the service

# In minikube, access a NodePort or LoadBalancer service from your laptop:
minikube service web-loadbalancer --url
🧠 Quick Check!
Your backend app has 5 Pods. One crashes and K8s starts a new Pod with a different IP. How does the frontend app keep connecting to the backend without any configuration changes?
Lesson 6 of 9

⚙️ ConfigMaps & Secrets

Configuration and secrets should never be baked into your container images. Kubernetes provides two dedicated resource types to inject them at runtime — one for plain config, one for sensitive data.

📋 ConfigMaps — Non-Sensitive Configuration

A ConfigMap stores arbitrary key-value configuration data decoupled from the Pod spec. This lets you change configuration without rebuilding your image. Common uses: feature flags, database hostnames, log levels, API endpoints, config file contents.

You can inject ConfigMap data into Pods in two ways:

  • Environment variables — each key becomes an env var in the container
  • Mounted files — the ConfigMap is mounted as a directory, each key becomes a file
configmap.yaml — Configuration as Env Vars and Files
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  DATABASE_HOST: "postgres-service"
  # Multi-line value becomes a file when volume-mounted
  nginx.conf: |
    server {
      listen 80;
      location / { proxy_pass http://web-service; }
    }

---
# Using the ConfigMap in a Pod / Deployment spec:
spec:
  containers:
    - name: app
      image: myapp:v1
      envFrom:
        - configMapRef:             # Inject ALL keys as env vars
            name: app-config
      volumeMounts:
        - name: config-vol
          mountPath: /etc/nginx/conf.d  # nginx.conf key → file here
  volumes:
    - name: config-vol
      configMap:
        name: app-config
🔐 Secrets — Sensitive Data

A Secret is structurally identical to a ConfigMap but is designed for sensitive data: passwords, API tokens, TLS certificates, SSH keys. Kubernetes handles Secrets with additional protections:

  • Stored base64-encoded in etcd (not plaintext — though base64 is not encryption)
  • Only sent to nodes that have Pods needing them
  • Mounted as tmpfs (in-memory) inside Pods — never written to node disk
  • Can be encrypted at rest in etcd when encryption providers are configured (highly recommended in production)
⚠️
Base64 ≠ Encryption
Kubernetes Secrets are base64-encoded, not encrypted, by default. Anyone with access to etcd or sufficient RBAC permissions can decode them. For real secrets management in production, integrate with an external system: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or the External Secrets Operator (a K8s operator that syncs secrets from external providers).
Terminal + YAML — Creating and Using Secrets
# Create a Secret imperatively (the --from-literal values are base64-encoded automatically)
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=supersecret123

# Create from a file (e.g., a TLS certificate)
kubectl create secret tls my-tls-secret \
  --cert=server.crt \
  --key=server.key

# Using a Secret in a Pod spec (as env var and as mounted file):
#
# spec:
#   containers:
#     - name: app
#       env:
#         - name: DB_PASSWORD
#           valueFrom:
#             secretKeyRef:
#               name: db-credentials
#               key: password
#       volumeMounts:
#         - name: secret-vol
#           mountPath: /run/secrets
#           readOnly: true
#   volumes:
#     - name: secret-vol
#       secret:
#         secretName: db-credentials
#         # Mounts as files: /run/secrets/username, /run/secrets/password

# List secrets (values are hidden)
kubectl get secrets

# Decode a secret value to verify it (be careful where you run this!)
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decode
🧠 Quick Check!
What is the key difference between a ConfigMap and a Secret in Kubernetes?
Lesson 7 of 9

💾 Persistent Storage

Pods are ephemeral — their storage disappears when they do. Kubernetes has a powerful storage system with three layers of abstraction that separates what developers need from how admins provision it.

🗂️ Three Storage Concepts

Docker had volumes. Kubernetes has three related but distinct concepts:

  • PersistentVolume (PV) — A piece of actual storage provisioned by a cluster admin. Could be an NFS share, an AWS EBS volume, a GCP Persistent Disk, or a local disk. It exists independently of any Pod.
  • PersistentVolumeClaim (PVC) — A request for storage made by a developer. "I need 10GB of ReadWriteOnce storage." Kubernetes finds a matching PV and binds them together. Developers don't need to know the underlying storage details.
  • StorageClass — Enables dynamic provisioning. Instead of an admin manually creating PVs, a StorageClass defines a provisioner (e.g., AWS EBS, GCP PD) that automatically creates a PV when a PVC requests one. This is how real production clusters work.
Storage Request Flow Developer creates a PVC: "I need 10Gi of ReadWriteOnce storage" │ ▼ K8s finds a matching PV (or StorageClass creates one dynamically) │ ▼ PVC is BOUND to a PV │ ▼ Pod mounts the PVC at a path like /data │ ▼ Pod writes to /dataPod is deleted ──→ PV and data SURVIVENew Pod mounts same PVC ──→ data is still there ✓
pvc.yaml + deployment — Using Persistent Storage
# Step 1: Create a PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce          # Can be mounted by ONE node at a time (good for DBs)
  storageClassName: standard # Which StorageClass to use for dynamic provisioning
  resources:
    requests:
      storage: 10Gi

---
# Step 2: Mount the PVC in a Pod / Deployment
spec:
  containers:
    - name: postgres
      image: postgres:16-alpine
      volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: postgres-storage
      persistentVolumeClaim:
        claimName: postgres-pvc   # Reference the PVC by name
📖 Access Modes

PVCs declare how the storage can be accessed by nodes:

  • ReadWriteOnce (RWO) — Mounted read-write by a single node. Standard for databases (PostgreSQL, MySQL). Most cloud block storage (EBS, GCP PD) supports this.
  • ReadOnlyMany (ROX) — Mounted read-only by many nodes simultaneously. Useful for shared configuration or static assets.
  • ReadWriteMany (RWX) — Mounted read-write by many nodes simultaneously. Requires network filesystems like NFS or cloud file storage (AWS EFS, GCP Filestore). Expensive but needed for shared uploads.
🐘
Stateful Apps in K8s: Use StatefulSets For stateful applications like databases that need stable network identities and ordered deployment, use a StatefulSet instead of a Deployment. StatefulSets give each Pod a stable hostname (postgres-0, postgres-1, postgres-2) and a dedicated PVC per Pod. They're the right tool for running PostgreSQL, MongoDB, Kafka, or Elasticsearch on Kubernetes.
🧠 Quick Check!
A developer creates a PVC requesting 10Gi of storage. They don't know whether the cluster uses AWS EBS, GCP disks, or NFS. Who or what handles the actual provisioning details?
Lesson 8 of 9

🚪 Ingress — Smart HTTP Routing Into the Cluster

LoadBalancer Services give you one cloud load balancer per Service — expensive and unscalable. Ingress lets a single load balancer route to dozens of Services based on hostname and URL path.

🤔 The Problem With LoadBalancer Services

If you have 10 microservices, each with a LoadBalancer Service, you'd pay for 10 cloud load balancers. On AWS, each ALB costs money per hour. For a startup with 20 services that's a significant monthly bill — just for routing.

An Ingress resource defines HTTP/HTTPS routing rules. A single Ingress Controller (typically Nginx or Traefik) runs inside the cluster and is exposed via one LoadBalancer Service. All routing decisions happen inside the cluster — one cloud load balancer, unlimited internal services.

WITHOUT Ingress (expensive): Internet → LB₁ → Service A Internet → LB₂ → Service B (3 cloud LBs = 3× cost) Internet → LB₃ → Service C WITH Ingress (efficient): Internet → 1 LBIngress Controller → routes based on rules: api.example.com/users → Service A api.example.com/products → Service B admin.example.com → Service C
ingress.yaml — Host and Path-Based Routing
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    # Annotations configure the specific Ingress controller's behavior
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    # cert-manager annotation: auto-issue a Let's Encrypt TLS certificate!
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx        # Which Ingress controller handles this
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls-cert    # cert-manager stores the cert here
  rules:
    # Route by hostname
    - host: api.example.com
      http:
        paths:
          # Route by URL path
          - path: /users
            pathType: Prefix
            backend:
              service:
                name: users-service
                port:
                  number: 80
          - path: /products
            pathType: Prefix
            backend:
              service:
                name: products-service
                port:
                  number: 80
🔒 TLS with cert-manager

cert-manager is a Kubernetes controller that automatically provisions and renews TLS certificates from Let's Encrypt (free) or other certificate authorities. Combined with Ingress, your workflow becomes:

  1. Install cert-manager in the cluster (one time, via Helm)
  2. Create a ClusterIssuer resource that points to Let's Encrypt
  3. Add the cert-manager.io/cluster-issuer annotation to your Ingress
  4. cert-manager automatically creates the certificate, stores it as a Secret, and renews it before expiry — forever, automatically

This is how most production Kubernetes clusters handle HTTPS — zero manual certificate management.

Terminal — Installing Nginx Ingress Controller + cert-manager
# Install Helm (the K8s package manager) if you don't have it
brew install helm

# Add the Nginx Ingress Helm repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Install the Nginx Ingress Controller into the cluster
helm install ingress-nginx ingress-nginx/ingress-nginx

# Install cert-manager for automatic TLS certificate management
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# In minikube, enable the built-in Ingress addon instead:
minikube addons enable ingress

# Check that the Ingress controller is running
kubectl get pods -n ingress-nginx

# List all Ingress resources
kubectl get ingress
🧠 Quick Check!
You have 15 microservices. Why is using Ingress better than creating 15 LoadBalancer Services?
Lesson 9 of 9

🏆 You're Cluster-Ready!

You now understand the foundational objects every Kubernetes engineer works with every day. Let's consolidate, give you a real project, and map the path forward.

📋 Complete Recap
  • Kubernetes orchestrates containers across a cluster of machines — self-healing, auto-scaling, zero-downtime deploys.
  • 🏛️ Control Plane (apiserver, etcd, scheduler, controller-manager) is the brain. Worker Nodes (kubelet, kube-proxy, container runtime) are the muscle.
  • 📦 Pods are the smallest deployable unit — a wrapper around one or more containers sharing a network and storage. Never create bare Pods in production.
  • 🔁 Deployments manage Pods via ReplicaSets — declarative scaling, rolling updates, instant rollbacks.
  • 🌐 Services give Pods a stable ClusterIP and DNS name. Four types: ClusterIP, NodePort, LoadBalancer, ExternalName.
  • ⚙️ ConfigMaps inject non-sensitive config. Secrets inject sensitive data. Both keep config out of images.
  • 💾 PVCs request storage. PVs provide it. StorageClasses provision it dynamically.
  • 🚪 Ingress routes HTTP/HTTPS traffic to multiple Services from one load balancer. cert-manager handles TLS automatically.
🎯 Capstone Project: Deploy a Full Stack on K8s

Port Your Docker Compose App to Kubernetes

Take the full-stack app you built in the Docker courses and run it on Kubernetes. In order:

  1. Push your app image to Docker Hub with a version tag.
  2. Write a Deployment for your web app with 3 replicas, liveness and readiness probes, and resource limits.
  3. Write a ClusterIP Service to front the web app Pods.
  4. Write a Deployment for PostgreSQL using the official image.
  5. Write a ClusterIP Service for PostgreSQL so the web app can reach it by name.
  6. Create a PVC for PostgreSQL data and mount it into the database Deployment.
  7. Put your database password in a Secret and your DB hostname in a ConfigMap. Inject them into both Deployments.
  8. Enable minikube's Ingress addon and write an Ingress resource to expose your web app at myapp.local.
  9. Add 127.0.0.1 myapp.local to your /etc/hosts file and visit it in your browser.
  10. Run kubectl rollout undo deployment/web to practice a rollback.
⎈ The CKAD Certification Path

The Certified Kubernetes Application Developer (CKAD) is the right first K8s certification. It's hands-on — a 2-hour practical exam where you solve real tasks in a live cluster using kubectl and YAML. No multiple choice.

Topics on the CKAD exam that go beyond this course (your next study list):

  • Namespaces — logical partitions within a cluster; RBAC is scoped to them
  • Resource Quotas and LimitRanges — enforce resource limits at the namespace level
  • Jobs and CronJobs — run batch tasks or scheduled tasks in Pods
  • Network Policies — firewall rules between Pods (which Pods can talk to which)
  • ServiceAccounts and RBAC — which users and Pods can do what in the cluster
  • Horizontal Pod Autoscaler (HPA) — automatically scale replicas based on CPU/memory metrics
  • Init Containers — containers that run to completion before the main container starts (migrations, cert downloads)
  • Multi-container Pod patterns — sidecar, ambassador, adapter
  • Helm — the package manager for Kubernetes; package and deploy complex apps with one command
CKAD Exam Tips The exam is entirely terminal-based. Speed matters. Master these before exam day:
— Set alias k=kubectl (saves thousands of keystrokes)
— Use kubectl explain pod.spec.containers to look up any field without leaving the terminal
— Use --dry-run=client -o yaml to generate YAML templates instead of writing from scratch
— Know how to use kubectl edit to modify live resources in-place
— Practice on killer.sh — the official CKAD simulator
🏆 Final Mastery Quiz
You have a web Deployment with 5 replicas. You update the image from v1 to v2. Pod v2-abc starts failing its readiness probe. What happens?
🏆 Final Mastery Quiz
Your frontend Pod needs to connect to your backend Service. The backend Service is named "api-service" in the same namespace. What connection string should the frontend use?
Roadmap