Blue-green deployment maintains two identical production environments — “blue” running the current release, “green” the new version — and switches traffic only after green passes all tests, giving you zero-downtime releases with instant rollback. The testing discipline is what makes the pattern safe: smoke tests, data-migration validation, and traffic-switch verification on the green environment before a single user touches it. This guide covers how to test each phase of a blue-green release.
TL;DR
- Before the switch, run smoke tests (health endpoint, database connectivity, critical dependencies) and integration tests against the green environment
- Validate database migrations — schema version, indexes, foreign keys — and keep schema changes backwards compatible so rolling back to blue stays possible
- Shift traffic to green gradually, watch error rate and p99 latency at each step, and compare blue and green responses
- Automate rollback: define metric thresholds that switch traffic back to blue
Best for: Teams that release with two identical production environments and need to validate green before the traffic switch Skip if: You cannot run two production environments in parallel — blue-green has the highest resource cost of the strategies compared below
What is Blue-Green Deployment?
Blue-green deployment is a release strategy that maintains two identical production environments: “blue” (current production) and “green” (new version). Traffic switches from blue to green only after the green environment passes all tests, enabling instant rollback if issues arise.
Key benefits:
- Zero downtime during deployments
- Instant rollback capability (just switch traffic back)
- Full production environment testing before going live
- Reduced deployment risk and stress
How it differs from other strategies:
| Strategy | Downtime | Rollback Speed | Resource Cost | Complexity |
|---|---|---|---|---|
| Blue-Green | None | Instant | High (2x) | Medium |
| Rolling | Minimal | Slow | Low | Low |
| Canary | None | Medium | Medium | High |
| Recreate | High | Slow | Low | Very Low |
Testing Fundamentals for Blue-Green Deployments
Pre-Deployment Testing Phase
Before switching traffic to your green environment, you need comprehensive validation:
1. Smoke Tests Quick sanity checks that verify basic functionality:
#!/bin/bash
# smoke-test.sh - Basic health check for green environment
GREEN_URL="https://green.example.com"
# Check application is responding
if ! curl -f -s "${GREEN_URL}/health" > /dev/null; then
echo "❌ Health endpoint not responding"
exit 1
fi
# Verify database connectivity
if ! curl -f -s "${GREEN_URL}/api/db-check" | grep -q "OK"; then
echo "❌ Database connection failed"
exit 1
fi
# Check critical dependencies
for service in redis kafka elasticsearch; do
if ! curl -f -s "${GREEN_URL}/api/check/${service}" | grep -q "healthy"; then
echo "❌ ${service} dependency check failed"
exit 1
fi
done
echo "✅ All smoke tests passed"
2. Integration Tests Verify that all system components work together:
# test_green_integration.py
import pytest
import requests
GREEN_BASE_URL = "https://green.example.com"
def test_user_registration_flow():
"""Test complete user registration workflow"""
# Create user
response = requests.post(f"{GREEN_BASE_URL}/api/users", json={
"email": "test@example.com",
"password": "SecurePass123!"
})
assert response.status_code == 201
user_id = response.json()["id"]
# Verify email sent
email_check = requests.get(f"{GREEN_BASE_URL}/api/emails/{user_id}")
assert email_check.json()["type"] == "verification"
# Complete verification
token = email_check.json()["token"]
verify = requests.post(f"{GREEN_BASE_URL}/api/verify", json={"token": token})
assert verify.status_code == 200
def test_payment_processing():
"""Verify payment gateway integration"""
response = requests.post(f"{GREEN_BASE_URL}/api/payments", json={
"amount": 1000,
"currency": "USD",
"method": "card"
})
assert response.status_code == 200
assert response.json()["status"] == "processed"
3. Database Migration Validation Critical for ensuring data integrity:
-- validate_migration.sql
-- Run these checks before traffic switch
-- 1. Verify schema version
SELECT version FROM schema_migrations
ORDER BY version DESC LIMIT 1;
-- Expected: 20251102_latest_migration
-- 2. Check data consistency
SELECT
(SELECT COUNT(*) FROM users) as total_users,
(SELECT COUNT(*) FROM users WHERE created_at > NOW() - INTERVAL '1 hour') as recent_users;
-- Recent users should be 0 (green is new)
-- 3. Validate indexes
SELECT schemaname, tablename, indexname
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename IN ('users', 'orders', 'products');
-- All expected indexes must exist
-- 4. Check foreign key constraints
SELECT COUNT(*) FROM information_schema.table_constraints
WHERE constraint_type = 'FOREIGN KEY'
AND table_schema = 'public';
-- Should match blue environment count
Post-Switch Validation
After switching traffic to green, monitor these critical metrics:
1. Golden Signals Monitoring
# prometheus-alerts.yml - Monitor green environment
groups:
- name: blue_green_deployment
interval: 30s
rules:
# Latency spike detection
- alert: GreenLatencyHigh
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{env="green"}[5m])) > 1.5
for: 2m
annotations:
summary: "Green environment showing high latency"
# Error rate increase
- alert: GreenErrorRateHigh
expr: rate(http_requests_total{env="green", status=~"5.."}[5m]) > 0.05
for: 1m
annotations:
summary: "Green error rate exceeds 5%"
# Traffic saturation
- alert: GreenSaturation
expr: rate(http_requests_total{env="green"}[1m]) > 10000
for: 5m
annotations:
summary: "Green environment handling high load"
2. Comparison Testing Run parallel traffic analysis between blue and green:
# parallel_test.py - Compare blue vs green responses
import asyncio
import aiohttp
import statistics
async def compare_endpoints(endpoint, iterations=100):
"""Compare response times and results between blue and green"""
blue_times = []
green_times = []
discrepancies = []
async with aiohttp.ClientSession() as session:
for i in range(iterations):
# Test blue
start = asyncio.get_event_loop().time()
async with session.get(f"https://blue.example.com{endpoint}") as resp:
blue_result = await resp.json()
blue_times.append(asyncio.get_event_loop().time() - start)
# Test green
start = asyncio.get_event_loop().time()
async with session.get(f"https://green.example.com{endpoint}") as resp:
green_result = await resp.json()
green_times.append(asyncio.get_event_loop().time() - start)
# Check for discrepancies
if blue_result != green_result:
discrepancies.append({
'iteration': i,
'blue': blue_result,
'green': green_result
})
return {
'blue_avg': statistics.mean(blue_times),
'green_avg': statistics.mean(green_times),
'blue_p99': statistics.quantiles(blue_times, n=100)[98],
'green_p99': statistics.quantiles(green_times, n=100)[98],
'discrepancies': len(discrepancies),
'discrepancy_rate': len(discrepancies) / iterations
}
# Run comparison
results = asyncio.run(compare_endpoints('/api/products'))
print(f"Blue avg: {results['blue_avg']:.3f}s, Green avg: {results['green_avg']:.3f}s")
print(f"Discrepancy rate: {results['discrepancy_rate']*100:.2f}%")
Advanced Testing Techniques
Shadow Traffic Testing
Send duplicate production traffic to green environment without impacting users:
# nginx.conf - Shadow traffic to green environment
upstream blue_backend {
server blue.example.com:8080;
}
upstream green_backend {
server green.example.com:8080;
}
server {
listen 80;
location / {
# Primary traffic goes to blue
proxy_pass http://blue_backend;
# Mirror traffic to green (async, no response used)
mirror /mirror;
mirror_request_body on;
}
location /mirror {
internal;
proxy_pass http://green_backend$request_uri;
proxy_set_header X-Shadow-Request "true";
}
}
Benefits of shadow testing:
- Test green with real production patterns
- No user impact if green fails
- Validate performance under actual load
- Discover edge cases missed in testing
Synthetic Transaction Monitoring
Deploy continuous synthetic tests that mimic real user behavior:
// synthetic-monitor.js - Datadog/New Relic style
const puppeteer = require('puppeteer');
async function runSyntheticTest(environment) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
// Monitor page load time
const startTime = Date.now();
await page.goto(`https://${environment}.example.com`);
const loadTime = Date.now() - startTime;
// Test critical user journey
await page.click('#search-input');
await page.type('#search-input', 'test product');
await page.click('#search-button');
await page.waitForSelector('.search-results');
// Add to cart
await page.click('.product-card:first-child .add-to-cart');
await page.waitForSelector('.cart-notification');
// Verify cart
await page.click('#cart-icon');
const cartItems = await page.$$('.cart-item');
return {
success: cartItems.length > 0,
loadTime: loadTime,
environment: environment,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
error: error.message,
environment: environment
};
} finally {
await browser.close();
}
}
// Run every 5 minutes
setInterval(async () => {
const greenResults = await runSyntheticTest('green');
if (!greenResults.success) {
// Alert on failure
console.error('❌ Green synthetic test failed:', greenResults);
}
}, 5 * 60 * 1000);
Database State Validation
Ensure database consistency between blue and green:
# db_validator.py - Compare database states
import psycopg2
from datetime import datetime, timedelta
def compare_databases(blue_conn, green_conn):
"""Compare critical database metrics between environments"""
checks = []
# 1. Row counts must match (with tolerance for recent writes)
tables = ['users', 'orders', 'products', 'inventory']
for table in tables:
blue_count = execute_query(blue_conn, f"SELECT COUNT(*) FROM {table}")
green_count = execute_query(green_conn, f"SELECT COUNT(*) FROM {table}")
# Allow 1% difference for active writes
tolerance = blue_count * 0.01
if abs(blue_count - green_count) > tolerance:
checks.append({
'table': table,
'status': 'FAIL',
'blue_count': blue_count,
'green_count': green_count,
'difference': abs(blue_count - green_count)
})
else:
checks.append({
'table': table,
'status': 'PASS'
})
# 2. Check recent data replication
cutoff = datetime.now() - timedelta(hours=1)
for table in ['orders', 'user_sessions']:
query = f"SELECT COUNT(*) FROM {table} WHERE updated_at > %s"
blue_recent = execute_query(blue_conn, query, (cutoff,))
green_recent = execute_query(green_conn, query, (cutoff,))
# Green should have similar or more recent data
if green_recent < blue_recent * 0.95:
checks.append({
'check': f'{table}_recent_data',
'status': 'FAIL',
'message': 'Green missing recent updates'
})
return checks
def execute_query(conn, query, params=None):
with conn.cursor() as cur:
cur.execute(query, params)
return cur.fetchone()[0]
Implementation Patterns
Canary Analysis and Gradual Rollout
A blue-green switch is often combined with canary analysis and a gradual rollout:
- Canary analysis - Send a small share of traffic to the new version first
- Failure injection - Inject failures in green to test resilience
- Metric comparison - Compare key metrics between the old and the new version
- Gradual rollout - Increase traffic to green step by step while watching the metrics
- Automatic rollback - Triggered if metrics degrade beyond thresholds
Key metrics to monitor:
- Request latency (p50, p90, p99)
- Error rates by service
- Success rate of critical user flows
Blue/Green on AWS Elastic Beanstalk
Elastic Beanstalk supports blue/green deployments through a CNAME swap. The AWS documentation describes the process:
- Clone the current environment, or launch a new one
- Deploy the new application version to the new environment
- Test the new version on the new environment
- Swap the environment URLs — Elastic Beanstalk swaps the CNAME records of the two environments, redirecting traffic from the old version to the new one
- Verify that the new environment responds on the old environment URL, and do not terminate the old environment until the DNS changes have propagated and the old DNS records have expired
The same page notes that a blue/green deployment requires the environment to run independently of the production database.
Dual-Write Database Migration
A dual-write strategy handles database migrations in blue-green deployments by writing to both the old and the new schema:
Phase 1: Dual-write mode
# Write to both old and new schema
def save_user(user_data):
# Write to old schema (blue)
old_db.users.insert({
'name': user_data['name'],
'email': user_data['email']
})
# Write to new schema (green)
new_db.users.insert({
'full_name': user_data['name'],
'email_address': user_data['email'],
'created_at': datetime.now()
})
Phase 2: Read from new, validate against old
def get_user(user_id):
# Read from new schema
user = new_db.users.find_one({'_id': user_id})
# Async validation against old schema
asyncio.create_task(validate_data(user_id, user))
return user
async def validate_data(user_id, new_data):
old_data = old_db.users.find_one({'_id': user_id})
if not data_matches(old_data, new_data):
log_discrepancy(user_id, old_data, new_data)
Best Practices
✅ Pre-Deployment Checklist
Create a comprehensive checklist for every deployment:
- All automated tests passing in green environment
- Database migrations completed successfully
- Schema changes are backwards compatible
- Feature flags configured for new features
- Load testing completed with production-like traffic
- Security scanning passed (OWASP, dependency audit)
- Smoke tests executed successfully
- Monitoring dashboards created for new features
- Rollback plan documented and tested
- On-call team notified and available
- Customer-facing documentation updated
- Internal runbooks updated
✅ Monitoring and Alerting
Set up comprehensive monitoring before switching traffic:
Critical metrics to track:
# Key Performance Indicators (KPIs)
response_time:
p50: < 100ms
p95: < 300ms
p99: < 1000ms
error_rate:
warning: > 0.5%
critical: > 1%
throughput:
min_rps: 1000 # Should handle normal load
max_rps: 5000 # Should handle peak
resource_usage:
cpu: < 70%
memory: < 80%
disk: < 75%
dependencies:
database_connections: < 80% of pool
cache_hit_rate: > 90%
queue_depth: < 1000 messages
✅ Gradual Traffic Shifting
Don’t switch 100% of traffic immediately:
# traffic_controller.py - Gradual traffic shift
import time
def gradual_traffic_shift(duration_minutes=60):
"""Shift traffic from blue to green over specified duration"""
steps = [1, 5, 10, 25, 50, 75, 100] # Percentage to green
step_duration = duration_minutes * 60 / len(steps)
for percentage in steps:
print(f"Shifting {percentage}% traffic to green...")
update_load_balancer(green_weight=percentage, blue_weight=100-percentage)
# Monitor for issues
time.sleep(step_duration)
metrics = get_green_metrics()
if metrics['error_rate'] > 0.01 or metrics['p99_latency'] > 1.5:
print(f"❌ Metrics degraded at {percentage}%, rolling back")
rollback_to_blue()
return False
print(f"✅ {percentage}% traffic handling well")
return True
✅ Automated Rollback Triggers
Implement automatic rollback based on metrics:
# auto_rollback.py
from prometheus_api_client import PrometheusConnect
prom = PrometheusConnect(url="http://prometheus:9090")
def check_rollback_conditions():
"""Check if automatic rollback should trigger"""
# 1. Error rate spike
error_rate_query = 'rate(http_requests_total{env="green",status=~"5.."}[5m])'
error_rate = prom.custom_query(error_rate_query)[0]['value'][1]
if float(error_rate) > 0.05: # 5% error rate
return True, "Error rate exceeded 5%"
# 2. Latency degradation
latency_query = 'histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{env="green"}[5m]))'
p99_latency = prom.custom_query(latency_query)[0]['value'][1]
if float(p99_latency) > 2.0: # 2 second p99
return True, "P99 latency exceeded 2 seconds"
# 3. Resource exhaustion
cpu_query = 'avg(rate(container_cpu_usage_seconds_total{env="green"}[5m]))'
cpu_usage = prom.custom_query(cpu_query)[0]['value'][1]
if float(cpu_usage) > 0.9: # 90% CPU
return True, "CPU usage exceeded 90%"
return False, None
# Run every 30 seconds
while True:
should_rollback, reason = check_rollback_conditions()
if should_rollback:
print(f"🚨 AUTOMATIC ROLLBACK TRIGGERED: {reason}")
execute_rollback()
send_alert(reason)
break
time.sleep(30)
Common Pitfalls and How to Avoid Them
⚠️ Database Schema Incompatibility
Problem: New code requires schema changes that break old code during rollback.
Solution: Use backwards-compatible migrations:
# BAD - Breaking change
# Migration 1: Add NOT NULL column
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL;
# GOOD - Backwards compatible
# Migration 1: Add nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
# Migration 2: Backfill data
UPDATE users SET phone = 'UNKNOWN' WHERE phone IS NULL;
# Migration 3: Add constraint (deploy after traffic fully on green)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
⚠️ Session State Issues
Problem: User sessions lost or corrupted during traffic switch.
Solution: Use centralized session storage:
# BAD - In-memory sessions (lost on environment switch)
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'secret'
@app.route('/login')
def login():
session['user_id'] = 123 # Stored locally, lost on switch
# GOOD - Redis-backed sessions (persistent across environments)
from flask import Flask
from flask_session import Session
import redis
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://shared-redis:6379')
Session(app)
@app.route('/login')
def login():
session['user_id'] = 123 # Stored in Redis, survives switch
⚠️ Third-Party API Rate Limits
Problem: Green environment gets rate-limited because blue already used quota.
Solution: Request separate API keys or implement smart rate limiting:
# rate_limit_manager.py
class EnvironmentAwareRateLimiter:
def __init__(self, redis_client):
self.redis = redis_client
self.env = os.getenv('ENVIRONMENT') # 'blue' or 'green'
def check_limit(self, api_name, limit_per_hour):
"""Check rate limit with environment-specific keys"""
key = f"ratelimit:{self.env}:{api_name}:{datetime.now().hour}"
current = self.redis.incr(key)
self.redis.expire(key, 3600) # 1 hour TTL
return current <= limit_per_hour
def use_quota(self, api_name):
"""Use quota from shared pool if blue environment"""
if self.env == 'blue':
# Use production quota
return self.check_limit(api_name, 10000)
else:
# Use reduced quota for green testing
return self.check_limit(api_name, 1000)
⚠️ Static Asset Caching
Problem: Users get old JavaScript/CSS from CDN cache after deployment.
Solution: Use cache-busting with versioned assets:
<!-- BAD - Same URL, cache may serve old version -->
<script src="/static/app.js"></script>
<!-- GOOD - Unique URL per build, no cache issues -->
<script src="/static/app.js?v=build-20251102-1534"></script>
<!-- BETTER - Content-based hashing -->
<script src="/static/app.a8f3d9e2.js"></script>
Tools and Frameworks
Terraform for Infrastructure
# blue-green.tf - Complete blue-green setup
resource "aws_lb_target_group" "blue" {
name = "app-blue-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/health"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_lb_target_group" "green" {
name = "app-green-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/health"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_lb_listener_rule" "production" {
listener_arn = aws_lb_listener.main.arn
priority = 100
action {
type = "forward"
target_group_arn = var.active_environment == "blue" ?
aws_lb_target_group.blue.arn :
aws_lb_target_group.green.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
}
Spinnaker for Orchestration
Open-source, multi-cloud continuous delivery platform with built-in support for the blue/green strategy (Spinnaker concepts):
| Feature | Description | Best For |
|---|---|---|
| Pipeline Templates | Reusable deployment workflows | Standardizing deployments |
| Automated Canary Analysis | Statistical comparison of metrics | Risk reduction |
| Multi-Cloud Support | AWS, GCP, Azure, Kubernetes | Hybrid environments |
| RBAC | Role-based access control | Enterprise security |
Pros:
- ✅ Comprehensive deployment strategies support
- ✅ Strong Kubernetes integration
- ✅ Active community
Cons:
- ❌ Complex setup and configuration
- ❌ Steep learning curve
- ❌ Resource-intensive (requires dedicated cluster)
AWS CodeDeploy
Native AWS service for automated deployments:
# appspec.yml - CodeDeploy configuration
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:us-east-1:123456:task-definition/app:2"
LoadBalancerInfo:
ContainerName: "app"
ContainerPort: 8080
PlatformVersion: "LATEST"
Hooks:
- BeforeInstall: "scripts/pre-deployment-tests.sh"
- AfterInstall: "scripts/smoke-tests.sh"
- AfterAllowTestTraffic: "scripts/integration-tests.sh"
- BeforeAllowTraffic: "scripts/validation.sh"
- AfterAllowTraffic: "scripts/post-deployment-monitoring.sh"
Flagger for Kubernetes
Progressive delivery operator for Kubernetes:
# flagger-canary.yml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: app
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: app
service:
port: 8080
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester/
timeout: 5s
metadata:
cmd: "hey -z 1m -q 10 -c 2 http://app:8080/"
Conclusion
Blue-green deployment testing is not just about having two environments—it’s about building confidence through comprehensive validation at every step. By implementing the testing strategies, monitoring practices, and automation tools covered in this guide, you can switch traffic with confidence and roll back quickly when something goes wrong.
Key takeaways:
- Test comprehensively before switching - Smoke tests, integration tests, and database validation are non-negotiable
- Use gradual traffic shifting - Don’t switch 100% at once; monitor metrics at each step
- Automate rollback decisions - Define clear thresholds and let systems react faster than humans can
- Maintain backwards compatibility - Especially critical for database schemas and API contracts
- Monitor the right metrics - Focus on latency, errors, saturation, and traffic (the four golden signals)
Next steps:
- Start with automated smoke tests for your current deployment process
- Implement health checks and monitoring before your next release
- Gradually introduce blue-green deployments to one service at a time
- Build confidence through repetition and continuous improvement
For more DevOps testing strategies, explore our guides on Kubernetes testing, CI/CD pipeline optimization, and infrastructure as code testing.
Additional resources:
FAQ
What is blue-green deployment testing? Blue-green deployment testing is the validation of a new release on the green environment before and after traffic is switched from blue, the current production. It includes smoke tests, integration tests and database migration validation before the switch, then monitoring of latency, error rate and saturation after it. Traffic moves to green only after green passes all tests.
What should you test on the green environment before switching traffic? Run smoke tests that check the health endpoint, database connectivity and critical dependencies, then integration tests for key flows such as user registration and payment processing. Validate database migrations: schema version, data consistency, indexes and foreign key constraints. The pre-deployment checklist also lists load testing with production-like traffic, security scanning and a documented, tested rollback plan.
How do you validate the traffic switch in a blue-green deployment? Do not switch 100% of traffic at once: shift it to green in steps and check error rate and p99 latency at each step. After the switch, monitor the golden signals and compare blue and green responses for discrepancies. Shadow traffic and synthetic transaction tests let you exercise green with real production patterns without impacting users.
How do you handle database migrations in a blue-green deployment? Use backwards-compatible migrations so the old code keeps working if you roll back: add a nullable column, backfill the data, and add the NOT NULL constraint only after traffic is fully on green. Before the switch, verify the schema version, indexes and foreign key constraints, and compare row counts between blue and green. A dual-write strategy writes to both the old and the new schema and validates reads from the new schema against the old one.
How do you prepare and test rollback in a blue-green deployment? Rollback means switching traffic back to the blue environment. The rollback plan should be documented and tested before the release, and schema changes must stay backwards compatible so the old code still works. Automated triggers can roll back when the error rate, p99 latency or CPU usage on green crosses a defined threshold.
See Also
- Continuous Testing in DevOps
- Kitchen-Terraform for Testing: Legacy Maintenance and Migration Guide - Kitchen-Terraform was archived in 2024. Learn how to maintain legacy setups,…
- CI/CD Pipeline Optimization for QA Teams - Optimize pipelines for blue-green deployments
- Test Automation Strategy - Build reliable smoke tests for deployment validation
- API Testing Mastery - Validate API endpoints during environment switches
- Exploratory Testing Guide - Perform exploratory validation on green environments
