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: 80

The 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:

ControllerUse for
DeploymentStateless apps (web/API) — the default choice
StatefulSetStateful apps needing stable identity/storage (databases)
DaemonSetOne Pod per node (log/metrics agents)
Job / CronJobRun-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:

ProbeQuestion it answersOn failure
livenessProbeIs the container still alive?Kill & restart the container
readinessProbeIs it ready for traffic?Remove Pod's IP from Services (no restart)
startupProbeHas 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:

on image change,creates a NEW ReplicaSetDeployment(desired state: image,replicas)ReplicaSet(keeps N identical Podsrunning)PodPodPodReplicaSet (new revision)
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: 80

Three 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 it

Two 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 above replicas.

🧠 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 templatenot 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 to

The four Service types — pick by "who needs to reach it?"

ClusterClusterIPinternal only(default)NodePortNodeIP:30000-32767LoadBalancercloud public/internal IPPod-to-PodtrafficExternal(dev/basic)External(production)ExternalNameCNAME to external DNS
TypeReachable fromTypical use
ClusterIP (default)Inside the cluster onlyService-to-service (e.g. order-serviceproduct-service)
NodePort<NodeIP>:<30000–32767>Dev/testing; building block under LoadBalancer
LoadBalancerExternal IP via the cloud LBProduction external endpoints; on AKS this provisions an Azure Load Balancer
ExternalNameDNS CNAME to an external hostPoint 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 undo reverts 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

  1. 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.
  2. Q: What triggers a Deployment rollout? A: A change to .spec.template (e.g. new image). Scaling does not.
  3. Q: Which probe removes a Pod from Service endpoints without killing it? A: readinessProbe.
  4. Q: Default Service type, and who can reach it? A: ClusterIPinside the cluster only.
  5. Q: How does one Pod address another app? A: By Service DNS name (svc-name.namespace.svc.cluster.local).
  6. 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
  1. ClusterIP — internal-only.
  2. LoadBalancer — external public IP (AKS provisions an Azure Load Balancer; for many HTTP routes you'd front it with Ingress).
  3. ExternalName — returns a CNAME to db.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.