Blue/Green Deployment (4): Operations and Optimization
Monitoring, cost optimization, networking considerations, a troubleshooting guide, and real-world case studies
Contents
Blue/Green operations stay stable only when the cutover checklist, the post-cutover observation window, and the immediate-rollback criteria are treated as three separate concerns.
TL;DR
- Blue/Green operations come down to a cutover checklist, monitoring, and clear immediate-rollback criteria.
- Automation should focus less on the deployment itself and more on holding the 5 to 10 minute observation window after the cutover steady.
- Runbooks and thresholds need regular updates based on real incidents, otherwise the same failures repeat.
Cutover and rollback checklist
Most of the cutover's stability is decided by how complete the checklist is. Separating pre-deployment verification, immediate post-cutover monitoring, and rollback conditions makes the response faster.
Before the cutover (verifying Green)
Infrastructure
- All Green pods/instances Running and Ready
- Health check endpoint responds (HTTP 200)
- Resource usage in normal range (CPU < 80%, memory < 80%)
Application
- Smoke test passes
- Critical API endpoints tested
- Dependency connections verified (DB, Redis, external APIs)
Database
- Migration scripts ran successfully
- Schema backward compatibility confirmed
- Data backfill complete where required
Monitoring
- Alert channels verified (Slack, PagerDuty)
- Dashboards reachable
- Rollback owner on standby
Right after the cutover (5 minutes of monitoring)
Critical metrics (first 5 minutes)
- HTTP 5xx error rate < 0.1%
- No spike in HTTP 4xx error rate
- P99 (99th percentile) latency no more than 20% above the previous version
- Throughput (RPS, requests per second) holding steady
Application logs
- No spike in ERROR/CRITICAL level logs
- No exception stack traces
- No connection refused/timeout entries
Business metrics
- No drop in key conversion rates (login, payment)
- User sessions preserved
Rollback decision criteria
| Metric | Threshold | Action |
|---|---|---|
| 5xx error rate | > 1% (over 5 minutes) | Roll back immediately |
| P99 latency | > 2x previous value | Investigate, then decide |
| Core feature failure | Payment/login failing | Roll back immediately |
| CPU utilization | > 90% (sustained) | Scale up or roll back |
| OOM (out of memory) kill | Occurred | Roll back immediately |
Monitoring setup
Defining a small set of SLIs (service level indicators) and SLOs (service level objectives) you can act on immediately beats watching a large number of metrics. Alert rules only pay off when each one is designed alongside the action it triggers (rollback or scaling).
Defining the core SLIs and SLOs
# SLO example
slos:
availability:
target: 99.9% # allows roughly 43.2 minutes of downtime per month
window: 30d
indicator: |
sum(rate(http_requests_total{status!~"5.."}[5m])) /
sum(rate(http_requests_total[5m]))
latency:
target: 95% # 95% of requests under 200ms
window: 30d
indicator: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)
error_budget:
monthly: 43.2m # 99.9% SLO = 43.2 minutes/month
alert_threshold: 50% # alert when half the budget is spentPrometheus alert rules
# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: blue-green-alerts
spec:
groups:
- name: deployment
rules:
# 5xx error rate spike
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) > 0.01
for: 2m
labels:
severity: critical
action: rollback
annotations:
summary: "High error rate detected after deployment"
description: "Error rate is {{ $value | humanizePercentage }} (threshold: 1%)"
runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
# latency spike
- alert: HighLatency
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
> 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "P99 latency exceeded 500ms"
description: "Current P99: {{ $value | humanizeDuration }}"
# pod restart detection
- alert: PodRestartLoop
expr: |
increase(kube_pod_container_status_restarts_total{
pod=~"my-app-.*"
}[15m]) > 3
labels:
severity: critical
action: rollback
annotations:
summary: "Pod restart loop detected"
# post-deployment rollback detection
- alert: DeploymentRollback
expr: |
kube_deployment_status_observed_generation{deployment="my-app"}
< kube_deployment_metadata_generation{deployment="my-app"}
for: 1m
labels:
severity: info
annotations:
summary: "Deployment rollback in progress"Grafana dashboard
The dashboard below compares the Blue/Green cutover at a glance through four panels. Those panels are traffic distribution, error rate per version, P99 latency per version, and deployment event markers.
{
"dashboard": {
"title": "Blue/Green Deployment Dashboard",
"panels": [
{
"title": "Traffic Distribution",
"type": "piechart",
"targets": [
{
"expr": "sum(rate(http_requests_total{color=\"blue\"}[5m]))",
"legendFormat": "Blue"
},
{
"expr": "sum(rate(http_requests_total{color=\"green\"}[5m]))",
"legendFormat": "Green"
}
]
},
{
"title": "Error Rate by Version",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total{status=~\"5..\",color=\"blue\"}[5m])) / sum(rate(http_requests_total{color=\"blue\"}[5m]))",
"legendFormat": "Blue 5xx"
},
{
"expr": "sum(rate(http_requests_total{status=~\"5..\",color=\"green\"}[5m])) / sum(rate(http_requests_total{color=\"green\"}[5m]))",
"legendFormat": "Green 5xx"
}
]
},
{
"title": "P99 Latency Comparison",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{color=\"blue\"}[5m])) by (le))",
"legendFormat": "Blue P99"
},
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{color=\"green\"}[5m])) by (le))",
"legendFormat": "Green P99"
}
]
},
{
"title": "Deployment Events",
"type": "annotations",
"datasource": "-- Grafana --",
"query": "tags=deployment"
}
]
}
}Recording deployment events
# deploy_marker.py - record deployment events in Grafana
import os
import requests
from datetime import datetime
def mark_deployment(version: str, environment: str, color: str):
"""Create a deployment event marker in Grafana"""
grafana_url = "https://grafana.example.com"
api_key = os.environ["GRAFANA_API_KEY"]
annotation = {
"time": int(datetime.now().timestamp() * 1000),
"tags": ["deployment", environment, color],
"text": f"Deployed {version} to {color} ({environment})"
}
response = requests.post(
f"{grafana_url}/api/annotations",
json=annotation,
headers={"Authorization": f"Bearer {api_key}"}
)
return response.status_code == 200Cost optimization
Blue/Green runs two environments, so cost goes up; the policy for steady state and the policy for deployment windows have to be separated. Combining spot/preemptible instances, autoscaling, and shrinking the idle environment keeps both stability and cost in range.
Cost breakdown
Using spot/preemptible instances
# Use spot instances for the Green environment (AWS EKS)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-green
spec:
template:
spec:
nodeSelector:
node.kubernetes.io/lifecycle: spot
tolerations:
- key: "node.kubernetes.io/lifecycle"
operator: "Equal"
value: "spot"
effect: "NoSchedule"
# graceful shutdown for spot interruptions
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]Autoscaling configuration
# green-hpa.yaml - keep the Green environment minimal
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-green
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app-green
minReplicas: 1 # minimum footprint in steady state
maxReplicas: 10 # expand during deployment
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # scale up immediately
policies:
- type: Percent
value: 100
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 minutes before scaling downShrinking Green outside deployment hours
#!/bin/bash
# scale-green.sh - shrink the Green environment outside business hours
HOUR=$(date +%H)
DAY=$(date +%u)
# scale down outside business hours (Mon-Fri 09:00-18:00)
if [ $DAY -gt 5 ] || [ $HOUR -lt 9 ] || [ $HOUR -gt 18 ]; then
echo "Off-hours: Scaling down Green environment"
kubectl scale deployment my-app-green --replicas=0
else
echo "Business hours: Maintaining Green environment"
kubectl scale deployment my-app-green --replicas=1
fiRegistering this script as two CronJobs takes Green down to replicas=0 at 19:00 on weekdays and brings it back to replicas=1 at 08:00.
# automated with CronJobs
apiVersion: batch/v1
kind: CronJob
metadata:
name: scale-green-down
spec:
schedule: "0 19 * * 1-5" # weekdays at 19:00
jobTemplate:
spec:
template:
spec:
serviceAccountName: deployment-scaler
containers:
- name: kubectl
image: bitnami/kubectl
command:
- kubectl
- scale
- deployment/my-app-green
- --replicas=0
restartPolicy: OnFailure
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: scale-green-up
spec:
schedule: "0 8 * * 1-5" # weekdays at 08:00
jobTemplate:
spec:
template:
spec:
serviceAccountName: deployment-scaler
containers:
- name: kubectl
image: bitnami/kubectl
command:
- kubectl
- scale
- deployment/my-app-green
- --replicas=1
restartPolicy: OnFailureNetworking considerations
A large share of failed cutovers originates in DNS, CDN, or session handling rather than in application code. Agreeing on TTL, cache, and connection draining policies in advance removes most of the incidents that happen mid-cutover.
Managing DNS TTL
# Route53 TTL example
aws route53 change-resource-record-sets \
--hosted-zone-id Z123456 \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "api.example.com",
"Type": "A",
"TTL": 60,
"ResourceRecords": [{"Value": "1.2.3.4"}]
}
}]
}'CDN cache invalidation
# CloudFront cache invalidation
aws cloudfront create-invalidation \
--distribution-id E123456789 \
--paths "/*"
# Fastly cache purge
curl -X POST "https://api.fastly.com/service/{service_id}/purge_all" \
-H "Fastly-Key: $FASTLY_API_KEY"Connection draining
# Kubernetes Service - connection draining settings
apiVersion: v1
kind: Service
metadata:
name: my-app
annotations:
# AWS ALB
service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "60"
spec:
# ...# Pod - graceful shutdown
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
# stop new connections, wait for existing ones to finish
command:
- /bin/sh
- -c
- |
# make the health check fail
touch /tmp/shutdown
# wait for existing connections to drain
sleep 30Handling sticky sessions
# Externalizing sessions through Redis (Spring Boot)
# application.yml
spring:
session:
store-type: redis
redis:
namespace: spring:session
redis:
host: redis.example.com
port: 6379
---
# When using session affinity in Kubernetes
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600Troubleshooting guide
How fast a problem gets resolved depends on whether the path from symptom to likely cause to immediate action is written down. Standardizing the patterns that recur in production lets a new team member respond the same way as everyone else.
Common problems and fixes
Problem 1: the Green environment never becomes Ready
# diagnose
kubectl get pods -l color=green
kubectl describe pod my-app-green-xxx
# common causes
# 1. image pull failure
kubectl get events --field-selector reason=Failed
# 2. readiness probe failure
kubectl logs my-app-green-xxx
# 3. insufficient resources
kubectl describe node | grep -A5 "Allocated resources"Problem 2: 5xx errors spike after the cutover
# roll back immediately
kubectl patch svc my-app -p '{"spec":{"selector":{"color":"blue"}}}'
# root cause analysis
# 1. check logs
kubectl logs -l color=green --tail=100
# 2. check dependency connectivity
kubectl exec -it my-app-green-xxx -- curl -v http://db-service:5432
# 3. compare environment variables
kubectl get deployment my-app-blue -o json | jq '.spec.template.spec.containers[0].env'
kubectl get deployment my-app-green -o json | jq '.spec.template.spec.containers[0].env'Problem 3: sessions are lost
# check Redis sessions
redis-cli KEYS "spring:session:*" | wc -l
# fix: verify session migration
# 1. check session store connectivity
kubectl exec my-app-green-xxx -- nc -zv redis.example.com 6379
# 2. check session key format (cross-version compatibility)
redis-cli GET "spring:session:sessions:xxx"Troubleshooting flowchart
Case studies
Real cases are what calibrate design principles against operational reality. Recording failures and rollbacks, not only the clean runs, is what makes the next deployment safer.
Case 1: e-commerce platform (payment system)
Situation: Blue/Green deployment of a payment module update
Environment
| Item | Spec |
|---|---|
| Infrastructure | AWS EKS (Kubernetes 1.28) |
| Deployment tool | Argo Rollouts |
| Traffic | 10,000 RPS (peak) |
Deployment strategy
Result
| Metric | Value |
|---|---|
| Downtime | 0 seconds |
| Rollback needed | None |
| Total elapsed time | 55 minutes |
Case 2: API server (including a DB schema change)
Situation: adding a new column to the users table
Deployment plan (expand and contract)
Problem and fix
| Stage | Detail |
|---|---|
| Problem | After the v2 deployment in Phase 1, some APIs returned null errors |
| Cause | Older clients sent data without the new column |
| Fix | Added a default value and redeployed |
Lessons
- DB schema changes need at least a two-week deployment plan
- Client compatibility testing is mandatory
Case 3: microservices (deploying several services together)
Situation: updating three services at once (API Gateway, Auth, User)
Deployment order (dependency aware)
Automation (GitHub Actions)
| Stage | Description |
|---|---|
| Build | Parallel builds per service |
| Deploy | Sequential deployment (dependency order) |
| Approval | Manual approval at each stage |
Problem and fix
| Stage | Detail |
|---|---|
| Problem | Auth calls failed after the User service cutover |
| Cause | User was unaware of the API change in the new Auth version |
| Fix | Rolled User back immediately, patched Auth for backward compatibility, redeployed |
Lessons
- Deploying microservices together requires API contract testing
- Consumer-driven contract testing is worth adopting
Summary
The theme across this series is that a verifiable cutover matters more than an automatic one. Tool choice should follow team capability and operational maturity, starting simple and expanding gradually. The checklist, the SLO-based alerts, and the rollback thresholds are what turn a cutover from a hope into a decision.
Key points
| Topic | Core content |
|---|---|
| Fundamentals | Two environments running side by side, immediate rollback available |
| Kubernetes | Argo Rollouts or Istio recommended |
| Cloud | ALB weights, Cloud Run tags, App Service slots |
| CI/CD | Automated verification, manual approval, monitoring |
| Operations | SLO-based monitoring, cost optimization, checklists |
Recommendations
- Start simple: begin with a plain Kubernetes Service selector
- Add sophistication gradually: adopt Argo Rollouts first, Istio later
- Automate: integrate Blue/Green into the CI/CD pipeline
- Monitoring first: build the SLO and alerting system before deploying
- Practice rollbacks: run rollback drills on a regular schedule