Search Tech Journey

Find topics, journeys and posts

6-month learning plan66 / 130
back to blog
systemsintermediate 55m read

S066 · Kubernetes I — Pods, Deployments, Services

Kubernetes without the mysticism — Pods run containers, Deployments keep N of them alive, Services give them a stable IP. Ship a real app to a local cluster in 25 minutes.

⚙️SystemsM07 · Systems & Infrastructure· Session 066 of 130 90 min

🎯 Understand Pod, Deployment, Service, and Ingress; deploy a 2-replica app to kind and hit it through a Service.

Why this session exists

Kubernetes is the operating system of the cloud in 2026 — every major cloud, most fintechs, and half the startups run on it. The reputation for complexity is deserved, but 80% of what you'll do sits on four objects: Pod, Deployment, Service, Ingress. Learn those four crisply and everything else (HPA, ConfigMap, StatefulSet) is decoration.

You will be able to
  • Explain Pod vs Deployment vs Service vs Ingress to a friend in under two minutes.
  • Write valid YAML for a 2-replica web app with a ClusterIP service and read it with kubectl.
  • Deploy to a local kind cluster, kill a pod, and watch the Deployment respawn it.
  • Diagnose the top-3 K8s beginner traps: label mismatches, unbounded resources, and CrashLoopBackOff loops.

Prerequisites

  • S065 · Docker — you can build and run container images.
  • S062 · Load balancers — reverse-proxy + health-check mental model.
  • S060 · Linux fundamentals — processes, network namespaces.


(a) Intuition · 5 min

A hotel with a smart concierge
🌍 Real world

A big hotel has rooms (single occupants), suites (a few people who share a bathroom), a housekeeping supervisor who guarantees ‘this floor always has 20 rooms clean and ready’, and a concierge at the desk who tells guests ‘Alice is in room 412’ without the guest needing to know Alice's room number in advance.

Rooms change. Guests come and go. But the hotel's public interface — call the concierge, get connected — never changes. That's the magic that lets a hotel with 500 rooms present a single phone number.

💻 Code world

A Pod is a suite: 1 or 2 tightly-coupled containers that share network + volumes. A Deployment is the housekeeping supervisor: it says ‘keep 3 pods of this shape alive at all times’, and rolls out new versions gradually. A Service is the concierge: a stable IP and DNS name that routes to whichever pods are healthy right now, even as pods churn.

An Ingress is the hotel's front door — the single public HTTPS entry that fans out to internal Services based on hostname and path.

The four objects that carry 80% of the mental load

Learn these four crisply, everything else is decoration
  • Pod — the smallest deployable unit. 1+ containers sharing a network namespace + volumes. Almost never created directly; you create Deployments that create Pods.
  • Deployment — declares ‘N replicas of this pod template’. Handles rolling updates, rollbacks, and self-healing (kills unhealthy, spawns replacements).
  • Service — a stable virtual IP + DNS name in the cluster. Uses label selectors to find backing pods. Types: ClusterIP (internal), NodePort (a fixed port on every node), LoadBalancer (asks the cloud for an external LB).
  • Ingress — the L7 HTTP router at the cluster edge. Maps hostnames + paths to Services. Requires an Ingress Controller (nginx, Traefik, Istio) doing the actual work.

A quick history so the ecosystem makes sense

  1. 2003
    Google Borg (internal)
    Google's cluster manager runs everything at Google for a decade. Kubernetes is its open-source spiritual successor.
  2. 2014
    Kubernetes 0.1 released · Google
    Joe Beda + Brendan Burns + Craig McLuckie open-source ‘Seven of Nine’ — codenamed Kubernetes.
  3. 2015
    Kubernetes 1.0 · CNCF founded
    K8s donated to the CNCF. Docker Swarm and Apache Mesos are still contenders.
  4. 2017
    K8s wins the orchestrator war
    AWS launches EKS, DockerCon adopts K8s, Mesos fades. K8s is the standard.
  5. 2020
    Dockershim removal announced
    Kubernetes 1.24 (2022) drops Docker as the container runtime; containerd takes over. Docker the CLI still works, kubelet no longer needs it.

