Week 8: Pods, ReplicaSets & Deployments

First real manifests this week. A Pod is the smallest thing Kubernetes schedules — but you'll almost never create one directly, because it has no self-healing behavior on its own. ReplicaSets and, on top of them, Deployments are what turn "one Pod" into "N Pods, always, self-healing, rolling out updates without downtime." This is the workload pattern you'll use for the vast majority of what you deploy going forward.

Module 8 of 17 Week 8 of 18 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Write a Pod manifest and explain why it's rarely created directly
  • Write a Deployment manifest and explain the ReplicaSet it manages underneath
  • Perform a zero-downtime rolling update and roll it back when it goes wrong

1. Pods, the Atomic Unit

A Pod is the smallest deployable unit in Kubernetes — one or more containers that always run together, on the same node, sharing the same network namespace and (optionally) storage. Most Pods run exactly one container; multiple containers in a Pod is the exception, reserved for tightly-coupled helper ("sidecar") patterns.

pod.yaml — a minimal Pod manifest
apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
  labels:
    app: my-app
spec:
  containers:
    - name: my-app
      image: myrepo/my-app:1.4.0
      ports:
        - containerPort: 3000
terminal
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod my-app-pod
kubectl logs my-app-pod
kubectl delete -f pod.yaml

Here's the crucial gap: if this Pod's node crashes, or the container inside it exits unexpectedly, nothing recreates it. A bare Pod has no controller watching over it — it's a one-shot declaration, not a self-healing promise. That gap is exactly what ReplicaSets exist to close.

You already know most of a Pod spec

