Kubernetes Blue-Green and Canary Deployments with Examples

Learn Kubernetes blue-green and canary deployments with YAML examples. Switch Service selectors, test mixed traffic, promote releases, and roll back.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Kubernetes blue-green and canary deployment patterns with two Deployments and a Service selector switch
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 text
  • v2 — new release text

Use consistent labels on every Pod template:

yaml
app: web
version: blue   # or green in the green Deployment

For the canary section, stable and canary Deployments share app: web and differ on track:

yaml
app: web
track: stable   # or canary

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

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

Create the namespace and blue Deployment:

bash
kubectl create namespace strategy-lab
bash
kubectl apply -f web-blue.yaml
bash
kubectl wait --for=condition=Available deployment/web-blue -n strategy-lab --timeout=120s

Save web-service-blue.yaml and route production traffic to blue:

yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: strategy-lab
spec:
  selector:
    app: web
    version: blue
  ports:
    - port: 80
      targetPort: 5678
bash
kubectl apply -f web-service-blue.yaml

Wait until two ready blue endpoints back the Service:

bash
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
done

Check which Pods back the web Service:

bash
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):

output
web-blue-7cbd7d75d8-k7m9p    192.168.5.6    ready=true
web-blue-7cbd7d75d8-x2r4q    192.168.5.7    ready=true

Verify that web returns v1:

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

output
v1
pod "probe-blue" deleted

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

yaml
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: 5678
bash
kubectl apply -f web-green.yaml
bash
kubectl wait --for=condition=Available deployment/web-green -n strategy-lab --timeout=120s

Verify green on a temporary Service before touching production traffic. Save this manifest as web-green-test.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: web-green-test
  namespace: strategy-lab
spec:
  selector:
    app: web
    version: green
  ports:
    - port: 80
      targetPort: 5678
bash
kubectl apply -f web-green-test.yaml
bash
kubectl 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:

output
v2
pod "verify-green" deleted

Green returns v2 while production Service traffic still points at blue.

bash
kubectl delete service web-green-test -n strategy-lab

Switch Production Traffic to Green

Update only the Service selector:

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

bash
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
done

Check the Service backends again:

bash
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-):

output
web-green-6f8b9c4d5d-abc12    192.168.5.58    ready=true
web-green-6f8b9c4d5d-def34    192.168.5.59    ready=true

Blue Pods still run but are no longer selected.

Verify production now returns v2:

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

output
v2
pod "probe-green" deleted

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

bash
kubectl patch service web -n strategy-lab --type=merge -p '{"spec":{"selector":{"app":"web","version":"blue"}}}'

Verify production returns v1 again:

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

output
v1
pod "probe-rollback" deleted

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

bash
kubectl delete deployment web-blue web-green -n strategy-lab --ignore-not-found=true --cascade=foreground --wait=true
bash
kubectl delete service web -n strategy-lab --ignore-not-found=true

Save web-stable.yaml:

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: 5678
bash
kubectl apply -f web-stable.yaml
bash
kubectl wait --for=condition=Available deployment/web-stable -n strategy-lab --timeout=120s

Deploy the Canary Version

Save web-canary.yaml:

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: 5678
bash
kubectl apply -f web-canary.yaml
bash
kubectl wait --for=condition=Available deployment/web-canary -n strategy-lab --timeout=120s

Route Traffic to Both Versions

Save the canary Service manifest as web-service-canary.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: strategy-lab
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 5678

The selector matches only app: web so both stable and canary Pods receive traffic.

bash
kubectl apply -f web-service-canary.yaml

Wait until five ready endpoints back the Service:

bash
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
done

Check which Pods back the Service:

bash
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):

output
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=true

Five ready endpoints means four stable and one canary Pod.

Verify Canary Traffic

Send 50 requests and count how many return each version:

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

output
41 v1
      9 v2
pod "probe-canary" deleted

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

  1. Increase canary replicas before reducing stable replicas.
  2. Continue validating responses and application behaviour.
  3. Update the stable Deployment's Pod template to the validated release—in this lab, change args from -text=v1 to -text=v2; in a real release, this normally includes the new image tag.
  4. Wait for the stable rollout to complete.
  5. 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:

bash
kubectl scale deployment web-canary -n strategy-lab --replicas=0

Wait until the canary scale-down completes before inspecting backends:

bash
kubectl rollout status deployment/web-canary -n strategy-lab --timeout=120s

Sample output:

output
deployment "web-canary" successfully rolled out

Wait until no canary target remains:

bash
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
done

Check the Service backends:

bash
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):

output
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

Confirm every response is v1:

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

output
v1
v1
v1
v1
v1
pod "probe-rollback" deleted

Or 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

bash
kubectl describe service web -n strategy-lab
bash
kubectl get endpointslices -n strategy-lab -l kubernetes.io/service-name=web
bash
kubectl 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


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.


Frequently Asked Questions

1. What is the difference between blue-green and canary deployment in Kubernetes?

Blue-green runs two full environments and switches all Service traffic at once by changing selectors. Canary runs stable and canary Pods behind one Service so both versions receive traffic in rough proportion to ready Pod counts.

2. How do I switch traffic in a Kubernetes blue-green deployment?

Update the Service spec.selector to match the new environment labels, such as changing version from blue to green. Endpoints update to the green Pod IPs while blue Pods keep running for rollback.

3. Does one canary Pod receive exactly twenty percent of traffic?

No. A standard ClusterIP Service balances across ready endpoints; one canary Pod among five total Pods is only an approximate one-fifth share. Connection reuse and kube-proxy behavior can skew what you observe.

4. Should I use kubectl rollout undo for blue-green rollback?

Not as the primary method. Blue-green rollback means changing the Service selector back to the previous label set while the old Deployment still runs. rollout undo applies to a single Deployment Pod template history.

5. Why does my Service have no endpoints after a selector change?

Pod template labels under spec.template.metadata.labels must match the new Service selector. Changing only the Deployment object metadata.labels does not relabel running Pods.

6. When should I use a normal rolling update instead?

Use a Deployment rolling update when Pods are interchangeable, gradual Pod replacement is enough, and you do not need two live versions or selector-based cutover.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)