Episode 3 — Core Workload Objects: Pods, Deployments, and Services
You have a cluster with node pools. Now we put software on it. Three objects do 90% of the work — Pods (run containers), Deployments (keep them running and update them safely), and Services (give them a stable address). Master this trio and you can operate almost any app.
Learning objectives
By the end of this episode you can:
- Explain the chain Deployment → ReplicaSet → Pod and why you almost never create Pods directly.
- Perform a rolling update and a rollback, and read
kubectl rollout status. - Choose the right Service type (
ClusterIP,NodePort,LoadBalancer,ExternalName) and use DNS service discovery. - Add liveness / readiness / startup probes so Kubernetes can self-heal your app.
Why it matters
Every deploy, every incident, every "why is my app unreachable?" traces back to these three objects. Pods are disposable, so you manage them through controllers; Services exist because Pod IPs change. Internalize that and the cluster stops feeling like magic.
1. Pods — the smallest deployable unit (and why you don't create them)
A Pod is one or more containers that share a network namespace and storage — an application-specific "logical host." Containers in the same Pod share one IP and can talk over localhost.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80The crucial operational fact: Pods are ephemeral and disposable. A Pod stays on its node until it finishes, is deleted, is evicted for lack of resources, or the node fails — and it is never rescheduled elsewhere on its own. A Pod is not a process; it's an environment for containers.
🧠 Mnemonic: Pods are cattle, not pets. You don't nurse a sick Pod back to health — a controller replaces it.
That's why you almost never write kind: Pod. Instead you use a workload controller that creates Pods from a Pod template and heals them:
| Controller | Use for |
|---|---|
| Deployment | Stateless apps (web/API) — the default choice |
| StatefulSet | Stateful apps needing stable identity/storage (databases) |
| DaemonSet | One Pod per node (log/metrics agents) |
| Job / CronJob | Run-to-completion / scheduled tasks |
Multi-container patterns and probes
A Pod can hold init containers (run to completion first), sidecar containers (run alongside the app), and ephemeral containers (attached for debugging). And the kubelet monitors container health with three probes — the single most under-used reliability feature:
| Probe | Question it answers | On failure |
|---|---|---|
livenessProbe | Is the container still alive? | Kill & restart the container |
readinessProbe | Is it ready for traffic? | Remove Pod's IP from Services (no restart) |
startupProbe | Has a slow app finished booting? | Hold off other probes until it passes |
💡 Tip: readiness ≠ liveness. A Pod can be alive but not ready (e.g. warming a cache). Getting these right is what makes rolling updates zero-downtime.
2. Deployments — declarative, self-healing, updatable
A Deployment provides declarative updates for Pods and ReplicaSets. You describe a desired state; the Deployment controller drives actual state to match at a controlled rate. The ownership chain:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx # MUST match template labels below
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80Three fields you must get right: replicas (how many Pods), selector (which Pods this Deployment owns — immutable after creation), and template (the Pod blueprint). The selector must match the template labels or the API rejects it.
Rolling updates — the everyday superpower
A rollout is triggered only when the Pod template (.spec.template) changes — e.g. a new image. Scaling alone does not trigger a rollout. The controller spins up a new ReplicaSet and scales it up while scaling the old one down, replacing Pods gradually:
kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1 # trigger rollout
kubectl rollout status deployment/nginx-deployment # watch itTwo knobs control the pace (both default 25%):
maxUnavailable— how many Pods may be down during the update.maxSurge— how many extra Pods may be created abovereplicas.
🧠 Mnemonic: surge = spare tires you add; unavailable = seats you're allowed to empty.
Rollback — undo a bad deploy in one command
Deployment keeps a revision history (default 10). If a rollout goes bad (crash loop, bad image tag), undo it:
kubectl rollout history deployment/nginx-deployment # list revisions
kubectl rollout undo deployment/nginx-deployment # back to previous
kubectl rollout undo deployment/nginx-deployment --to-revision=2⚠️ Pitfall: rollback only reverts the Pod template — not the replica count. A rollout can get stuck on image-pull errors, failing readiness probes, or quota limits; set progressDeadlineSeconds so Kubernetes reports ProgressDeadlineExceeded instead of hanging silently.
You can also kubectl scale, kubectl rollout pause/resume (batch several edits into one rollout), and choose strategy RollingUpdate (default) vs Recreate (kill all, then recreate — causes downtime, used when versions can't coexist).
3. Services — a stable address in front of disposable Pods
Because Pod IPs come and go, clients can't target Pods directly. A Service gives a stable virtual IP (the cluster IP) and DNS name in front of a set of Pods selected by label. The Service controller keeps the backing EndpointSlices in sync as Pods appear and disappear.
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp # picks the Pods to load-balance across
ports:
- protocol: TCP
port: 80 # the Service port clients hit
targetPort: 9376 # the container port traffic is sent toThe four Service types — pick by "who needs to reach it?"
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP (default) | Inside the cluster only | Service-to-service (e.g. order-service → product-service) |
NodePort | <NodeIP>:<30000–32767> | Dev/testing; building block under LoadBalancer |
LoadBalancer | External IP via the cloud LB | Production external endpoints; on AKS this provisions an Azure Load Balancer |
ExternalName | DNS CNAME to an external host | Point in-cluster name at an external DB/service |
💡 On AKS, type: LoadBalancer makes Azure provision a Standard SKU load balancer and a public (or internal) IP automatically — that's how the quickstart app got its browsable IP. For HTTP with many routes behind one IP, you'll use Ingress/Gateway instead (Episode 6).
Service discovery = DNS
With CoreDNS running (it always is on AKS), any Pod can reach a Service by name:
<service-name>.<namespace>.svc.cluster.local
# same namespace? just: <service-name>
So product-service calling http://order-service:3000 just works — no IPs hardcoded. Other useful bits: multi-port Services (name each port), sessionAffinity: ClientIP for sticky sessions, and externalTrafficPolicy: Local to preserve the client source IP (and avoid an extra network hop).
4. The daily kubectl loop
You'll live in these commands. Learn the verbs, not the whole reference:
kubectl apply -f app.yaml # create/update from manifest (declarative)
kubectl get pods -o wide # list Pods + which node they're on
kubectl get deploy,rs,svc # see the whole chain at once
kubectl describe pod <name> # events + why it's Pending/CrashLooping
kubectl logs <pod> [-c <container>] [-f] # container logs (-f to follow)
kubectl exec -it <pod> -- sh # shell into a running container
kubectl rollout status deploy/<name> # watch a rollout
kubectl rollout undo deploy/<name> # roll back
kubectl scale deploy/<name> --replicas=5 # manual scale🧠 Mnemonic — "AGDLE": Apply, Get, Describe, Logs, Exec — the five verbs that solve most day-to-day problems.
Key takeaways
- Pods are disposable; you manage them through controllers (Deployment for stateless apps). Never hand-nurse a Pod.
- A Deployment owns a ReplicaSet owns Pods; changing the Pod template creates a new ReplicaSet = a rolling update.
- Rolling updates are governed by
maxSurge/maxUnavailable;kubectl rollout undoreverts a bad deploy. - Probes make apps self-healing: liveness restarts, readiness gates traffic, startup covers slow boots.
- A Service gives Pods a stable IP + DNS name; pick ClusterIP (internal), NodePort/LoadBalancer (external), or ExternalName.
- Service discovery is DNS:
service.namespace.svc.cluster.local.
Quick recall
- Q: A node dies. Does the Pod on it move itself to another node? A: No — a Pod never reschedules itself; a controller (e.g. Deployment/ReplicaSet) creates a replacement elsewhere.
- Q: What triggers a Deployment rollout? A: A change to
.spec.template(e.g. new image). Scaling does not. - Q: Which probe removes a Pod from Service endpoints without killing it? A:
readinessProbe. - Q: Default Service type, and who can reach it? A:
ClusterIP— inside the cluster only. - Q: How does one Pod address another app? A: By Service DNS name (
svc-name.namespace.svc.cluster.local). - Q: Roll back to two revisions ago? A:
kubectl rollout undo deploy/<name> --to-revision=<N>.
Exercises
Exercise 3.1 — Trace the chain.
You run kubectl apply on a Deployment with replicas: 3. List the objects Kubernetes creates and their parent/child relationship, then name the command that shows Pods and their nodes.
Worked solution
Kubernetes creates: 1 Deployment → 1 ReplicaSet → 3 Pods (ReplicaSet name is <deploy>-<hash>; Pods are <deploy>-<hash>-<random>). The Deployment owns the ReplicaSet; the ReplicaSet owns the Pods and keeps the count at 3. Show Pods + nodes with kubectl get pods -o wide.
Exercise 3.2 — Zero-downtime deploy, then a rollback.
Write the commands to (a) update web Deployment to image myapp:2.0, (b) watch the rollout, and (c) roll it back after discovering 2.0 crash-loops. What field would have made Kubernetes report the stall automatically?
Worked solution
kubectl set image deployment/web web=myapp:2.0 # (a)
kubectl rollout status deployment/web # (b)
kubectl rollout undo deployment/web # (c)Set .spec.progressDeadlineSeconds (e.g. 600) so the controller marks the rollout ProgressDeadlineExceeded instead of waiting forever. Readiness probes are what detect the crash-loop so the new Pods never receive traffic.
Exercise 3.3 — Choose the Service type.
For each, name the Service type: (1) an internal payments API only other Pods call; (2) a public website users browse; (3) pointing legacy-db in-cluster at db.corp.example.com.
Worked solution
ClusterIP— internal-only.LoadBalancer— external public IP (AKS provisions an Azure Load Balancer; for many HTTP routes you'd front it with Ingress).ExternalName— returns a CNAME todb.corp.example.com, no proxying.
Coming up
Your app runs and is reachable. But it still has config baked into the image and no resource guardrails. In Episode 4 we fix both: ConfigMaps and Secrets to externalize configuration, and requests/limits to control scheduling, QoS, and cost.