(b) Visual walkthrough · 15 min

The object hierarchy — how the four pieces connect

Reading the diagram: the Service is not a proxy in the pod path — it's a virtual IP that kube-proxy installs as iptables/IPVS rules. When you curl app-svc, the packet gets DNAT'd to one of the backing pods. Zero application-level indirection.

The lifecycle of a Deployment rollout

1
kubectl apply -f deploy.yaml

You send desired state (image: myapp:v2) to the API server, which stores it in etcd.

2
Deployment controller sees change

Creates a new ReplicaSet for v2, sets desired replicas gradually (maxSurge/maxUnavailable rules).

3
Scheduler places new pods

Assigns each new pod to a node with capacity + matching affinity/taints.

4
kubelet on node pulls image + starts container

Registers pod status back to the API server.

5
Readiness probe passes

Pod becomes Ready. Service selector picks it up automatically.

6
Old ReplicaSet scales down

One old pod terminated per new pod ready. Old ReplicaSet kept around for instant rollback.

The four Service types you'll actually use

ClusterIP (default)

Internal only

  • Virtual IP reachable only inside the cluster
  • Perfect for microservice-to-microservice
  • kube-dns gives it a DNS name: `svc.namespace.svc.cluster.local`
  • The 95% default
NodePort

Fixed port on every node

  • Opens the same port (30000-32767) on every node in the cluster
  • Cheap way to expose a service without a cloud LB
  • Mostly used for local dev + demos
  • Awkward at scale — you need to know node IPs
LoadBalancer

Ask the cloud for an external LB

  • Cloud controller provisions AWS ELB / Azure LB / GCP TCP LB
  • One external IP per Service — gets expensive fast
  • Good for a small number of top-level entry points
  • In practice you often prefer one Ingress over many LoadBalancer Services
Ingress (not a Service)

L7 router in front of many ClusterIP services

  • Host/path routing, TLS termination, one external IP
  • Requires an Ingress Controller (nginx-ingress, Traefik, Istio)
  • Standard pattern: N ClusterIP services + 1 Ingress + 1 LB
  • The production-grade pattern

YAML anatomy — the shape you'll write a hundred times

Every K8s manifest has these four top-level keys

apiVersion
Which API to use (e.g., apps/v1 for Deployment, v1 for Service). Different objects live in different API groups.
api
kind
The object type: Deployment, Service, Pod, ConfigMap, etc.
type
metadata
name, namespace, labels, annotations. Labels are the glue Selectors use to find related objects.
meta
spec
The desired state. This is what the controller reconciles reality against. For Deployment: replicas, selector, template.
spec

Common misconception
✗ What most people think

"Kubernetes runs my containers. I give it a Deployment and it starts them, like a fancier Docker Compose."

✓ What is actually true

Kubernetes does not run anything imperatively. You declare a desired state, and a set of independent controllers continuously compare desired to actual and act to reduce the difference. Nothing "starts" your pod in a single step — the Deployment controller creates a ReplicaSet, the ReplicaSet controller creates Pods, the scheduler binds them to nodes, and the kubelet on that node asks the container runtime to run them. Each is an independent loop that could run in any order.

Why the myth is so sticky

The myth is sticky because kubectl apply feels imperative — you run a command and pods appear, so cause and effect look direct. It matters the moment something does not happen: with an imperative model your only question is "did the command fail?", whereas the reconciliation model tells you exactly where to look. Some controller is either not running, blocked, or continuously trying and failing, and it will keep trying forever without ever reporting a top-level error.

Prove it to yourself

Watch reconciliation rather than execution — delete a pod and observe that nothing "restarts" it in the imperative sense:

kubectl delete pod my-app-abc123
kubectl get pods -w
# a NEW pod appears with a different name.
# nothing restarted it: the ReplicaSet controller noticed
# actual(2) != desired(3) and created one.

