Skip to content
Codeloom

Courses / CI/CD & GitHub Actions

Lesson 13 of 16

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.

Intermediate 14 min read

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

  1. Blue is running version 1, serving all traffic.
  2. Deploy version 2 to Green.
  3. Run smoke tests and health checks against Green.
  4. Switch the load balancer or DNS to route traffic to Green.
  5. Green is now live. Blue becomes the standby.
  6. 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();
}
  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

StrategyRollback SpeedCostComplexityBest For
Blue-GreenInstantHigh (2x infra)LowCritical services needing instant rollback
CanaryFastLowMediumServices with good observability
RollingMediumLowLowStandard stateless services
A/BFastMediumHighFeature 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.

Progress is saved locally to your browser.