The containers block maps almost directly to concepts from Weeks 1-6 — image, ports, and (as you'll see in Week 10) env. Kubernetes doesn't reinvent container configuration, it wraps the same ideas in a declarative, cluster-aware layer.

2. ReplicaSets

A ReplicaSet declares "I want exactly N Pods matching this template, running at all times" and continuously reconciles toward that — if a Pod disappears, it creates a replacement; if there are too many, it deletes the excess.

replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: my-app-rs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:                    # this IS a Pod template, embedded
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: myrepo/my-app:1.4.0
          ports:
            - containerPort: 3000
prove the self-healing, live
kubectl apply -f replicaset.yaml
kubectl get pods -l app=my-app     # 3 Pods

kubectl delete pod <one-of-the-pod-names>
kubectl get pods -l app=my-app     # still 3 — a replacement appeared automatically

The selector is how the ReplicaSet knows which Pods belong to it — it matches Pods by label, not by name or creation order. This label-based matching pattern is used everywhere in Kubernetes, and you'll see it again for Services in Week 9.

You'll almost never write a bare ReplicaSet

ReplicaSets have one glaring gap: no built-in way to roll out a new image version without downtime — change the image and you'd have to manually delete Pods to force replacement, all at once. That's precisely the problem Deployments solve, and it's why you'll write Deployments, not ReplicaSets, in practice.

3. Deployments

A Deployment manages ReplicaSets, the way a ReplicaSet manages Pods. You declare the desired state; the Deployment controller creates and manages the ReplicaSets underneath, and specifically knows how to transition between them safely when your Pod template changes.

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: myrepo/my-app:1.4.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
the ownership chain
kubectl apply -f deployment.yaml
kubectl get deployments        # 1 Deployment
kubectl get replicasets        # 1 ReplicaSet, created and owned by the Deployment
kubectl get pods               # 3 Pods, created and owned by that ReplicaSet

This is worth internalizing as a chain: Deployment → ReplicaSet → Pods, each layer owning and reconciling the one below it. You'll basically always interact with the Deployment; the ReplicaSet is an implementation detail the Deployment manages for you, most visible when a rolling update creates a second one temporarily.

resources.requests/limits aren't optional in practice

Without them, the scheduler has no idea how much of a node's capacity your Pod actually needs, and a runaway container has nothing stopping it from starving its neighbors. This block previews Week 14's deep dive on resource management — start the habit of setting it now.

4. Rolling Updates & Rollbacks

Change the image in a Deployment's Pod template, and the Deployment controller rolls it out gradually — spinning up new Pods on the new version, waiting for them to be ready, then retiring old ones, a few at a time, never taking the whole app down at once.

a rolling update
kubectl set image deployment/my-app my-app=myrepo/my-app:1.5.0
kubectl rollout status deployment/my-app
# Waiting for deployment "my-app" rollout to finish: 2 out of 3 new replicas...
# deployment "my-app" successfully rolled out

kubectl get replicasets
# TWO ReplicaSets now exist — the old one scaled to 0, the new one scaled to 3
something's wrong — roll it back
kubectl rollout history deployment/my-app       # see every past revision
kubectl rollout undo deployment/my-app          # roll back to the previous revision
kubectl rollout undo deployment/my-app --to-revision=2   # or a specific one

This is why the old ReplicaSet doesn't get deleted after a rollout — it's kept around (scaled to zero) specifically so a rollback is instant: scale the old one back up, scale the new one down, no rebuild or redeploy required.

This is why Week 6's tagging discipline matters

A rollout is only as safe as the image tag it points to. Deploying "latest" here means kubectl rollout undo has nothing meaningful to roll back to — the "old" ReplicaSet's image reference is still "latest," which may since have changed underneath you. Immutable version tags make rollbacks actually reliable.

5. Hands-on Exercise

Hands-on

Deploy your own image, break it with a bad rollout, and roll it back

Take an image you pushed to a registry in Week 6 and run it as a real Deployment, then deliberately trigger a failed rollout to practice recovering from one.

Part 1 — Deploy for real:

  1. Write a Deployment manifest for your Week 6 image, 3 replicas, with resource requests and limits set.
  2. kubectl apply -f it, then confirm 1 Deployment, 1 ReplicaSet, and 3 Pods exist and are Running.
  3. Delete one Pod manually and confirm a replacement appears within a few seconds.
  4. Scale it to 5 replicas with kubectl scale deployment/my-app --replicas=5 and confirm the count updates.
Hint

If your image is in a private registry, you'll need an imagePullSecret configured for kubectl to pull it — a quick search for "kubectl create secret docker-registry" will get you unblocked if you hit an ImagePullBackOff error.

Part 2 — Break a rollout on purpose, and recover:

  1. Tag and push a new, deliberately broken version of your image (e.g. one with a typo'd CMD that crashes immediately), then kubectl set image your Deployment to point at it.
  2. Watch kubectl rollout status — it should hang or fail as new Pods crash-loop instead of becoming ready.
  3. Run kubectl get pods and kubectl describe pod <a-crashing-one> to see the real failure evidence (CrashLoopBackOff status, relevant Events).
  4. Roll back with kubectl rollout undo, confirm the Deployment returns to a healthy state serving the previous version, and check kubectl rollout history to see both revisions recorded.
Hint

By default, a Deployment's rolling update strategy keeps at least some old, working Pods available while new ones are rolled out — this is exactly why a bad rollout usually doesn't take your whole app offline immediately, giving you time to notice and roll back.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why is a bare Pod (with no ReplicaSet/Deployment) rarely used directly?

A bare Pod has no controller watching over it — if its node fails or its container crashes, nothing recreates it. It's a one-time declaration, not a self-healing guarantee, which is why almost every real workload is managed through a Deployment instead.

Q2

What's the ownership chain from a Deployment down to a running container?

Deployment → ReplicaSet → Pod → container. The Deployment manages ReplicaSets (handling rollouts between them); each ReplicaSet manages a set of Pods matching its label selector; each Pod runs one or more containers.

Q3

Why does the old ReplicaSet stick around (scaled to 0) after a successful rolling update?

It's kept so a rollback can be instant — kubectl rollout undo just scales the old ReplicaSet back up and the new one back down, with no rebuild or redeploy needed. This is the whole mechanism behind fast, reliable rollbacks.

Q4

Why does deploying "latest" undermine the reliability of kubectl rollout undo?

The rollback re-references the OLD ReplicaSet's image tag — if that tag is "latest," it's a moving pointer that may no longer refer to the image that was actually running before. An immutable version tag guarantees the rollback restores the exact previous image, not whatever "latest" currently happens to mean.