In Part 3 you saw how namespaces keep a cluster tidy, and back in Part 2 you ran raw Pods by hand. Running Pods by hand is fine for learning, but it falls apart fast in real life: nobody restarts a crashed Pod at 3 a.m., and nobody wants downtime just to ship new code. In this part you will cook with Deployments, the workload object that manages replicas, heals itself, rolls out updates without downtime, and keeps a one-command undo button in your pocket.

What you need

  • A local cluster from Part 1 (kind or minikube both work).
  • kubectl configured to talk to it (kubectl get nodes should answer).
  • Comfort with Pods from Part 2. That is all — every file below is created from scratch.

Recipe 1: Serve your first Deployment

Problem: You want three copies of your demo app running at all times, managed as one unit, not three loose Pods you babysit.

Create a file named deployment.yaml:

apiVersion: apps/v1 kind: Deployment metadata: name: clinerds-demo labels: app: clinerds-demo spec: replicas: 3 selector: matchLabels: app: clinerds-demo template: metadata: labels: app: clinerds-demo spec: containers: - name: web image: hashicorp/http-echo args: - "-text=hello from the deployment" ports: - containerPort: 5678 

Apply it and look at everything it created:

kubectl apply -f deployment.yaml kubectl get deploy,rs,pods 
NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/clinerds-demo 3/3 3 3 20s NAME DESIRED CURRENT READY AGE replicaset.apps/clinerds-demo-7d9c4f8b5 3 3 3 20s NAME READY STATUS RUNNING AGE pod/clinerds-demo-7d9c4f8b5-2xkpt 1/1 Running 0 20s pod/clinerds-demo-7d9c4f8b5-8qmzv 1/1 Running 0 20s pod/clinerds-demo-7d9c4f8b5-wj6ln 1/1 Running 0 20s 

Notice you only wrote one YAML file, but three kinds of objects appeared. Check one Pod actually serves traffic:

kubectl port-forward deployment/clinerds-demo 8080:5678 # in another terminal: curl localhost:8080 

How it works: A Deployment does not run containers directly. It works through a chain: Deployment creates a ReplicaSet, and the ReplicaSet creates the Pods. A ReplicaSet is a simple bookkeeping object whose only job is "keep N Pods with these labels alive." The Deployment's selector.matchLabels must match the labels in template.metadata.labels — that label is the string tying the whole chain together. Why the extra layer? Because the ReplicaSet is pinned to an exact Pod template. When you change the template later, the Deployment makes a new ReplicaSet, and the old one stays behind as a snapshot. That is what makes rollouts and rollbacks possible in Recipes 4 and 5. Also note the Pod names: ReplicaSet hash plus a random suffix. Never hardcode a Pod name anywhere — they are disposable by design.

Recipe 2: Prove self-healing

Problem: You want to know that when a Pod dies — crash, node hiccup, or a clumsy kubectl delete — your app keeps running without you.

Watch the Pods in one terminal:

kubectl get pods -w 

In another terminal, delete one of them (use a real name from your cluster):

kubectl delete pod clinerds-demo-7d9c4f8b5-2xkpt 

In the watching terminal you will see the old Pod go Terminating and a brand new Pod — with a different random suffix — appear and reach Running within seconds:

clinerds-demo-7d9c4f8b5-2xkpt 1/1 Terminating 0 5m clinerds-demo-7d9c4f8b5-9cfrd 0/1 Pending 0 0s clinerds-demo-7d9c4f8b5-9cfrd 1/1 Running 0 4s 

How it works: Kubernetes runs a reconciliation loop. Controllers constantly compare desired state (your YAML says 3 replicas) with actual state (how many matching Pods are alive right now). The moment actual drops below desired, the ReplicaSet creates a replacement. You never asked it to — the loop just keeps pulling reality back toward your declaration. This is the big mental shift from Docker: with docker run you manage processes; with Kubernetes you declare an outcome and the cluster maintains it forever.

When it goes wrong:

  • If the new Pod stays Pending, your cluster is out of room — run kubectl describe pod <name> and read the Events at the bottom.
  • If it flaps between Running and CrashLoopBackOff, the container itself is failing. That is app trouble, not scheduling trouble: kubectl logs <pod> is your friend.

Recipe 3: Scale the service

Problem: Traffic doubled (or you just want more headroom). You need five copies instead of three, without touching anything else.

There are two ways. The declarative way: edit deployment.yaml, change replicas: 3 to replicas: 5, and re-apply:

kubectl apply -f deployment.yaml kubectl get pods 