kubectl describe pod my-app-xyz | tail -20
# the Events section is the reconciliation log - read it FIRST
From first principles
Start with the question

Why does Kubernetes schedule Pods rather than containers, and why does a Pod have that shared-network-namespace design?

  1. 1
    Some containers are genuinely co-dependent: a log shipper reading a file the app writes, a proxy handling the app's traffic, an adapter translating its metrics.
    forced by · these helpers exist only to serve one specific application instance and are useless without it
  2. 2
    If such containers were scheduled independently, they could land on different nodes, and their entire purpose — sharing a filesystem or a network endpoint — would be impossible.
    forced by · a local file or a localhost connection cannot cross a machine boundary
  3. 3
    Therefore the scheduling unit must be a group that is guaranteed co-located and scheduled atomically.
    forced by · partial placement of a co-dependent group is never a useful state
  4. 4
    Co-location alone is insufficient: they must also share a network identity, so the proxy can intercept traffic on localhost and the app needs no awareness of it.
    forced by · transparent sidecars require the app to be unable to tell the difference, which means one network namespace
  5. 5
    So the Pod is defined as a set of containers sharing a network namespace and optionally volumes, with a single IP address, scheduled as one unit.
    forced by · one IP per Pod is what makes localhost communication and transparent interception work
⇒ Therefore

Therefore the Pod is not an arbitrary wrapper — it is the smallest unit that can express "these processes must be together", which containers alone cannot express.

And note what this predicts: the entire service mesh pattern becomes possible without modifying application code, because a sidecar proxy in the same network namespace can transparently intercept all traffic. It also predicts the constraint that catches everyone out: containers in a Pod cannot bind the same port, since they share one network namespace. And it explains why a Pod is never "updated" in place — changing the image means a new Pod, because the unit of scheduling is the whole group.

Mental modelA thermostat, not a remote control

You do not press buttons; you set a target. Controllers watch the gap between what you declared and what exists, and act continuously to close it. If you delete something, it comes back. If a node dies, its pods are recreated elsewhere. The system is always converging, never finished.

The API server plus etcd holds the declared and observed state; everything else is a loop reading from it and writing back. There is no orchestrator in the middle — just many small loops agreeing on one database.

  • Every object type has a controller, and diagnosis means finding which loop is stuck. kubectl describe Events is the controller's own account of what it tried and why it failed — read it before logs, before metrics, before anything.
  • Services are stable virtual IPs with label-selector membership; Pods are cattle with changing IPs. Never address a Pod IP directly — the indirection through Services is what makes pods disposable.
  • Probes have distinct jobs and confusing them causes outages: readiness removes a pod from Service endpoints (temporary, no restart), liveness kills and restarts the container (permanent-failure assumption), startup suspends the other two while a slow application boots. A liveness probe pointed at a dependency turns that dependency's slowness into a restart loop across your entire fleet.
  • Requests drive scheduling; limits drive enforcement. Requests are what the scheduler reserves; a CPU limit throttles rather than kills, while a memory limit triggers an OOM kill. Setting requests too high wastes cluster capacity, and setting them too low gets you evicted under pressure.
🔔 Fires when you see

Fire this model when you see: a pod stuck Pending · a CrashLoopBackOff · a deleted resource reappearing · traffic reaching a pod that is not ready · a pod that runs fine locally and gets OOM-killed in the cluster.

The tradeoff

How do you set CPU and memory requests and limits for a service?

