
Deploying software directly to your entire user base turns every unexpected bug or performance regression into an immediate, high-stakes incident. Rather than risking full-scale service disruptions with all-at-once releases, progressive delivery enables engineering teams to deploy a new application version alongside the existing baseline, directing only a tiny percentage of live user requests to the update while closely tracking system health. Practicing this strategy in an isolated sandbox helps engineers build hands-on familiarity with traffic routing mechanisms, telemetry evaluation, and fail-safe recovery procedures before managing live workloads. For engineers and teams looking to sharpen these capabilities, DevOpsSchool provides practical educational pathways covering Kubernetes management, deployment automation, and continuous delivery patterns. Learning to manage controlled traffic splits and immediate rollbacks in a local laboratory gives you the operational confidence needed to protect production systems and release software with minimal risk.
Understand the Canary Deployment Model
To implement canary releases effectively, you need a clear mental model of the lifecycle, traffic mechanics, and architectural trade-offs compared to other deployment strategies.
The Core Canary Workflow
A canary release follows an iterative operational loop:
+-------------------------------------------------------------------------+
| Canary Rollout Cycle |
| |
| [ Deploy Canary v2 ] ──► [ Route Initial Traffic (e.g., 5-10%) ] |
| │ |
| ▼ |
| [ Observe System Telemetry ] |
| - HTTP Error Rate (5xx) |
| - P95 / P99 Latency |
| - Resource Usage & Restarts |
| │ |
| ┌──────────────────┴──────────────────┐ |
| ▼ ▼ |
| [ Health Checks Pass ] [ Error Threshold Hit ] |
| │ │ |
| ▼ ▼ |
| [ Increase Traffic Increment ] [ Terminate Canary (0%) ] |
| (25% -> 50% -> 100%) │ |
| │ ▼ |
| ▼ [ Revert to Stable v1 ] |
| [ Retire Old Stable Version ] |
+-------------------------------------------------------------------------+
- Deploy Stable Baseline (v1): The current, trusted production version runs normally and handles 100% of production traffic.
- Deploy Canary (v2): The new release is deployed into the same network space with identical environmental configurations, but receives zero traffic initially.
- Route Canary Traffic: A small slice of traffic (e.g., 5% to 10%) is diverted to the canary release.
- Telemetry Observation: Telemetry systems collect and compare metrics between the baseline (v1) and the canary (v2) across error rates, response latencies, and resource utilization.
- Progressive Promotion: If metrics remain within acceptable bounds over an observation window, traffic shifts incrementally (e.g., 10% → 25% → 50% → 100%).
- Promotion or Rollback: Upon reaching 100% traffic without degradation, the canary becomes the new stable baseline and the old version is decommissioned. If the canary breaches any health threshold, routing rules instantly snap back to 100% on the stable version, and the canary instances are terminated.
Comparing Release Strategies
Choosing the right deployment pattern depends on statefulness, infrastructure overhead, and tolerance for downtime:
| Strategy | Traffic Shift Method | Resource Overhead | Rollback Speed | Best Used When… |
| Rolling Update | Pod-by-pod replacement behind a standard service load balancer | Low (Temporary burst of 1–2 pods) | Moderate (Requires reverse rolling update) | Updates are low-risk, stateless, and backward-compatible with no extra infrastructure cost. |
| Blue-Green | Instant cutover (0% to 100%) via router or DNS change | High (Requires 200% peak capacity to run duplicate fleets) | Near Instant (Flip router back to Blue environment) | You require zero downtime cutovers and absolute isolation, but can afford running duplicate production environments. |
| Canary | Fine-grained percentage-based or header-based traffic routing | Low to Moderate (Requires only enough headroom for small canary subset) | Near Instant (Re-route canary percentage back to 0%) | Releases carry performance or business logic risks that require validation under live production traffic. |
Prepare a Lab Environment Before the First Release
Before diving into deployment manifests, set up a dedicated local lab environment that mirrors cloud-native production primitives.
+-------------------------------------------------------------------------+
| Local Lab Machine |
| |
| [ Docker Engine / Container Runtime ] |
| │ |
| ▼ |
| [ Local Kubernetes Cluster (Minikube / kind) ] |
| │ |
| ├── Ingress Controller (NGINX) ──► Traffic Splitting Hook |
| ├── Application Workloads ──► Stable v1 & Canary v2 Pods |
| └── Observability Stack ──► Prometheus & Grafana |
+-------------------------------------------------------------------------+
Lab Component Breakdown
- Container Engine (Docker or Podman): Compiles application source code, packs runtime dependencies, and produces container images.
- Local Kubernetes Cluster (
kindorMinikube): Provides a standard Kubernetes API runtime locally without incurring cloud bills. - NGINX Ingress Controller: Acts as the layer-7 HTTP reverse proxy capable of dynamic, weight-based request routing.
- Application Workloads: Simple web services exposing version metadata and configurable latency/fault injection endpoints.
- Monitoring Stack (Prometheus & Grafana): Gathers container metrics and request statistics to validate rollout health.
Beginner Note: For your initial experiments, an Ingress controller and basic Kubernetes resources are all you need. You can inspect responses directly using curl. You can add automated metrics scraping with Prometheus and Grafana once you are comfortable with basic traffic splitting.
Setting Up the Cluster
Ensure you have installed docker, kubectl, and kind (Kubernetes in Docker). Create a cluster configuration file named kind-canary-cluster.yaml that exposes ports 80 and 443 to your host machine:
YAML
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
Deploy the cluster and install the NGINX Ingress Controller:
Bash
# 1. Create the kind cluster
kind create cluster --config kind-canary-cluster.yaml --name canary-lab
# 2. Deploy NGINX Ingress Controller configured for kind
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# 3. Wait for the Ingress Controller to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s
Expected Result: The command completes with a message stating that the ingress-nginx controller pod condition is met (condition=ready), indicating that your local cluster can now accept and route external HTTP traffic on port 80.
Lab Exercise One: Deploy the Stable Application
With your cluster online, build and run the baseline stable application (v1.0.0).
Step 1: Write the Application
Create a simple Node.js application in an empty directory named app/:
JavaScript
// app/server.js
const http = require('http');
const os = require('os');
const PORT = 8080;
const VERSION = process.env.APP_VERSION || "v1.0.0";
const COLOR = process.env.APP_COLOR || "blue";
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ status: 'HEALTHY', version: VERSION }));
}
// Simulate minimal baseline latency (20ms)
setTimeout(() => {
res.writeHead(200, {
'Content-Type': 'application/json',
'X-App-Version': VERSION
});
res.end(JSON.stringify({
message: "Request successfully processed",
version: VERSION,
color: COLOR,
hostname: os.hostname()
}));
}, 20);
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT} as version ${VERSION}`);
});
Create a standard Dockerfile:
Dockerfile
# app/Dockerfile
FROM node:18-alpine
WORKDIR /usr/src/app
COPY server.js .
EXPOSE 8080
USER node
CMD ["node", "server.js"]
Step 2: Build the Stable Image
Bash
docker build -t canary-demo:v1.0.0 ./app
kind load docker-image canary-demo:v1.0.0 --name canary-lab
Step 3: Define Kubernetes Manifests for the Stable Release
Create a file named stable-deployment.yaml:
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-stable
labels:
app: demo-app
track: stable
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
track: stable
template:
metadata:
labels:
app: demo-app
track: stable
spec:
containers:
- name: web
image: canary-demo:v1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "v1.0.0"
- name: APP_COLOR
value: "blue"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
resources:
limits:
cpu: "200m"
memory: "128Mi"
requests:
cpu: "50m"
memory: "64Mi"
---
apiVersion: v1
kind: Service
metadata:
name: app-stable-service
labels:
app: demo-app
track: stable
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
selector:
app: demo-app
track: stable
Apply the stable manifest:
Bash
kubectl apply -f stable-deployment.yaml
Step 4: Configure the Production Ingress
Create production-ingress.yaml to expose the stable service under the virtual host demo.local:
YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress-production
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-stable-service
port:
number: 80
Apply the Ingress:
Bash
kubectl apply -f production-ingress.yaml
Step 5: Verify Application Health
Test the stable application by sending a request through your local Ingress controller using the configured host header:
Bash
curl -i -H "Host: demo.local" http://localhost/
Expected Result:
JSON
HTTP/1.1 200 OK
Content-Type: application/json
X-App-Version: v1.0.0
{"message":"Request successfully processed","version":"v1.0.0","color":"blue","hostname":"app-stable-78486955d7-4k2lb"}
Every request routes exclusively to the app-stable deployment, returning version: v1.0.0.
Lab Exercise Two: Introduce the Canary Version
Now, build and deploy an updated version (v2.0.0) alongside the stable release without exposing it to incoming production traffic yet.
Step 1: Build the Canary Container Image
Update the application behavior to reflect version v2.0.0. Build and load it into your local kind cluster:
Bash
docker build --build-arg VERSION=v2.0.0 -t canary-demo:v2.0.0 ./app
kind load docker-image canary-demo:v2.0.0 --name canary-lab
Step 2: Create the Canary Kubernetes Resources
Create canary-deployment.yaml. Notice that the canary maintains distinct deployment and service labels (track: canary), isolating it from the stable version:
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-canary
labels:
app: demo-app
track: canary
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
track: canary
template:
metadata:
labels:
app: demo-app
track: canary
spec:
containers:
- name: web
image: canary-demo:v2.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "v2.0.0"
- name: APP_COLOR
value: "green"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
resources:
limits:
cpu: "200m"
memory: "128Mi"
requests:
cpu: "50m"
memory: "64Mi"
---
apiVersion: v1
kind: Service
metadata:
name: app-canary-service
labels:
app: demo-app
track: canary
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
selector:
app: demo-app
track: canary
Apply the deployment:
Bash
kubectl apply -f canary-deployment.yaml
Verify that both pods run simultaneously:
Bash
kubectl get pods -l app=demo-app --show-labels
Expected Result:
Plaintext
NAME READY STATUS RESTARTS AGE LABELS
app-canary-5c94bb5fbb-5x2kz 1/1 Running 0 18s app=demo-app,track=canary
app-stable-78486955d7-4k2lb 1/1 Running 0 5m app=demo-app,track=stable
app-stable-78486955d7-m6df8 1/1 Running 0 5m app=demo-app,track=stable
app-stable-78486955d7-w99xp 1/1 Running 0 5m app=demo-app,track=stable
Step 3: Validate Canary Isolation
Test production traffic again:
Bash
curl -s -H "Host: demo.local" http://localhost/ | grep -o '"version":"[^"]*"'
Output:
Plaintext
"version":"v1.0.0"
Even though app-canary is fully operational in the cluster, production traffic continues to route entirely to v1.0.0 because the Ingress points exclusively to app-stable-service.
Lab Exercise Three: Control How Traffic Reaches the Canary
Directing a small, controlled amount of traffic to your canary requires choosing a routing strategy that fits your architectural requirements.
Traffic Routing Strategies:
A. Replica Ratio (Crude):
[Service (Random Round-Robin)]
├── Pod v1 (Stable)
├── Pod v1 (Stable) ──► 3/4 Requests (75%)
├── Pod v1 (Stable)
└── Pod v2 (Canary) ──► 1/4 Requests (25% minimum floor)
B. Ingress Weighting (L7 Controlled):
[NGINX Ingress Controller]
├── (Weight: 90) ──► [Stable Service] ──► Pods v1 (90%)
└── (Weight: 10) ──► [Canary Service] ──► Pods v2 (10%)
Traffic Routing Mechanisms
- Replica-Based Distribution (Crude): Point a single Kubernetes service selector at a shared label (
app: demo-app). If you run 9 stable pods and 1 canary pod, random round-robin routing will send roughly 10% of requests to the canary.- Limitation: Highly inaccurate. The lowest possible traffic percentage is bounded by your total pod count ($1/N$). Maintaining 99 stable pods just to achieve a 1% canary split is inefficient and impractical.
- Ingress-Based Traffic Splitting (Recommended for Labs): An Ingress controller (such as NGINX) evaluates HTTP requests at Layer 7 and splits traffic between distinct services based on defined percentage weights or header flags.
- Service Mesh Routing (Advanced): Tools like Istio, Linkerd, or Envoy sit transparently beside every container, enabling dynamic percentage-based splits, header inspections, and distributed tracing.
- Gateway API: The modern Kubernetes Gateway API offers standard HTTPRoute resources that support built-in weight-based routing natively across infrastructure providers.
Implementing an Ingress-Based 90/10 Canary Split
To route 10% of traffic to the canary using NGINX Ingress, apply a companion Ingress manifest that references the identical host name and uses special annotations:
nginx.ingress.kubernetes.io/canary: "true"nginx.ingress.kubernetes.io/canary-weight: "10"
Create canary-ingress-split.yaml:
YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress-canary
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-canary-service
port:
number: 80
Apply the routing manifest:
Bash
kubectl apply -f canary-ingress-split.yaml
Verifying the Traffic Split
Run a bash loop sending 100 requests to your application endpoint, counting how many times each version responds:
Bash
for i in {1..100}; do
curl -s -H "Host: demo.local" http://localhost/ | grep -o '"version":"[^"]*"'
done | sort | uniq -c
Expected Result:
Plaintext
91 "version":"v1.0.0"
9 "version":"v2.0.0"
The NGINX Ingress controller dynamically directs approximately 10% of requests to the canary pod, keeping the remaining 90% safely on the stable baseline.
Observe the Canary Before Increasing Traffic
Never increase traffic to a canary without first verifying its operational health through quantitative telemetry.
Observability Evaluation:
Stable (v1) Canary (v2) Threshold
────────────── ────────────── ─────────────
[ HTTP 5xx Error Rate ] 0.01% 0.02% < 0.5% --> PASS
[ P95 Response Latency ] 22 ms 24 ms < 50 ms --> PASS
[ Memory Consumption ] 64 MiB 68 MiB < 100 MiB --> PASS
[ Unhandled Pod Restarts ] 0 0 0 --> PASS
Essential Canary Metrics
To assess a canary release objectively, focus on four key operational signals:
- HTTP Error Rate: The percentage of client requests resulting in HTTP 5xx errors. A healthy canary should mirror the baseline error rate (typically < 0.1%).
- Request Latency (P95/P99): The 95th and 99th percentile response durations. Increases here often reveal database lock contention, memory leaks, or inefficient code paths that slip past synthetic tests.
- Resource Saturation (CPU & Memory): Steady increases in memory usage under constant traffic indicate memory leaks, while unexpected CPU spikes point to unoptimized loops or serialization bottlenecks.
- Container Restarts and System Logs: Look for unexpected crashes or OOMKilled events via
kubectl get pods, and inspect application logs for unhandled exceptions usingkubectl logs -l track=canary.
Practical Lab Inspection Commands
If Prometheus is not yet set up in your lab, monitor basic pod health and system metrics using standard kubectl commands:
Bash
# 1. Verify zero container crashes or restarts
kubectl get pods -l app=demo-app
# 2. Check live resource consumption
kubectl top pods -l app=demo-app
# 3. Stream canary runtime logs
kubectl logs -l track=canary -f --tail=20
When Prometheus is configured, monitor request rates and error distributions using PromQL queries:
Code snippet
# Canary HTTP 5xx error rate over 2-minute rolling windows
sum(rate(nginx_ingress_controller_requests{status=~"5.*", exported_service="app-canary-service"}[2m]))
/
sum(rate(nginx_ingress_controller_requests{exported_service="app-canary-service"}[2m])) * 100
If the canary error rate stays below your predefined threshold (e.g., < 0.5%) and P95 latency is within 10% of the stable baseline throughout the evaluation window, the release is healthy and ready for more traffic.
Lab Exercise Four: Increase Traffic Gradually
Progressive delivery is built around disciplined, staged exposure. Rather than jumping straight from 10% to 100%, step up traffic in controlled tiers. This allows caching layers, connection pools, and backend dependencies to adjust under real-world loads.
Traffic Promotion Stages:
Stage 1: 10% Canary ──[ Monitor 5m: Stable ]──►
Stage 2: 25% Canary ──[ Scale Canary: 2 Pods ]──► [ Monitor 5m: Stable ]──►
Stage 3: 50% Canary ──[ Scale Canary: 3 Pods ]──► [ Monitor 5m: Stable ]──►
Stage 4: 100% Promo ──[ Canary Becomes Stable ]──► [ Decommission Old Stable ]
Step 1: Scale Canary Replicas
Before routing more traffic to your canary, ensure it has enough computing capacity to handle the increased load. Increase app-canary from 1 pod to 2:
Bash
kubectl scale deployment app-canary --replicas=2
Step 2: Increase Traffic Split to 25%
Update the nginx.ingress.kubernetes.io/canary-weight annotation on your canary Ingress resource:
Bash
kubectl annotate ingress app-ingress-canary \
nginx.ingress.kubernetes.io/canary-weight="25" --overwrite
Validate the new traffic distribution:
Bash
for i in {1..100}; do
curl -s -H "Host: demo.local" http://localhost/ | grep -o '"version":"[^"]*"'
done | sort | uniq -c
Expected Result:
Plaintext
74 "version":"v1.0.0"
26 "version":"v2.0.0"
Approximately one out of every four requests now lands on the canary release.
Step 3: Advance to 50%
Scale the canary deployment to match the stable deployment’s capacity:
Bash
kubectl scale deployment app-canary --replicas=3
kubectl annotate ingress app-ingress-canary \
nginx.ingress.kubernetes.io/canary-weight="50" --overwrite
Step 4: Promote to 100% and Clean Up
Once the canary has operated reliably at 50% traffic through your validation period, promote it to become the new stable baseline:
- Update the base
stable-deployment.yamlmanifest so its container image points tocanary-demo:v2.0.0. - Apply the updated stable deployment:Bash
kubectl set image deployment/app-stable web=canary-demo:v2.0.0 - Remove the canary Ingress routing rule:Bash
kubectl delete ingress app-ingress-canary - Scale down and delete the canary deployment:Bash
kubectl delete -f canary-deployment.yaml
Expected Result: The stable deployment is now running v2.0.0 across all pods, while total traffic routes directly through app-ingress-production. The rollout is complete.
Lab Exercise Five: Break the Canary and Practice Rollback
A release strategy is only as dependable as its rollback path. A core advantage of running canaries in a lab is practicing how to react when a release fails.
Failure Scenario & Rollback Sequence:
1. Canary (v3-faulty) Introduced (10% Traffic)
2. Traffic Loop Injected ──► HTTP 500 Responses Detected
3. Operator Intervenes:
[ Set Ingress Weight = 0% ] ──► (Traffic Instantly Reverts to Stable)
4. Verify Production Impact Confined to Canary Window
5. Terminate Broken Canary Deployment
Step 1: Build an Unstable Canary Image (v3-faulty)
Create a faulty version of the application that triggers HTTP 500 errors on roughly 50% of incoming requests:
JavaScript
// Add error simulation inside server.js
if (VERSION === "v3.0.0-faulty") {
if (Math.random() > 0.5) {
res.writeHead(500, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
error: "Internal Server Error: Uncaught runtime exception",
version: VERSION
}));
}
}
Build and load the faulty image into kind:
Bash
docker build --build-arg VERSION=v3.0.0-faulty -t canary-demo:v3-faulty ./app
kind load docker-image canary-demo:v3-faulty --name canary-lab
Step 2: Deploy the Faulty Canary at 10% Traffic
Deploy the broken canary:
YAML
# faulty-canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-canary
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
track: canary
template:
metadata:
labels:
app: demo-app
track: canary
spec:
containers:
- name: web
image: canary-demo:v3-faulty
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: APP_VERSION
value: "v3.0.0-faulty"
- name: APP_COLOR
value: "red"
Apply the deployment and recreate the canary Ingress rule set to 10%:
Bash
kubectl apply -f faulty-canary.yaml
kubectl apply -f canary-ingress-split.yaml
Step 3: Run Traffic and Observe the Failure
Simulate 50 incoming requests and inspect the returned HTTP status codes:
Bash
for i in {1..50}; do
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: demo.local" http://localhost/
done | sort | uniq -c
Expected Result:
Plaintext
45 200
5 500
Notice the impact: because the canary was restricted to a 10% traffic split, only 5 of the 50 requests failed. In a traditional big-bang deployment, 25 or more of those requests would have failed across your user base.
Step 4: Execute an Immediate Traffic Cutoff (Rollback)
To stop the bleeding, immediately drop the canary traffic weight to 0 or remove the canary Ingress rule altogether:
Bash
# Option A: Instantly sever canary traffic
kubectl delete ingress app-ingress-canary
# Option B: Alternatively, set the weight annotation to "0"
# kubectl annotate ingress app-ingress-canary nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
Re-test the traffic immediately:
Bash
for i in {1..30}; do
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: demo.local" http://localhost/
done | sort | uniq -c
Expected Result:
Plaintext
30 200
Production stability is restored in seconds. With traffic safely diverted, decommission the faulty canary deployment and investigate the root cause offline using its pod logs:
Bash
kubectl logs deployment/app-canary --tail=50
kubectl delete deployment app-canary
Automate Canary Deployments Through CI/CD
Manually adjusting Ingress annotations and running curl loops works well for learning, but modern production platforms automate these steps through CI/CD pipelines.
Automated Canary CI/CD Pipeline Architecture:
[Commit / PR]
│
▼
[Stage 1: Build & Unit Test]
│
▼
[Stage 2: Container Security Scan]
│
▼
[Stage 3: Deploy Canary v2 (0% Traffic)]
│
▼
[Stage 4: Route Initial Traffic (10%)]
│
▼
[Stage 5: Automated Metric Analysis]
│
├── Error Rate > 0.5%? ──► [Trigger Automatic Rollback (Weight = 0%)]
│
▼ (Passes)
[Stage 6: Incremental Promotion (25% -> 50% -> 100%)]
│
▼
[Stage 7: Decommission Old Stable]
Declarative Pipeline Example: GitHub Actions
Here is an illustrative GitHub Actions workflow showing how a continuous deployment pipeline automates the validation and promotion sequence:
YAML
name: Progressive Canary Rollout
on:
push:
branches: [ main ]
jobs:
canary-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Container Image
run: |
docker build -t registry.internal/demo-app:${{ github.sha }} ./app
- name: Vulnerability Scan
run: |
echo "Running security vulnerability scan on container image..."
- name: Deploy Canary Deployment (0% Traffic)
run: |
kubectl set image deployment/app-canary web=registry.internal/demo-app:${{ github.sha }}
kubectl rollout status deployment/app-canary --timeout=60s
- name: Shift Traffic to 10%
run: |
kubectl annotate ingress app-ingress-canary \
nginx.ingress.kubernetes.io/canary-weight="10" --overwrite
- name: Automated Metric Analysis (Canary Evaluation)
id: analysis
run: |
echo "Evaluating Prometheus error metrics over 3-minute window..."
# Example query checking for 5xx errors
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=canary_error_rate" | jq '.data.result[0].value[1]')
# Guard condition: Roll back if error rate exceeds 0.5%
if (( $(echo "$ERROR_RATE > 0.5" | bc -l) )); then
echo "Error rate limit breached ($ERROR_RATE%). Triggering rollback!"
exit 1
fi
- name: Rollback on Failure
if: failure()
run: |
echo "Aborting canary rollout. Restoring 100% traffic to stable baseline..."
kubectl annotate ingress app-ingress-canary nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
kubectl scale deployment app-canary --replicas=0
- name: Promote to Full Production (100%)
if: success()
run: |
echo "Metric verification passed. Promoting new version to stable..."
kubectl set image deployment/app-stable web=registry.internal/demo-app:${{ github.sha }}
kubectl rollout status deployment/app-stable
kubectl annotate ingress app-ingress-canary nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
Note: This sample configuration illustrates core pipeline logic. A production-ready pipeline should add secrets management, distributed tracing verifications, and integrated approval checkpoints.
Explore Advanced Canary Deployment Tools
As deployment architectures mature, teams often transition from custom pipeline scripts to specialized progressive delivery controllers.
Advanced Progressive Delivery Ecosystem:
┌──► Argo Rollouts (CRD-Based Kubernetes Controller)
│
[Progressive Tools] ┼──► Flagger (Service Mesh & Ingress Metric Automator)
│
└──► Istio / Linkerd (L7 Mutual TLS & Fine-Grained Telemetry)
Argo Rollouts
Argo Rollouts replaces the native Kubernetes Deployment object with a custom Rollout Custom Resource Definition (CRD). It provides declarative, step-by-step canary release strategies directly within your Kubernetes cluster:
YAML
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: dynamic-canary-rollout
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m }
- setWeight: 30
- pause: { duration: 10m }
- setWeight: 60
Argo Rollouts handles pod scaling, weight adjustments, and rollbacks natively inside Kubernetes, reducing the need for complex pipeline scripts.
Flagger
Flagger is a progressive delivery operator that automates canary releases by monitoring Prometheus metrics and orchestrating routing adjustments across Ingress controllers (like NGINX, Gloo, or Traefik) and service meshes (like Istio or Linkerd). When an anomaly occurs, Flagger reverts routing changes automatically without human intervention.
Istio Service Mesh
While Ingress controllers operate at the edge of your cluster, a service mesh like Istio enables canary releases between internal microservices deep within your call graph. Istio uses VirtualService and DestinationRule resources to split traffic based on request percentages, HTTP request headers, session cookies, or user agent properties.
Lab Challenges Learners Should Try Independently
Once you have mastered the basic lab exercises, work through these real-world deployment challenges to deepen your understanding:
Self-Paced Lab Challenges:
[1. Header-Based Routing] --> Route exclusively for testers with X-Canary: true
[2. Synthetic Latency] --> Inject a 2000ms delay and evaluate P99 metrics
[3. Failing Readiness Probe] --> Block broken pods before traffic ever reaches them
[4. Resource Exhaustion] --> Set tiny memory limits and watch OOMKilled events
[5. Flagger Auto-Rollout] --> Connect Prometheus alerts directly to automated rollbacks
Challenge 1: Header-Based Targeted Canary Releases
- Task: Instead of an indiscriminate percentage split, configure your Ingress to route to the canary only when a request contains the HTTP header
X-Canary: internal-tester. - Learning Objective: Understand how internal teams can test upcoming releases against production databases and services without exposing public users to unreleased features.
Challenge 2: Artificial Response Latency Injection
- Task: Add a 2000ms sleep delay to your canary application code. Configure a Prometheus query that calculates the P99 response time and flags the deployment as degraded.
- Learning Objective: Learn how to detect performance regressions that do not throw explicit 500 errors but still degrade user experience.
Challenge 3: Simulate a Failing Readiness Probe
- Task: Configure the canary’s
/healthendpoint to return HTTP 503 while keeping the container running. - Learning Objective: Observe how Kubernetes prevents broken pods from entering Service endpoints, keeping the ingress router from routing traffic to dead instances.
Challenge 4: Memory Leak and OOMKilled Simulation
- Task: Add an endpoint that appends data to a global array on every request until the container hits its 128Mi memory limit.
- Learning Objective: Identify
OOMKilledcrash loops usingkubectl describe podand observe how pod restarts disrupt live traffic.
Challenge 5: Automated Canary Promotion with Flagger
- Task: Install Flagger and Prometheus into your
kindcluster. Define aCanaryresource that promotes releases automatically if the 5xx error rate stays below 1% for five consecutive minutes. - Learning Objective: Gain experience with production-grade, metric-driven progressive delivery automation.
Common Mistakes During Canary Deployment Practice
Watch out for these common architectural and operational pitfalls when working with canary releases:
Common Canary Mistakes & Solutions:
[ Mistake ] [ Solution ]
Routing to Unready Pods ──► Configure strict Readiness Probes
Short Observation Windows ──► Allow sufficient time for metrics to stabilize
Ignoring Database Schemas ──► Enforce backward-compatible schema changes
Relying Solely on Pod Ratios ──► Use dynamic L7 traffic splitting proxies
Deploying Without Automated Tests ──► Keep automated smoke tests running during canary stages
- Routing Traffic to Unready Instances: Never assume a running container is ready to accept requests. If your application takes 15 seconds to load application state, incoming requests will fail unless you configure an accurate
readinessProbe. - Short Evaluation Windows: Evaluating a canary over 30 seconds rarely surfaces real-world performance patterns. Latency increases, database connection pool exhaustion, and memory leaks typically emerge only under sustained traffic over time.
- Breaking Database Backward Compatibility: If version 2 drops a database column that version 1 still relies on, rolling back the canary will not restore system stability. Always use multi-phase, backward-compatible database schema migrations.
- Relying Exclusively on Pod Replica Counts for Traffic Splits: Distributing traffic solely through pod ratios (e.g., 1 canary pod alongside 3 stable pods) provides noisy, imprecise routing splits and prevents you from testing smaller increments like 1% or 2%.
- Confusing Deployment Success with Application Health: A pod status of
Runningsimply confirms the container process has not terminated. It does not mean the application is handling incoming requests correctly or returning valid payloads. Always validate actual HTTP status codes and application metrics.
Frequently Asked Questions
What is a canary deployment in DevOps?
A canary deployment is a progressive delivery strategy where a new software version is exposed to a small subset of users (e.g., 5% to 10%) alongside the stable production version. System metrics are monitored closely; if no errors or regressions appear, the new version gradually takes on more traffic until it replaces the old release entirely.
How can beginners practice canary deployments?
Beginners can build a free, fully functional lab using a local machine with Docker, kind or Minikube, and an NGINX Ingress controller. Deploy two versions of a simple web application, use Ingress annotations to split traffic by weight, and observe the results using curl loops and standard kubectl commands.
Is Kubernetes required for canary deployment?
No. While Kubernetes simplifies canary management with declarative resource models, the pattern itself is infrastructure-agnostic. You can implement canary releases using cloud load balancers (like AWS ALB), API gateways (such as Kong or KrakenD), reverse proxies (HAProxy, NGINX), or DNS-level routing.
What is the difference between canary and blue-green deployment?
A blue-green deployment runs two identical environments simultaneously, instantly shifting 100% of traffic from the old version (Blue) to the new version (Green) once testing is complete. A canary deployment gradually shifts traffic in small increments (e.g., 10%, 25%, 50%, 100%), evaluating live production metrics at every step before expanding user exposure.
How do teams control canary traffic percentages?
Teams manage traffic splits using Layer-7 proxies, Ingress controllers (like NGINX), or service meshes (such as Istio and Linkerd). These tools evaluate incoming requests and route specific percentages to different backend services according to configured weights, headers, or cookies.
Which monitoring tools are useful for canary releases?
Prometheus is widely used to scrape and evaluate operational metrics (such as HTTP error rates and latency percentiles), while Grafana provides clear dashboard visualizations. For automated deployments, tools like Argo Rollouts and Flagger query Prometheus directly to make automated promotion or rollback decisions.
How does automated canary rollback work?
An automated rollback system continuously tracks application health metrics during the canary release. If a critical metric breaches a defined threshold—such as the HTTP 5xx error rate exceeding 1% over a two-minute window—the controller immediately resets the traffic weight to 0% and scales down the canary pods, restoring 100% of traffic to the stable release.
Can canary deployments be integrated with CI/CD pipelines?
Yes. Modern CI/CD platforms like GitHub Actions, GitLab CI, and Jenkins integrate directly with progressive delivery controllers. Pipelines trigger the build, deploy the canary resources, run automated smoke tests, wait for health analysis checks to clear, and then proceed with incremental promotion or automated rollback based on the outcome.
Final Perspective
Building resilient software delivery pipelines requires balancing rapid iteration with production stability. Canary deployments eliminate the anxiety of “big-bang” releases by giving teams fine-grained control over how changes reach end users. By exposing new code to a small fraction of traffic first, you reduce your blast radius, give yourself time to catch subtle regressions, and make rollbacks predictable and stress-free. Theoretical understanding is a helpful start, but real operational confidence comes from hands-on experimentation. By creating local labs, testing Layer-7 traffic splitting, inducing failures, and executing rollbacks firsthand, you build the practical instincts needed to design reliable delivery platforms. Set up your local cluster, experiment with different traffic weights, break your test applications, and make progressive delivery an integral part of your production engineering toolkit.