CI/CD Deployment Strategies Explained: Blue-Green, Canary, Rolling & A/B
Understand blue-green, canary, rolling, and A/B deployment strategies with architecture diagrams, real configuration examples, and guidance on when to use each.
What you'll learn
- ✓How blue-green deployments enable instant rollback
- ✓Canary releases for gradual traffic shifting and risk reduction
- ✓Rolling deployments for zero-downtime updates in clusters
- ✓A/B deployments for feature validation with real users
- ✓Choosing the right strategy based on risk, cost, and complexity
Prerequisites
- •Basic understanding of CI/CD pipelines
- •Familiarity with load balancers or Kubernetes
Deploying new code to production is the riskiest moment in any release cycle. The deployment strategy you choose determines how much risk you absorb, how fast you can roll back, and how your users experience the transition. There is no single best strategy; each one trades off complexity, cost, and safety differently.
Blue-Green Deployment
Blue-green deployment maintains two identical production environments. One (blue) serves live traffic while the other (green) receives the new version. After testing, you switch the router to point at green. If anything goes wrong, switch back to blue instantly.
How It Works
- Blue is running version 1, serving all traffic.
- Deploy version 2 to Green.
- Run smoke tests and health checks against Green.
- Switch the load balancer or DNS to route traffic to Green.
- Green is now live. Blue becomes the standby.
- If problems appear, switch back to Blue in seconds.
Kubernetes Implementation
Using Kubernetes services and deployments, blue-green can be implemented with label selectors:
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
labels:
app: myapp
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: myapp:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
labels:
app: myapp
version: green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myapp:2.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
The service selector determines which deployment receives traffic:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: green # Switch this between blue and green
ports:
- port: 80
targetPort: 8080
Switch traffic by patching the service selector:
# Switch from blue to green
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# Rollback to blue
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'
CI/CD Pipeline for Blue-Green
name: Blue-Green Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Determine inactive environment
id: env
run: |
CURRENT=$(kubectl get svc myapp -o jsonpath='{.spec.selector.version}')
if [ "$CURRENT" = "blue" ]; then
echo "target=green" >> "$GITHUB_OUTPUT"
else
echo "target=blue" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to inactive environment
run: |
kubectl set image deployment/myapp-${{ steps.env.outputs.target }} \
myapp=myapp:${{ github.sha }}
kubectl rollout status deployment/myapp-${{ steps.env.outputs.target }} --timeout=300s
- name: Run smoke tests
run: |
TARGET_IP=$(kubectl get svc myapp-${{ steps.env.outputs.target }}-internal -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl --fail --retry 5 --retry-delay 10 "http://$TARGET_IP/health"
- name: Switch traffic
run: |
kubectl patch service myapp -p '{"spec":{"selector":{"version":"${{ steps.env.outputs.target }}"}}}'
When to Use Blue-Green
- You need instant rollback capability.
- Your application can run two full environments simultaneously (cost consideration).
- Database schema changes are backward-compatible or handled separately.
Canary Deployment
Canary deployment routes a small percentage of traffic to the new version while the majority continues hitting the stable version. If metrics look good, you gradually increase the canary’s traffic share until it handles 100%.
Traffic Splitting with Nginx Ingress
# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 80
This sends 10% of traffic to the canary service. Increase the weight incrementally:
# Increase canary traffic to 25%
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="25" --overwrite
# Increase to 50%
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="50" --overwrite
# Promote: remove canary, update main deployment
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="100" --overwrite
Automated Canary with Metrics
A production canary pipeline should check metrics at each step:
name: Canary Deploy
on:
push:
branches: [main]
jobs:
canary:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy canary (10%)
run: |
kubectl set image deployment/myapp-canary myapp=myapp:${{ github.sha }}
kubectl rollout status deployment/myapp-canary --timeout=300s
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="10" --overwrite
- name: Monitor canary (5 minutes)
run: |
sleep 300
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..', deployment='canary'}[5m])" | jq '.data.result[0].value[1] // "0"' -r)
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "Error rate too high: $ERROR_RATE"
kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
exit 1
fi
- name: Promote to 50%
run: |
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="50" --overwrite
sleep 300
- name: Full promotion
run: |
kubectl set image deployment/myapp-stable myapp=myapp:${{ github.sha }}
kubectl rollout status deployment/myapp-stable --timeout=300s
kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
When to Use Canary
- You want to limit the blast radius of a bad deployment.
- You have metrics and monitoring in place to detect issues automatically.
- Your application handles stateless requests where routing to different versions is safe.
Rolling Deployment
Rolling deployments update instances incrementally. Instead of swapping entire environments, you replace pods or instances one (or a few) at a time. This is the default strategy in Kubernetes.
Kubernetes Rolling Update
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 6
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # Create up to 2 extra pods during update
maxUnavailable: 1 # At most 1 pod can be unavailable
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:2.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
Key parameters:
- maxSurge: How many pods above the desired count can exist during the update. Higher values speed up deployment.
- maxUnavailable: How many pods can be down during the update. Setting this to 0 ensures full capacity throughout.
Rollback is built in:
# Rollback to the previous revision
kubectl rollout undo deployment/myapp
# Rollback to a specific revision
kubectl rollout undo deployment/myapp --to-revision=3
# Check rollout history
kubectl rollout history deployment/myapp
When to Use Rolling
- You want zero-downtime updates without the cost of duplicate environments.
- Your application is stateless or handles mixed-version traffic gracefully.
- You are running in Kubernetes or a similar orchestrator that handles rolling updates natively.
A/B Deployment
A/B deployment routes traffic based on user attributes rather than random percentages. Different user segments see different versions, enabling feature validation with real production traffic.
Header-Based Routing
# a-b-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-experiment
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "X-Feature-Group"
nginx.ingress.kubernetes.io/canary-by-header-value: "beta-users"
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-beta
port:
number: 80
Users with the X-Feature-Group: beta-users header hit the beta service. Everyone else gets the stable version. Your frontend or API gateway sets this header based on user attributes:
// API gateway middleware
function routeMiddleware(req, res, next) {
const user = req.user;
if (user.betaOptIn || user.employeeId) {
req.headers['X-Feature-Group'] = 'beta-users';
}
next();
}
Cookie-Based Routing
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-cookie: "beta-version"
Set the cookie beta-version=always to route to the new version, or beta-version=never to force the stable version.
When to Use A/B
- You need to validate a feature with specific user segments before a full rollout.
- Business stakeholders want to measure the impact of a change on conversion, engagement, or revenue.
- You have the infrastructure to set routing headers or cookies based on user attributes.
Choosing the Right Strategy
| Strategy | Rollback Speed | Cost | Complexity | Best For |
|---|---|---|---|---|
| Blue-Green | Instant | High (2x infra) | Low | Critical services needing instant rollback |
| Canary | Fast | Low | Medium | Services with good observability |
| Rolling | Medium | Low | Low | Standard stateless services |
| A/B | Fast | Medium | High | Feature validation with metrics |
Start with rolling deployments if you are on Kubernetes, since they are the default and require no extra configuration. Graduate to canary when you have monitoring that can detect errors within minutes. Use blue-green for databases, stateful services, or anything where rollback must be instantaneous. Add A/B when product teams need to validate features with controlled user segments.
Summary
Deployment strategies exist on a spectrum of risk versus complexity. Rolling updates handle most cases with zero downtime and minimal cost. Canary deployments add a safety net by limiting exposure. Blue-green deployments provide the ultimate rollback guarantee at the cost of running two full environments. A/B deployments extend the concept to targeted user segments for product experimentation. Choose based on your risk tolerance, observability maturity, and infrastructure budget.
Related articles
- CI/CD CI/CD Deployment Strategies: Blue-Green, Canary, Rolling
Learn the most popular CI/CD deployment strategies including blue-green, canary, and rolling deployments with practical examples and configuration.
- CI/CD CI/CD Environment Promotion: Dev to Staging to Production
Implement robust environment promotion patterns with approval gates, environment protection rules, and automated validation at every stage.
- CI/CD Blue-Green vs Canary Deployments Explained
Compare blue-green and canary deployment strategies, including how they handle rollback, traffic shifting, and observability, with concrete Kubernetes and AWS examples.
- CI/CD CI/CD Deployment Strategies Overview
Compare rolling, blue/green, canary, shadow, and feature flag deployments. Learn when to pick each strategy and the trade-offs in risk and cost.