Requests equal limits (Guaranteed QoS)
+ you gain the most predictable behaviour available: the pod gets exactly what it reserved, is last to be evicted under node pressure, and its performance does not depend on what its neighbours are doing.
− you pay you pay for peak capacity continuously. Cluster utilisation is poor because reserved-but-unused capacity cannot be given to anyone else, and this is where most cloud overspend originates.
pick when latency-sensitive production services where predictability is the requirement and the cost is justified by the SLA
Requests below limits (Burstable QoS)
+ you gain much higher cluster utilisation, because pods reserve their typical usage and borrow idle capacity when they need to burst. More workloads fit on the same nodes.
− you pay performance now depends on neighbours. Under contention you are throttled to your request, so latency degrades exactly when the cluster is busiest — which is exactly when you least want it. Eviction risk is higher too.
pick when batch jobs, background workers, and services with spiky traffic and no strict tail-latency requirement
No memory limit, request set accurately
+ you gain avoids OOM kills caused by a limit set slightly too low, which is one of the most common and most confusing failure modes — particularly for runtimes whose memory use depends on available memory rather than a configured heap.
− you pay one leaking pod can consume the node and cause evictions across every workload on it. You have traded a contained failure for an uncontained one.
pick when essentially never in a shared cluster; it is defensible only on a dedicated node pool running a single workload
What a senior engineer actually does

Always set a memory limit — memory is incompressible, so exceeding it has no graceful degradation, only an OOM kill. Set the memory request equal to the limit for production services so the QoS class is predictable. Be more relaxed with CPU: exceeding a CPU limit only throttles, so a request well below the limit is a reasonable trade for utilisation.

Set the numbers from observed usage, not from guesses. Take the p99 of actual consumption over a representative period and add headroom. The failure mode of guessing is bimodal and both ends are bad: guess high and you quietly waste a large fraction of your cluster spend, guess low and you get evictions and OOM kills under exactly the load conditions where you needed the service most.


(c) Hands-on · 25 min

Spin up a real Kubernetes cluster on your laptop with kind, deploy a 2-replica web app, expose it via a Service, and prove self-healing by killing a pod.

#!/usr/bin/env bash# s066-k8s-demo.sh real K8s in ~5 minutes.# Requires: docker, kubectl, kind (https://kind.sigs.k8s.io/)set -euo pipefail DIR="$HOME/projects/learning/s066"mkdir -p "$DIR" && cd "$DIR"log() { printf "\033[1;36m %s\033[0m\n" "$*"; } log "1/5 Creating a local kind cluster"if ! kind get clusters | grep -q

What each block does

Anatomy of the manifest

kind: Deployment · replicas: 2
Declares desired state: two identical pods should always exist. Deployment controller enforces this against reality.
workload
matchLabels vs template.labels
The critical pair. Deployment's selector must match its own pod template labels, and the Service's selector must also match. Get this wrong = phantom pods.
glue
strategy: RollingUpdate maxSurge:1 maxUnavailable:0
During a version bump, briefly run 3 pods (2 desired + 1 new) and never drop below 2 available. Zero-downtime deploys are opt-in via this stanza.
rollout
requests vs limits
requests = what the scheduler reserves; limits = what the kernel enforces. Set requests too low → node overpacks. Set limits too low → OOMKill.
resources
readinessProbe
‘Am I ready to receive traffic?’ Service excludes pods that fail this. First endpoint for a debug: `kubectl describe pod` shows recent probe failures.
traffic
livenessProbe
‘Am I alive at all?’ Failing this = pod is killed and restarted. Set generous initialDelaySeconds so slow-starting apps don't get killed during startup.
health
Try itBreak the app deliberately and watch CrashLoopBackOff

Run:

kubectl set image deployment/web nginx=nginx:doesnotexist
kubectl get pods -l app=web -w

You'll see pods flip through ImagePullBackOff and (if you use a real image with a bad command) CrashLoopBackOff. The restart backoff grows exponentially — 10s, 20s, 40s, 80s, capped at 5 min. Roll back:

kubectl rollout undo deployment/web
kubectl rollout status deployment/web

Rollback is one command because the old ReplicaSet is still around. This is why K8s deploys feel bulletproof after a while.

💡 Hint · Set the container image to a nonexistent tag. kubectl describe pod shows exactly why, and how the backoff timer grows.

(d) Production reality · 15 min

War story Reddit· 2023314-minute total outage
🔥 What broke

