Deployment strategies

RollingUpdate vs Recreate, and blue/green in practice.

Advanced10 min · lesson 12 of 65
In plain terms
Choosing a rollout is choosing how you swap tyres: change them one at a time while still driving (rolling), stop the car completely (recreate), or keep a second car ready and just switch cars (blue/green).

A Deployment never upgrades a running Pod. When you push a new image, nothing reaches into the old container and swaps the binary out from under it. Kubernetes builds a fresh set of Pods, waits for them, and retires the old ones. A Pod is the smallest thing the cluster runs: one or more containers that share a network address and live and die together. The strategy field on a Deployment decides the choreography of that swap. How many new Pods come up before old ones go down, and whether both versions are ever alive at the same moment.

RollingUpdate vs Recreate

Two strategies ship in the box. RollingUpdate is the default, and it works like a relay race where the next runner is already sprinting alongside before the current one hands off the baton and peels away. New Pods start, they pass their health check, and only then do old Pods get terminated, a few at a time, until the whole fleet is the new version. The service stays up the entire time because there are always enough healthy Pods answering requests. Recreate is the opposite move. It kills every old Pod first, waits for them to fully die, then starts the new ones. You get a gap where nothing is serving, but you are guaranteed to never have two versions running together for even a second.

Here is the part that trips people up. A Deployment does not manage Pods directly. It manages ReplicaSets, and a ReplicaSet is the little controller that keeps a fixed number of copies of a Pod alive. Every time you change the Pod template (a new image, a new environment variable, anything at all), the Deployment controller creates a brand-new ReplicaSet and stamps its Pods with a pod-template-hash label so the two generations never get confused. A rollout is really just the controller scaling the new ReplicaSet up while scaling the old one down. Two dials govern the pace: maxSurge (how many extra Pods it may run above your desired count) and maxUnavailable (how many it is allowed to be short). Set maxUnavailable: 0 with maxSurge: 25% and you get the safe pattern, which adds new capacity first and never dips below full strength.

rolling.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 4
selector:
matchLabels: { app: web }
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # up to 1 extra Pod (25% of 4) during the swap
maxUnavailable: 0 # never drop below 4 ready Pods
minReadySeconds: 10 # a new Pod must stay Ready 10s before it counts
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: nginx:1.26
ports: [{ containerPort: 80 }]
readinessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 5
apply and verify
$ kubectl apply -f rolling.yaml
deployment.apps/web created
$ kubectl get deploy web
NAME READY UP-TO-DATE AVAILABLE AGE
web 4/4 4 4 40s

Now roll a new version out and watch the controller work. kubectl set image edits the Pod template in place, which is exactly the change that spawns a new ReplicaSet and starts the swap.

trigger the rollout
$ kubectl set image deployment/web web=nginx:1.27
deployment.apps/web image updated
$ kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 1 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 2 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 3 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 3 of 4 updated replicas are available...
deployment "web" successfully rolled out

Run kubectl get rs while that is happening and you can watch the two ReplicaSets trade places. The old one drains as the new one fills, and the pod-template-hash in each name is what keeps their Pods separate.

the ReplicaSet handoff
$ kubectl get rs -l app=web
NAME DESIRED CURRENT READY AGE
web-6f8b9c7d5 2 2 2 11m # old, nginx:1.26, scaling down
web-7d4f9b8c6 3 3 2 18s # new, nginx:1.27, scaling up

One more control matters when things go wrong: progressDeadlineSeconds (default 600). If the new ReplicaSet cannot make progress in that window, say the new image crash-loops or never passes its readiness check, the Deployment stops waiting and sets a condition Progressing=False with reason ProgressDeadlineExceeded. That is your signal. kubectl rollout status exits non-zero, and kubectl describe deployment shows the reason. The important thing is the old ReplicaSet is still there and still serving traffic, so the service never went down. To bail out, kubectl rollout undo scales the previous ReplicaSet back up. Kubernetes keeps old ReplicaSets around for exactly this, capped by revisionHistoryLimit (default 10).

