| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Kubernetes clusters with Linux worker nodes |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Blue-green and canary patterns with Deployments, Pod labels, ClusterIP Services, selector switches, in-cluster verification, rollback, comparison tables, and common routing mistakes. Does not cover maxSurge tuning, Helm, GitOps, service mesh weights, Ingress canary annotations, Gateway API splitting, or progressive delivery controllers. |
| Related guides | Pod versus Deployment Choose a Kubernetes workload resource |
This walkthrough uses the strategy-lab namespace and hashicorp/http-echo so responses show v1 or v2 in plain text. You will run blue-green cutover with a Service selector change, then a canary release that mixes stable and canary Pods behind one Service.
Kubernetes Deployment Strategy Overview
A Deployment normally performs rolling updates: it replaces Pods incrementally through ReplicaSets. That path is documented in Deployments and rolling updates.
Blue-green and canary are release patterns built from standard resources:
- Blue-green maintains two complete application environments and moves traffic in one step.
- Canary runs most Pods on the stable version and a smaller set on the new version at the same time.
Kubernetes provides Deployments, labels, and Services. Blue-green and canary are patterns you implement with those primitives, not separate workload types.
Prepare the Example Application
Two image arguments identify the version in responses:
v1— current stable textv2— new release text
Use consistent labels on every Pod template:
app: web
version: blue # or green in the green DeploymentFor the canary section, stable and canary Deployments share app: web and differ on track:
app: web
track: stable # or canaryThe echo server listens on port 5678. Services map port 80 to that container port.
Blue-Green Deployment in Kubernetes
Blue runs the current production version. Green runs the new version beside it. You establish blue as production first, verify green on a temporary Service, then switch production traffic only after green is healthy.
Flow before cutover:
Service (selector: blue) → Blue Deployment Pods
After cutover:
Service (selector: green) → Green Deployment Pods
Blue Pods stay running for fast rollback.
Deploy Blue and Route Production Traffic
Save web-blue.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-blue
namespace: strategy-lab
spec:
replicas: 2
selector:
matchLabels:
app: web
version: blue
template:
metadata:
labels:
app: web
version: blue
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0.0
args: ["-text=v1"]
ports:
- containerPort: 5678Create the namespace and blue Deployment:
kubectl create namespace strategy-labkubectl apply -f web-blue.yamlkubectl wait --for=condition=Available deployment/web-blue -n strategy-lab --timeout=120sSave web-service-blue.yaml and route production traffic to blue:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: strategy-lab
spec:
selector:
app: web
version: blue
ports:
- port: 80
targetPort: 5678kubectl apply -f web-service-blue.yamlWait until two ready blue endpoints back the Service:
until [ "$(kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.targetRef.name}{"\n"}{end}' |
grep -c '^web-blue-')" -eq 2 ]; do
sleep 1
doneCheck which Pods back the web Service:
kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[*]}{.targetRef.name}{"\t"}{.addresses[0]}{"\tready="}{.conditions.ready}{"\n"}{end}'Sample output (Pod names and addresses will differ):
web-blue-7cbd7d75d8-k7m9p 192.168.5.6 ready=true
web-blue-7cbd7d75d8-x2r4q 192.168.5.7 ready=trueVerify that web returns v1:
kubectl run probe-blue \
-n strategy-lab \
--image=busybox:1.36 \
--restart=Never \
--rm \
-i \
--command \
-- sh \
-c 'until wget \
-qO- http://web; do sleep 1; done'Sample output:
v1
pod "probe-blue" deletedFor how Services build backend lists from selectors, see Kubernetes Services.
Deploy and Validate Green
Save web-green.yaml with the new response text and version: green labels:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-green
namespace: strategy-lab
spec:
replicas: 2
selector:
matchLabels:
app: web
version: green
template:
metadata:
labels:
app: web
version: green
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0.0
args: ["-text=v2"]
ports:
- containerPort: 5678kubectl apply -f web-green.yamlkubectl wait --for=condition=Available deployment/web-green -n strategy-lab --timeout=120sVerify green on a temporary Service before touching production traffic. Save this manifest as web-green-test.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-green-test
namespace: strategy-lab
spec:
selector:
app: web
version: green
ports:
- port: 80
targetPort: 5678kubectl apply -f web-green-test.yamlkubectl run verify-green \
-n strategy-lab \
--image=busybox:1.36 \
--restart=Never \
--rm \
-i \
--command \
-- sh \
-c 'until wget \
-qO- http://web-green-test; do sleep 1; done'Sample output:
v2
pod "verify-green" deletedGreen returns v2 while production Service traffic still points at blue.
kubectl delete service web-green-test -n strategy-labSwitch Production Traffic to Green
Update only the Service selector:
kubectl patch service web -n strategy-lab --type=merge -p '{"spec":{"selector":{"app":"web","version":"green"}}}'Wait until exactly two green endpoints back the Service and no blue endpoints remain:
until kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.targetRef.name}{"\n"}{end}' |
awk '
/^web-green-/ { green++ }
/^web-blue-/ { blue++ }
END { exit !(green == 2 && blue == 0) }
'; do
sleep 1
doneCheck the Service backends again:
kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[*]}{.targetRef.name}{"\t"}{.addresses[0]}{"\tready="}{.conditions.ready}{"\n"}{end}'Sample output (names should begin with web-green-):
web-green-6f8b9c4d5d-abc12 192.168.5.58 ready=true
web-green-6f8b9c4d5d-def34 192.168.5.59 ready=trueBlue Pods still run but are no longer selected.
Verify production now returns v2:
kubectl run probe-green -n strategy-lab --image=busybox:1.36 --restart=Never --rm -i --command -- sh -c '
until response=$(wget -qO- http://web 2>/dev/null) &&
[ "$response" = "v2" ]; do
sleep 1
done
printf "%s\n" "$response"
'Sample output:
v2
pod "probe-green" deletedPod-template labels must match the selector you set on the Service. Plan spec.template.metadata.labels and spec.selector together — see Kubernetes labels and selectors for label matching rules.
Roll Back to Blue
Switch the selector back to blue:
kubectl patch service web -n strategy-lab --type=merge -p '{"spec":{"selector":{"app":"web","version":"blue"}}}'Verify production returns v1 again:
kubectl run probe-rollback -n strategy-lab --image=busybox:1.36 --restart=Never --rm -i --command -- sh -c '
until response=$(wget -qO- http://web 2>/dev/null) &&
[ "$response" = "v1" ]; do
sleep 1
done
printf "%s\n" "$response"
'Sample output:
v1
pod "probe-rollback" deletedRollback is fast because blue Pods were never deleted. This pattern does not rely on kubectl rollout undo; you move traffic with the Service selector.
This walkthrough now leaves production on blue after demonstrating rollback. In a real successful green release, keep blue during the rollback window, then scale down or delete web-blue only after green remains stable.
Canary Deployment in Kubernetes
Most Pods run the stable version. A smaller set runs the new version. One Service selects all Pods with the shared application label, so both tracks receive traffic.
Example replica split:
- Stable Deployment: four replicas (
v1) - Canary Deployment: one replica (
v2)
Traffic split is approximate: ready Pod counts influence distribution, not exact percentages.
Deploy the Stable Version
Remove the blue-green Deployments and Service before the canary lab reuses the same names. Wait until blue and green Pods are gone so the new Service does not select them:
kubectl delete deployment web-blue web-green -n strategy-lab --ignore-not-found=true --cascade=foreground --wait=truekubectl delete service web -n strategy-lab --ignore-not-found=trueSave web-stable.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-stable
namespace: strategy-lab
spec:
replicas: 4
selector:
matchLabels:
app: web
track: stable
template:
metadata:
labels:
app: web
track: stable
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0.0
args: ["-text=v1"]
ports:
- containerPort: 5678kubectl apply -f web-stable.yamlkubectl wait --for=condition=Available deployment/web-stable -n strategy-lab --timeout=120sDeploy the Canary Version
Save web-canary.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-canary
namespace: strategy-lab
spec:
replicas: 1
selector:
matchLabels:
app: web
track: canary
template:
metadata:
labels:
app: web
track: canary
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0.0
args: ["-text=v2"]
ports:
- containerPort: 5678kubectl apply -f web-canary.yamlkubectl wait --for=condition=Available deployment/web-canary -n strategy-lab --timeout=120sRoute Traffic to Both Versions
Save the canary Service manifest as web-service-canary.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: strategy-lab
spec:
selector:
app: web
ports:
- port: 80
targetPort: 5678The selector matches only app: web so both stable and canary Pods receive traffic.
kubectl apply -f web-service-canary.yamlWait until five ready endpoints back the Service:
until [ "$(kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}x{end}' |
wc -c)" -eq 5 ]; do
sleep 1
doneCheck which Pods back the Service:
kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[*]}{.targetRef.name}{"\t"}{.addresses[0]}{"\tready="}{.conditions.ready}{"\n"}{end}'Sample output (Pod names and addresses will differ):
web-stable-7cbd7d75d8-a1b2c 192.168.5.17 ready=true
web-stable-7cbd7d75d8-d3e4f 192.168.5.21 ready=true
web-stable-7cbd7d75d8-g5h6i 192.168.5.37 ready=true
web-stable-7cbd7d75d8-j7k8l 192.168.5.40 ready=true
web-canary-6f8b9c4d5d-m9n0o 192.168.5.45 ready=trueFive ready endpoints means four stable and one canary Pod.
Verify Canary Traffic
Send 50 requests and count how many return each version:
kubectl run probe-canary -n strategy-lab --image=busybox:1.36 --restart=Never --rm -i --command -- sh -c '
until wget -qO- http://web >/dev/null; do sleep 1; done
i=1
while [ "$i" -le 50 ]; do
wget -qO- http://web
i=$((i + 1))
done | sort | uniq -c
'Sample output will vary:
41 v1
9 v2
pod "probe-canary" deletedA Service routes connections across its ready EndpointSlice backends, but it does not guarantee an exact request ratio. One canary Pod among five total Pods does not guarantee exactly twenty percent of requests. Connection reuse and endpoint selection can still skew results.
Promote the Canary Release
Gradual promotion without a service mesh:
- Increase canary replicas before reducing stable replicas.
- Continue validating responses and application behaviour.
- Update the stable Deployment's Pod template to the validated release—in this lab, change
argsfrom-text=v1to-text=v2; in a real release, this normally includes the new image tag. - Wait for the stable rollout to complete.
- Scale the canary Deployment to zero and delete it.
Detailed monitoring stacks are outside this article.
Roll Back the Canary Release
Scale the canary Deployment to zero:
kubectl scale deployment web-canary -n strategy-lab --replicas=0Wait until the canary scale-down completes before inspecting backends:
kubectl rollout status deployment/web-canary -n strategy-lab --timeout=120sSample output:
deployment "web-canary" successfully rolled outWait until no canary target remains:
until ! kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[*]}{.targetRef.name}{"\n"}{end}' |
grep -q '^web-canary-'; do
sleep 1
doneCheck the Service backends:
kubectl get endpointslices \
-n strategy-lab \
-l kubernetes.io/service-name=web \
-o jsonpath='{range .items[*].endpoints[*]}{.targetRef.name}{"\t"}{.addresses[0]}{"\tready="}{.conditions.ready}{"\n"}{end}'Sample output (only web-stable- targets should remain):
web-stable-7cbd7d75d8-a1b2c 192.168.5.17 ready=true
web-stable-7cbd7d75d8-d3e4f 192.168.5.21 ready=true
web-stable-7cbd7d75d8-g5h6i 192.168.5.37 ready=true
web-stable-7cbd7d75d8-j7k8l 192.168.5.40 ready=trueConfirm every response is v1:
kubectl run probe-rollback -n strategy-lab --image=busybox:1.36 --restart=Never --rm -i --command -- sh -c '
until wget -qO- http://web >/dev/null; do sleep 1; done
for i in 1 2 3 4 5; do
wget -qO- http://web
done
'Sample output:
v1
v1
v1
v1
v1
pod "probe-rollback" deletedOr delete the canary Deployment entirely when you do not need to keep it for later testing.
Blue-Green vs Canary Deployment
| Feature | Blue-green | Canary |
|---|---|---|
| New-version exposure | All traffic after switch | Small traffic portion first |
| Environments | Two complete versions | Stable plus smaller canary |
| Rollback | Switch Service selector | Remove or scale down canary |
| Extra capacity | Usually high | Usually lower |
| Traffic control | Service selector switch | Approximate through Pod ratios |
| Best suited for | Fast cutover and rollback | Gradual validation |
When to Use Each Strategy
Use blue-green when:
- A full parallel environment can run in the cluster
- You want immediate cutover after verification
- Selector-based rollback must be fast
Use canary when:
- The new version should receive a small share of traffic first
- You will observe behaviour gradually
- Exact traffic percentages are not required from standard Service routing
This standard Service example cannot target a specific user audience.
Use a normal rolling update when:
- Pods are interchangeable
- Gradual Pod replacement through one Deployment is enough
- Running separate live versions is unnecessary
Common Problems
kubectl describe service web -n strategy-labkubectl get endpointslices -n strategy-lab -l kubernetes.io/service-name=webkubectl get pods -n strategy-lab --show-labels| Symptom | Likely cause | Fix |
|---|---|---|
| Service still routes to old version | Selector still matches old labels | Update spec.selector; confirm Pod template labels |
| Service has no endpoints after switch | Green Pods not Ready or labels mismatch | Match spec.template.metadata.labels to the new selector |
| Canary traffic not as expected | Pod ratios are approximate; connection reuse | Treat counts as rough; use a mesh or ingress weights for exact splits |
| Changing Deployment metadata does not affect routing | Services select Pod-template labels, not Deployment object labels | Point the Service at existing Pod-template labels, or create a separate Deployment whose selector and Pod-template labels match; do not change only metadata.labels |
| Blue removed too early | Rollback window closed | Keep the old Deployment until the release is stable |
What's Next
- Deploy Applications on Kubernetes with Helm
- Kubernetes Bases, Overlays and Patches
- Manage Kubernetes Resources with apply, edit, patch and replace
References
Summary
You ran blue-green and canary releases without a separate controller. Blue-green keeps two Deployments online and moves production by changing the Service selector from version: blue to version: green, with blue Pods still available for a selector rollback. Canary keeps stable and canary Deployments behind one Service that selects only app: web, so traffic splits roughly by ready Pod count.
Neither pattern replaces Deployment rolling updates for routine image changes. Blue-green trades extra capacity for instant cutover. Canary trades precision for simplicity on a standard ClusterIP Service. Plan Pod-template labels before you write Services, and verify the inactive environment before you switch selectors.