Reddit upgraded Kubernetes from 1.23 to 1.24. Post-upgrade, a niche default changed — the way pods discovered nodes via the cluster's internal DNS. Route lookups started failing intermittently.

Cascading effect: Redis clients couldn't reach Redis, cache misses stormed the DB, every API endpoint slowed to seconds. Rolling back the K8s version isn't a one-liner; it took hours to isolate the specific change and patch DNS config.

🧯 The fix
Immediate: patched CoreDNS config to restore the old behaviour. Long-term: staging cluster now runs every K8s version bump for 2 weeks before prod. Reddit publishes a fantastic post-mortem worth reading in full.
🎓 Lesson to steal
Kubernetes minor-version upgrades are NOT patch releases — behaviour changes routinely. Always upgrade dev → staging → prod with soak time. Read the release notes for deprecated fields and default changes.
Post-mortem
War story Common failure mode · every first Kubernetes deploy· 2024daily
🔥 What broke
A pod has no resource requests set. The scheduler places it anywhere. Under load, the pod eats 16 GB of RAM on a node with a 2 GB Java heap next to it. Kernel OOM-killer picks the biggest process — often the Java pod — and kills something that had done nothing wrong.
🧯 The fix
Every workload must have resources.requests AND resources.limits set. Requests tell the scheduler how much to reserve. Limits give the kernel a ceiling. Adopt a policy: no requests = pod gets rejected by the admission controller (Gatekeeper, Kyverno).
🎓 Lesson to steal
Kubernetes without resource requests is Kubernetes without the ‘orchestration’ part. It's just ‘random pod placement’. Set them, tune them from real usage (VPA or Grafana), enforce them.
War story Common failure mode · label mismatch· 2024every first-week engineer
🔥 What broke
Deployment labels its pods app: my-app. Service selector is app: myapp (no dash). kubectl get pods shows healthy pods. kubectl get svc shows the Service. kubectl get endpoints my-svc shows NONE — zero backing pods. Every request 503s and nobody knows why.
🧯 The fix
Always check endpoints as the first debugging step: kubectl get endpoints <service>. If it's empty, your selector doesn't match. Adopt Kustomize / Helm to generate labels from a single source, so mismatch becomes impossible.
🎓 Lesson to steal
Kubernetes routing is late-binding via labels. Two objects can be perfectly healthy AND still not connected. `kubectl get endpoints` is your best diagnostic tool. Learn it early.

Common footguns to internalise

  • CrashLoopBackOff without logs — the app crashes so fast that logs are empty. Try kubectl logs <pod> --previous to get the last dying breath.
  • imagePullPolicy: Always with :latest — every restart re-pulls the tag. Combined with mutable :latest, every restart is a Russian roulette deploy.
  • Missing PodDisruptionBudget — cluster maintenance drains a node, all your replicas get evicted simultaneously, zero downtime turns into an outage. Set minAvailable: 1 (at least).
  • No graceful shutdown — SIGTERM arrives, your app doesn't handle it, in-flight requests die. Set terminationGracePeriodSeconds: 30 and handle SIGTERM in your app.

Where this shows up in the rest of the plan

Kubernetes basics feed every following ops session
S067 · Kubernetes advanced
ConfigMaps, Secrets, HPA autoscaling, NetworkPolicies. Builds directly on this session.
S068 · Azure Cloud
AKS = managed K8s. The four objects here are exactly what you deploy on AKS.
S069 · IaC · Terraform
Terraform provisions the cluster; K8s manifests deploy INTO it. Two layers of IaC.
S078 · SRE · SLOs
Pod-level metrics via Prometheus; per-Deployment SLO burn rates.
S085 · Security · pod security
PSA, admission controllers, network policies. The security layer on top.
S095 · System design · microservices
Every microservice = a Deployment + Service. Standard pattern for a decade to come.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

  1. What's the difference between a Pod and a Deployment?
  2. How does a Service find the pods it routes to? (label selector + endpoints)
  3. Name two things you must set on every production pod. (resources · readinessProbe · non-root user · terminationGracePeriodSeconds)

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.