a stuck rollout, and the escape hatch
$ kubectl rollout status deployment/web --timeout=90s
Waiting for deployment "web" rollout to finish: 1 out of 4 new replicas have been updated...
error: deployment "web" exceeded its progress deadline
$ kubectl rollout undo deployment/web
deployment.apps/web rolled back

To switch to Recreate you patch the strategy field, and there is a catch the cluster enforces: rollingUpdate and type: Recreate cannot both be set, so you have to null the old block out or the patch is rejected. Trigger a rollout afterward and watch the events. You will see the tell-tale order that proves versions never overlap: everything scales to zero first, then back up.

switch to Recreate
$ kubectl patch deployment web --type merge \
-p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
deployment.apps/web patched
$ kubectl set image deployment/web web=nginx:1.28
deployment.apps/web image updated
$ kubectl describe deployment web | grep -A6 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 20s deployment-controller Scaled down replica set web-7d4f9b8c6 to 0 from 4
Normal ScalingReplicaSet 6s deployment-controller Scaled up replica set web-5c9d7f6b4 to 4 from 0

Blue/green and canary: patterns you assemble

Neither blue/green nor canary is a strategy you can type into a Deployment. They are patterns you build from two Deployments and a Service. A Service is the stable front door for your app: it has one virtual IP address that never changes, and it forwards traffic to whichever Pods match its label selector. That selector is the switch you flip.

Blue/green means you run the current version (blue) and the new version (green) as two full Deployments at the same time, labelled so the Service can tell them apart. The Service points at blue. You test green privately, hitting its Pods directly, and when you are happy you flip the selector to green in a single edit. Cutover is instant because it is just a label change, and rollback is nothing more than flipping back. The bill for that safety is capacity: for a while you are paying to run two full copies of the app.

blue to green cutover
$ kubectl patch service web -p '{"spec":{"selector":{"app":"web","version":"green"}}}'
service/web patched
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.1.31:80,10.244.2.44:80,10.244.3.12:80 23m

Canary is the cautious cousin, named after the caged bird miners once sent ahead to test the air. Instead of an all-at-once flip, you send a small slice of live traffic to the new version and watch the metrics. The crude version runs one green Pod next to nine blue Pods behind the same Service, so roughly 10% of requests land on green. The precise version hands traffic-splitting to an ingress controller (the component that routes outside traffic into the cluster) or a service mesh (a networking layer that sits between your services), either of which can weight traffic 95/5 and step it up on a schedule. Either way, the point is to expose the new version to real users a little at a time and roll back at the first sign of errors, before most of your traffic ever touched it.

maxUnavailable: 0 means nothing without a readiness probe
The controller only treats a new Pod as available once its readiness probe passes. Leave the probe out, and Kubernetes calls a Pod ready the instant its container process starts, before your app has loaded config, warmed a cache, or opened a database connection. A rolling update then happily deletes old Pods while the new ones are still booting, and requests hit Pods that are not listening yet. You get dropped connections on every single deploy and end up blaming the load balancer. Always ship a readinessProbe with a rolling Deployment, otherwise maxUnavailable: 0 is a promise the cluster cannot keep.
Diagram
Can the old and new versions run at the same time?
This one question drives almost the whole choice
No: versions clash (schema, singleton, shared state)
Recreate
Kill every old Pod, then start the new ones. Accept a short outage to guarantee one version at a time.
Yes: routine stateless deploy
RollingUpdate
The default. Overlap old and new with maxSurge, maxUnavailable: 0, and a readiness probe for true zero-downtime.
Yes, but you need instant rollback
Blue/green
Two full Deployments. Flip the Service selector to cut over, flip it back to undo. Costs double capacity.
Yes, but prove it on real traffic first
Canary
Send a small % to the new version, watch metrics, then expand or abort.

Recreate is honest for apps that cannot run two versions against one database schema. Do not pretend RollingUpdate fixes incompatible migrations.

Blue/green needs twice the capacity and a crisp cutover checklist. Selector edits are fast and easy to get wrong under stress.

Canary is a percentage problem: metrics and a rollback switch matter more than the YAML shape.

Try this

