Skip to content
SRE & DevOpsHow-To Published Updated 5 min readViews unavailable

How to Implement Blue-Green and Canary Deployments in Kubernetes

A practical Kubernetes guide to blue-green cutovers and cautious canary releases, with health gates, rollback checks, and routing caveats.

Both blue-green and canary deployments solve the same underlying problem — reducing the blast radius of a bad release — through different mechanisms. This walks through a complete, working setup for each using plain Kubernetes primitives, no additional tooling required.

Blue-Green: two full environments, instant switch

Step 1: deploy the current version as “blue”

# deployment-blue.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-blue
spec:
  replicas: 3
  selector:
    matchLabels: {app: myapp, version: blue}
  template:
    metadata:
      labels: {app: myapp, version: blue}
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp@sha256:BLUE_IMAGE_DIGEST
          readinessProbe:
            httpGet: {path: /ready, port: 8080}
            periodSeconds: 5

Step 2: point the live Service at blue

apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
    version: blue      # <- this line controls which version receives traffic
  ports:
    - port: 80
      targetPort: 8080

Step 3: deploy the new version as “green,” fully, alongside blue

# deployment-green.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-green
spec:
  replicas: 3
  selector:
    matchLabels: {app: myapp, version: green}
  template:
    metadata:
      labels: {app: myapp, version: green}
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp@sha256:GREEN_IMAGE_DIGEST
          readinessProbe:
            httpGet: {path: /ready, port: 8080}
            periodSeconds: 5
kubectl apply -f deployment-green.yaml

Green runs fully, receiving zero production traffic, while you test it directly (port-forwarding to it, or a separate internal test Service pointed at version: green).

Do not test only the process-level health endpoint. Exercise the same authentication, dependencies, cache paths, and database reads that real requests use. A small internal Service makes this repeatable:

apiVersion: v1
kind: Service
metadata:
  name: myapp-green-preview
spec:
  selector: {app: myapp, version: green}
  ports: [{port: 80, targetPort: 8080}]

Before switching, confirm that every green replica is Ready and that the Service has only green endpoints:

kubectl rollout status deployment/myapp-green
kubectl get endpointslices -l kubernetes.io/service-name=myapp-green-preview

Step 4: switch traffic instantly

kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'

New connections now resolve to green endpoints without a rolling Deployment transition. Existing keep-alive connections, in-flight requests, DNS caches outside the cluster, and terminating endpoints can make the observed cutover less than mathematically instantaneous. Watch both versions until the longest legitimate request has drained.

Step 5: roll back just as instantly, if needed

kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'

Since blue never stopped running, rollback is exactly as instant as the cutover was — the old version was never scaled down.

Step 6: clean up the old version once confident

kubectl delete deployment myapp-blue

Canary: gradual, traffic-percentage-based rollout

Step 1: run stable and canary as separate deployments

apiVersion: apps/v1
kind: Deployment
metadata: {name: myapp-stable}
spec:
  replicas: 9
  selector: {matchLabels: {app: myapp, track: stable}}
  template:
    metadata: {labels: {app: myapp, track: stable}}
    spec: {containers: [{name: myapp, image: "registry.example.com/myapp@sha256:STABLE_IMAGE_DIGEST"}]}
apiVersion: apps/v1
kind: Deployment
metadata: {name: myapp-canary}
spec:
  replicas: 1
  selector: {matchLabels: {app: myapp, track: canary}}
  template:
    metadata: {labels: {app: myapp, track: canary}}
    spec: {containers: [{name: myapp, image: "registry.example.com/myapp@sha256:CANARY_IMAGE_DIGEST"}]}

Step 2: one Service selecting both, by the shared label only

apiVersion: v1
kind: Service
metadata: {name: myapp}
spec:
  selector: {app: myapp}      # matches BOTH stable and canary pods
  ports: [{port: 80, targetPort: 8080}]

Because the Service selects on app: myapp alone, both versions become endpoints. A 9:1 replica ratio changes endpoint capacity, but it is not a dependable promise that exactly 10% of requests reach the canary. Persistent connections, HTTP/2 multiplexing, client behavior, topology-aware routing, readiness changes, and uneven pod capacity can all skew the result. Treat this technique as a coarse exposure mechanism only.

For an enforceable traffic percentage, use an implementation that supports weighted routing, such as a conformant Gateway API controller or a service mesh. Keep separate stable and canary Services and configure the routing layer with explicit backend weights; the exact resource depends on the installed controller and its supported Gateway API version.

Step 3: monitor the canary closely

kubectl logs -l track=canary --tail=100 -f

Watch request volume, error rate, latency percentiles, saturation, restarts, and application-specific outcomes for each version. A canary that receives almost no requests cannot prove anything, while a healthy aggregate can hide a severe canary regression. Label telemetry with a stable release identifier and compare equal time windows.

Step 4: gradually shift the ratio

kubectl scale deployment myapp-canary --replicas=3
kubectl scale deployment myapp-stable --replicas=7

Incrementally rebalance the replica counts toward the canary as confidence grows, watching metrics at each step.

Step 5: complete or abort the rollout

To complete: deploy the tested image digest as the new stable version, wait for readiness and then remove the old workload. To abort: first remove the canary from routing, wait for in-flight traffic to drain, and only then scale it to zero. Do not rely on a rollback to reverse destructive schema migrations, irreversible writes, or messages already emitted; design those changes to remain compatible with both versions during the observation window.

Production checklist before either cutover

  • Pin images by digest and retain the previous digest so the rollback target cannot move.
  • Define readiness, startup, and graceful shutdown behavior; a process that started is not necessarily ready for traffic.
  • Establish measurable success criteria and a maximum observation period before beginning.
  • Verify capacity: blue-green temporarily doubles the application fleet, while a canary must not reduce stable capacity below safe headroom.
  • Test both the forward change and rollback in a non-production environment, including database compatibility.
  • Record who can change the Service or routing object, and capture the change in version control.

Choosing between them

Blue-green is the right choice when you need an instant, all-or-nothing cutover and rollback — appropriate for changes you’re fairly confident about but want a fast escape hatch for. Canary is the right choice when you want to limit exposure to a smaller fraction of real traffic first, catching a problem before it affects everyone, at the cost of a more gradual, closely-monitored rollout process. Production systems handling significant traffic commonly use a proper service mesh (Istio, Linkerd) for precise percentage-based canary routing rather than the replica-ratio approximation shown here — this walkthrough uses only plain Kubernetes primitives specifically to show the underlying mechanism without additional infrastructure dependencies.

Related:

Sources:

Comments