The imperative way, straight from the command line:

kubectl scale deployment clinerds-demo --replicas=5 

Either way, two new Pods appear. Scale back down with replicas: 3 and the extra Pods are terminated.

How it works: Both paths end at the same place — the replicas field on the Deployment, which the ReplicaSet treats as its target. kubectl scale is a shortcut that edits that one field on the live object; kubectl apply re-syncs the whole manifest. The loop from Recipe 2 does the rest, creating or deleting Pods until actual matches desired.

Variations:

  • Prefer the YAML route when you keep manifests in Git. kubectl scale only changes the cluster, so your file and reality drift apart — and the next kubectl apply silently scales you back.
  • Kubernetes can also scale for you. kubectl autoscale deployment clinerds-demo --min=2 --max=10 --cpu-percent=80 creates a Horizontal Pod Autoscaler (HPA) that adds replicas when CPU usage climbs. Caveat: it needs metrics-server installed, which minikube ships as an addon (minikube addons enable metrics-server) but kind does not include by default. We will revisit autoscaling later in the series.

Recipe 4: Roll out an update with zero downtime

Problem: You want to ship a new version — a different message from http-echo — without visitors ever hitting a gap.

First, add a rollout strategy to deployment.yaml under spec (same level as replicas), and change the text:

spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: clinerds-demo template: metadata: labels: app: clinerds-demo spec: containers: - name: web image: hashicorp/http-echo args: - "-text=version two is live" ports: - containerPort: 5678 

Apply and watch the rollout happen:

kubectl apply -f deployment.yaml kubectl rollout status deployment clinerds-demo 
Waiting for deployment "clinerds-demo" rollout to finish: 2 out of 3 new replicas have been updated... Waiting for deployment "clinerds-demo" rollout to finish: 1 old replicas are pending termination... deployment "clinerds-demo" successfully rolled out 

While it runs, kubectl get rs shows the overlap — the old ReplicaSet draining as the new one fills:

NAME DESIRED CURRENT READY AGE clinerds-demo-7d9c4f8b5 1 1 1 30m clinerds-demo-5b6f9d7c4 3 3 3 40s 

How it works: Changing the Pod template produces a new template hash, so the Deployment builds a new ReplicaSet and performs a RollingUpdate: spin up new Pods, wait for each to pass its readiness, then retire old ones. The strategy block tunes the shape of that handoff. maxSurge: 1 allows one extra Pod above your replica count during the swap, and maxUnavailable: 0 forbids dropping below the full count — together they guarantee three working Pods at every moment. kubectl rollout status just blocks until the new ReplicaSet is fully up and the old one is empty, which makes it perfect for CI scripts.

Recipe 5: Roll back a bad release

Problem: Version two shipped a bug (or just the wrong message). You need to go back — fast, and ideally to a specific known-good revision.

First, make history useful. Annotate each release as you apply it:

kubectl annotate deployment clinerds-demo \ kubernetes.io/change-cause="switch text to version two" --overwrite kubectl rollout history deployment clinerds-demo 
REVISION CHANGE-CAUSE 1 initial release: hello from the deployment 2 switch text to version two 

Now undo the release:

kubectl rollout undo deployment clinerds-demo kubectl rollout status deployment clinerds-demo 

Curl the app again and the original message is back. To jump to a specific revision instead of just "one step back":

kubectl rollout undo deployment clinerds-demo --to-revision=1 

How it works: Remember those old ReplicaSets from Recipe 4? The Deployment keeps them around (scaled to zero) as frozen snapshots of previous Pod templates. rollout undo does not rebuild anything — it points the Deployment back at an older ReplicaSet and scales it back up, using the same zero-downtime rolling mechanics. change-cause annotations are what turn rollout history from a list of mysterious numbers into a changelog your future self will thank you for.

Variations:

  • kubectl rollout pause deployment clinerds-demo freezes rollouts so you can apply several changes (image, env, resources) and ship them as one batch, then kubectl rollout resume to release it all at once.
  • The number of old revisions kept is controlled by spec.revisionHistoryLimit (default 10). Lower it on noisy Deployments to keep etcd lean.

Where to go next

Your Pods now heal, scale, and update themselves — but they still get a brand new IP address every time they are replaced, and nothing stable points at them. In Part 5: Services you will give the app a permanent virtual IP and DNS name, and learn how traffic finds the right Pods. For the full field-by-field reference, the official Deployment documentation is excellent. The whole series lives at /series/kubernetes-cookbook.