Compare a RollingUpdate Deployment with a Recreate one in a namespace you can break. Then sketch how blue/green would look with two Deployments and a Service selector flip.

terminal
$ kubectl apply -f rolling.yaml
deployment.apps/web created
$ kubectl get deploy web
NAME READY UP-TO-DATE AVAILABLE AGE
web 4/4 4 4 40s
$ kubectl set image deployment/web web=nginx:1.27
deployment.apps/web image updated
$ kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 1 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 2 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 3 out of 4 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 3 of 4 updated replicas are available...
deployment "web" successfully rolled out
$ kubectl get rs -l app=web
NAME DESIRED CURRENT READY AGE
web-6f8b9c7d5 2 2 2 11m # old, nginx:1.26, scaling down
web-7d4f9b8c6 3 3 2 18s # new, nginx:1.27, scaling up
$ kubectl rollout status deployment/web --timeout=90s
Waiting for deployment "web" rollout to finish: 1 out of 4 new replicas have been updated...
error: deployment "web" exceeded its progress deadline
$ kubectl rollout undo deployment/web
deployment.apps/web rolled back
$ kubectl patch deployment web --type merge \
-p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
deployment.apps/web patched
$ kubectl set image deployment/web web=nginx:1.28
deployment.apps/web image updated
$ kubectl describe deployment web | grep -A6 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 20s deployment-controller Scaled down replica set web-7d4f9b8c6 to 0 from 4
Normal ScalingReplicaSet 6s deployment-controller Scaled up replica set web-5c9d7f6b4 to 4 from 0
$ kubectl patch service web -p '{"spec":{"selector":{"app":"web","version":"green"}}}'
service/web patched
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.1.31:80,10.244.2.44:80,10.244.3.12:80 23m

Takeaway

RollingUpdate trades capacity for smoothness. Recreate is downtime by design. Blue/green and canaries are Service or mesh tricks on top of Deployments.

Quick check
01Your rolling Deployment has maxUnavailable: 0 and maxSurge: 25%, yet every deploy still throws a few seconds of 502 (bad gateway) errors. The Pods start fine and the app is stateless. What is the most likely cause?
Correct — With maxUnavailable: 0 the controller waits for new Pods to be available before deleting old ones, but with no readiness probe available just means the container process started. Traffic reaches Pods that are not listening yet.
Incorrect — maxSurge controls extra capacity above desired, not readiness. Raising it does not stop requests from hitting Pods that were counted ready before they could actually serve. The missing readiness signal is the real gap.
Incorrect — Recreate would make it worse. It takes every Pod down before starting new ones, turning a few 502s into a full outage window, and a stateless app does not need it at all.
Incorrect — A broken selector gives sustained failures, not a brief blip that lines up with each deploy. The timing pointing straight at the rollout is the clue that this is a readiness problem.
02RollingUpdate is the default and keeps the service up throughout. When would you deliberately choose the Recreate strategy instead?
Incorrect — Extra pods above the count is maxSurge under RollingUpdate; Recreate adds no capacity and takes everything down first.
Correct — Recreate kills every old pod before starting new ones, producing a gap in service but guaranteeing the versions never overlap, which is what you want for something like a non-backward-compatible schema change.
Incorrect — That is canary, a pattern assembled from Deployments and a Service, not the Recreate strategy.
Incorrect — That describes blue/green; Recreate involves an outage and has no instant-selector switch.
03You want to ship a new version but first route only about 5% of real user traffic to it, watch error metrics, and abort at the first sign of trouble before most users are affected. Which approach fits?
Incorrect — Recreate does a full-fleet swap with an outage and sends 100% of traffic to the new version, the opposite of a small slice.
Incorrect — Blue/green cuts over all traffic in a single flip; it does not gradually expose a small percentage first.
Incorrect — RollingUpdate replaces every pod until the whole fleet is new; maxUnavailable governs availability, not a fixed traffic percentage.
Correct — canary sends a small percentage to the new version, watches, then widens or rolls back, achieved with a few green pods among many blue or an ingress/mesh weighting like 95/5.

Related