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:
- How to Set Up Canary Analysis with Automated Rollback
- How to Set Up Horizontal Pod Autoscaling in Kubernetes
Sources: