Deploying software shouldn't mean downtime. Users shouldn't see errors during your update. Modern deployment strategies eliminate downtime, reduce risk, and enable instant rollbacks. In this lesson, you'll learn rolling updates, blue-green deployments, canary releases, and feature flags—the techniques that let you deploy with confidence.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Implement rolling updates for gradual changes
- Set up blue-green deployments for instant rollbacks
- Use canary releases to test with real traffic
- Implement feature flags for controlled rollouts
- Choose the right strategy for your application
- Automate deployments in CI/CD pipelines
2. Why This Matters
Real-world scenario: Your team ships 50 deployments per day. One deployment introduces a bug that affects 1% of users. Without proper strategies, you either roll back everyone or let the bug affect all users. With canary deployments, only 1% see the bug. With feature flags, you disable the feature instantly.
3. Core Concepts
Rolling Updates (Kubernetes Native)
# rolling-update-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Can create 1 extra pod
maxUnavailable: 0 # Never go below desired replicas
template:
spec:
containers:
- name: app
image: myapp:v2
# Rolling update process:
# 1. Creates 1 new pod with v2
# 2. Waits for it to be ready
# 3. Removes 1 old pod
# 4. Repeats until all 5 pods are v2
# Commands
kubectl set image deployment/myapp app=myapp:v2
kubectl rollout status deployment/myapp
kubectl rollout history deployment/myapp
kubectl rollout undo deployment/myapp
# For zero-downtime, configure readiness probes
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Blue-Green Deployment
# blue-green-deployment.yaml
# Blue (current production) - v1
# Green (new version) - v2
# Blue deployment (current)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
labels:
version: blue
spec:
replicas: 5
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: app
image: myapp:v1
# Green deployment (new)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
labels:
version: green
spec:
replicas: 5
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: app
image: myapp:v2
# Service (points to active version)
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
version: blue # Changes to 'green' during switch
ports:
- port: 80
targetPort: 8080
# Blue-Green deployment script
#!/bin/bash
# deploy-blue-green.sh
echo "Deploying green version..."
kubectl apply -f green-deployment.yaml
echo "Waiting for green pods to be ready..."
kubectl wait --for=condition=ready pod -l version=green --timeout=300s
echo "Running smoke tests on green..."
GREEN_IP=$(kubectl get pods -l version=green -o jsonpath='{.items[0].status.podIP}')
curl -f http://$GREEN_IP:8080/health || exit 1
echo "Switching traffic to green..."
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"green"}}}'
echo "Waiting for traffic to drain from blue..."
sleep 30
echo "Scaling down blue..."
kubectl scale deployment myapp-blue --replicas=0
echo "Blue-green deployment complete!"
# Rollback (instant - just switch service back)
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"blue"}}}'
Canary Deployment
# canary-deployment.yaml
# Send small percentage of traffic to new version
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- port: 80
---
# Stable version (95% traffic)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
spec:
replicas: 19
selector:
matchLabels:
app: myapp
track: stable
template:
metadata:
labels:
app: myapp
track: stable
spec:
containers:
- name: app
image: myapp:v1
---
# Canary version (5% traffic)
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: app
image: myapp:v2
# Traffic splitting with Istio
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-virtualservice
spec:
hosts:
- myapp-service
http:
- route:
- destination:
host: myapp-stable
subset: v1
weight: 95
- destination:
host: myapp-canary
subset: v2
weight: 5
# Progressive canary (increase traffic gradually)
#!/bin/bash
# progressive-canary.sh
# Start with 1%
kubectl patch virtualservice myapp-virtualservice --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 1}]'
sleep 300 # Monitor for 5 minutes
# Increase to 10%
kubectl patch virtualservice myapp-virtualservice --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 10}]'
sleep 300
# Increase to 50%
kubectl patch virtualservice myapp-virtualservice --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 50}]'
sleep 300
# Full rollout (100%)
kubectl patch virtualservice myapp-virtualservice --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 100}]'
Feature Flags (Feature Toggles)
# feature_flags.py - Feature flag implementation
import os
import redis
from functools import wraps
from flask import Flask, jsonify, request
app = Flask(__name__)
redis_client = redis.Redis(host='redis-service', decode_responses=True)
class FeatureFlags:
"""Feature flag management"""
def __init__(self):
self.default_flags = {
'new_checkout': False,
'dark_mode': False,
'analytics_v2': False,
'recommendations': True,
}
def is_enabled(self, flag_name, user_id=None):
"""Check if feature flag is enabled"""
# Check if overridden in Redis
flag_value = redis_client.get(f'feature_flag:{flag_name}')
if flag_value is not None:
return flag_value.lower() == 'true'
# Check user-specific override (percentage rollout)
if user_id:
percentage = redis_client.get(f'feature_flag:{flag_name}:percentage')
if percentage:
hash_value = hash(f"{user_id}:{flag_name}") % 100
if hash_value < int(percentage):
return True
# Return default
return self.default_flags.get(flag_name, False)
def enable(self, flag_name, percentage=None):
"""Enable feature flag for all users or percentage"""
if percentage:
redis_client.set(f'feature_flag:{flag_name}:percentage', percentage)
else:
redis_client.set(f'feature_flag:{flag_name}', 'true')
def disable(self, flag_name):
"""Disable feature flag"""
redis_client.delete(f'feature_flag:{flag_name}')
redis_client.delete(f'feature_flag:{flag_name}:percentage')
feature_flags = FeatureFlags()
# Decorator for feature-flagged endpoints
def feature_flag(flag_name):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = request.headers.get('X-User-ID')
if feature_flags.is_enabled(flag_name, user_id):
return f(*args, **kwargs)
return jsonify({'error': 'Feature not available'}), 404
return decorated_function
return decorator
# API endpoints to manage flags
@app.route('/api/features', methods=['GET'])
def list_features():
"""List all feature flags and their status"""
flags = {}
for flag in feature_flags.default_flags:
flags[flag] = feature_flags.is_enabled(flag)
return jsonify(flags)
@app.route('/api/features/<flag_name>', methods=['POST'])
def enable_feature(flag_name):
"""Enable a feature flag"""
data = request.json
percentage = data.get('percentage')
feature_flags.enable(flag_name, percentage)
return jsonify({'status': 'enabled', 'flag': flag_name})
@app.route('/api/features/<flag_name>', methods=['DELETE'])
def disable_feature(flag_name):
"""Disable a feature flag"""
feature_flags.disable(flag_name)
return jsonify({'status': 'disabled', 'flag': flag_name})
# Feature-flagged endpoint
@app.route('/api/checkout')
@feature_flag('new_checkout')
def new_checkout():
"""New checkout flow (behind feature flag)"""
return jsonify({'checkout_flow': 'v2', 'features': ['one-click', 'saved-cards']})
@app.route('/api/checkout/old')
def old_checkout():
"""Old checkout flow (fallback)"""
return jsonify({'checkout_flow': 'v1', 'features': ['standard']})
# Enable feature for 10% of users
# curl -X POST /api/features/new_checkout -H 'Content-Type: application/json' -d '{"percentage": 10}'
4. Complete Project: Multi-Strategy Deployment Pipeline
# .github/workflows/deployment-pipeline.yml
name: Multi-Strategy Deployment
on:
push:
branches: [main]
workflow_dispatch:
inputs:
strategy:
description: 'Deployment strategy'
required: true
default: 'rolling'
type: choice
options:
- rolling
- blue-green
- canary
canary_percentage:
description: 'Canary traffic percentage'
default: '10'
type: string
env:
CLUSTER: production
NAMESPACE: production
jobs:
deploy-rolling:
if: github.event.inputs.strategy == 'rolling' || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Set up kubeconfig
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Rolling update
run: |
kubectl set image deployment/myapp \
app=${{ secrets.REGISTRY }}/myapp:${{ github.sha }} \
-n ${{ env.NAMESPACE }}
kubectl rollout status deployment/myapp -n ${{ env.NAMESPACE }} --timeout=300s
deploy-blue-green:
if: github.event.inputs.strategy == 'blue-green'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy green version
run: |
envsubst < k8s/green-deployment.yaml | kubectl apply -f -
kubectl wait --for=condition=ready pod -l version=green -n ${{ env.NAMESPACE }} --timeout=300s
- name: Verify green deployment
run: |
GREEN_POD=$(kubectl get pod -l version=green -n ${{ env.NAMESPACE }} -o jsonpath='{.items[0].metadata.name}')
kubectl exec $GREEN_POD -n ${{ env.NAMESPACE }} -- curl -f localhost:8080/health || exit 1
- name: Switch traffic
run: |
kubectl patch service myapp-service -n ${{ env.NAMESPACE }} \
-p '{"spec":{"selector":{"version":"green"}}}'
- name: Scale down blue
run: kubectl scale deployment myapp-blue -n ${{ env.NAMESPACE }} --replicas=0
deploy-canary:
if: github.event.inputs.strategy == 'canary'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy canary version
run: |
envsubst < k8s/canary-deployment.yaml | kubectl apply -f -
kubectl wait --for=condition=ready pod -l track=canary -n ${{ env.NAMESPACE }} --timeout=300s
- name: Set canary traffic percentage
run: |
kubectl patch virtualservice myapp-virtualservice -n ${{ env.NAMESPACE }} --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": ${{ github.event.inputs.canary_percentage }}}]'
- name: Monitor canary
run: |
echo "Monitoring canary for 10 minutes..."
sleep 600
# Check error rate
ERROR_RATE=$(kubectl exec -n monitoring prometheus-0 -- \
wget -q -O- "http://localhost:9090/api/v1/query?query=sum(rate(http_requests_total{track=canary,status=~'5..'}[5m]))/sum(rate(http_requests_total{track=canary}[5m]))" | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate too high! Rolling back canary..."
kubectl patch virtualservice myapp-virtualservice -n ${{ env.NAMESPACE }} --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 0}]'
exit 1
fi
echo "Canary successful, promoting to 100%"
kubectl patch virtualservice myapp-virtualservice -n ${{ env.NAMESPACE }} --type='json' \
-p='[{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 100}]'
notify:
needs: [deploy-rolling, deploy-blue-green, deploy-canary]
runs-on: ubuntu-latest
if: always()
steps:
- name: Slack notification
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment ${{ job.status }}: ${{ github.event.inputs.strategy }} to ${{ env.NAMESPACE }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
5. Strategy Comparison
┌─────────────────┬──────────────┬───────────────┬─────────────┬──────────────┐
│ Strategy │ Downtime │ Rollback │ Complexity │ Risk │
├─────────────────┼──────────────┼───────────────┼─────────────┼──────────────┤
│ Rolling Update │ None │ Slow │ Low │ Medium │
├─────────────────┼──────────────┼───────────────┼─────────────┼──────────────┤
│ Blue-Green │ None │ Instant │ Medium │ Low │
├─────────────────┼──────────────┼───────────────┼─────────────┼──────────────┤
│ Canary │ None │ Instant │ High │ Very Low │
├─────────────────┼──────────────┼───────────────┼─────────────┼──────────────┤
│ Feature Flags │ None │ Instant │ High │ Lowest │
├─────────────────┼──────────────┼───────────────┼─────────────┼──────────────┤
│ Recreate │ Downtime │ Slow │ Lowest │ High │
└─────────────────┴──────────────┴───────────────┴─────────────┴──────────────┘
When to use each:
- Rolling Update: Default for stateless apps, simple to implement
- Blue-Green: When you need instant rollback capability
- Canary: High-traffic apps, risk-averse teams
- Feature Flags: Gradual rollout, A/B testing, complex features
- Recreate: Batch jobs, development environments only
6. Common Errors & Solutions
7. Summary Checklist
8. Next Steps
Next lesson: CI/CD Lesson 7.4: Infrastructure as Code in CI
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment