Chapter 40: CI/CD & Deployment Strategies
From code commit to production with confidence
CI/CD Pipeline
A CI/CD pipeline automates the journey from source code to production, ensuring consistent, reliable, and rapid delivery. Modern pipelines consist of multiple stages, each with specific responsibilities and quality gates.
Pipeline Stages
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Build application
run: npm run build
- name: Security scan
run: npm audit --audit-level=high
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
- name: Deploy to staging
run: |
aws s3 sync dist/ s3://staging-bucket/
aws cloudfront create-invalidation \
--distribution-id $STAGING_DIST_ID \
--paths "/*"
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
- name: Deploy to production
run: |
aws s3 sync dist/ s3://prod-bucket/
aws cloudfront create-invalidation \
--distribution-id $PROD_DIST_ID \
--paths "/*"
Branching Strategies
| Aspect | Trunk-Based Development | GitFlow |
|---|---|---|
| Branch Lifetime | Short-lived (hours to 1 day) | Long-lived (days to weeks) |
| Main Branches | main (always deployable) | main, develop, release, hotfix |
| Integration | Merge to main multiple times/day | Merge to develop, then release |
| Feature Flags | Required for incomplete features | Optional |
| Release Process | Deploy from main at any time | Create release branch, stabilize, merge |
| Complexity | Low (fewer branches) | High (multiple branch types) |
| Best For | Fast-moving teams, SaaS products | Scheduled releases, multiple versions |
Continuous Integration
Continuous Integration (CI) is the practice of automatically building, testing, and validating code changes as they're committed. The goal is fast feedback to detect and fix issues early.
Core Principles
- Commit Frequently: Multiple commits per day to reduce integration complexity
- Automated Testing: Run unit, integration, and static analysis on every commit
- Fast Feedback: Keep build times under 10 minutes; parallelize tests
- Fix Failures Immediately: Broken builds are highest priority; don't commit on red
- Consistent Environments: Build in containers to ensure reproducibility
- Artifact Management: Store versioned build artifacts for deployment
// Test categorization for fast feedback
// Unit tests: < 5 minutes (run on every commit)
// Integration tests: 5-15 minutes (run on PR)
// E2E tests: 15-30 minutes (run on merge to main)
// jest.config.js
module.exports = {
projects: [
{
displayName: 'unit',
testMatch: ['**/*.test.ts'],
testPathIgnorePatterns: ['/integration/', '/e2e/'],
},
{
displayName: 'integration',
testMatch: ['**/integration/**/*.test.ts'],
setupFilesAfterEnv: ['./test/integration-setup.ts'],
},
{
displayName: 'e2e',
testMatch: ['**/e2e/**/*.test.ts'],
setupFilesAfterEnv: ['./test/e2e-setup.ts'],
testTimeout: 30000,
},
],
};
// package.json scripts
{
"scripts": {
"test:unit": "jest --selectProjects unit",
"test:integration": "jest --selectProjects integration",
"test:e2e": "jest --selectProjects e2e",
"test:ci": "npm run test:unit && npm run test:integration"
}
}
Build Artifacts
CI produces immutable, versioned artifacts that move through environments unchanged. This ensures what you test is what you deploy.
# Semantic versioning with build metadata
VERSION="1.5.2"
BUILD_NUMBER=$GITHUB_RUN_NUMBER
COMMIT_SHA=$(git rev-parse --short HEAD)
ARTIFACT_VERSION="${VERSION}+build.${BUILD_NUMBER}.${COMMIT_SHA}"
# Build Docker image with multiple tags
docker build -t myapp:${ARTIFACT_VERSION} \
-t myapp:${VERSION} \
-t myapp:latest .
# Push to registry
docker push myapp:${ARTIFACT_VERSION}
docker push myapp:${VERSION}
# Store artifact metadata
cat > artifact-manifest.json <
Continuous Delivery vs Deployment
While often confused, Continuous Delivery and Continuous Deployment represent different automation levels in the release process.
Delivery} D -->|Manual Approval| E[Deploy Production] D -->|Automated| F[Deploy Production] E --> G[Monitor] F --> G style D fill:#F39C12,color:#fff style E fill:#3498DB,color:#fff style F fill:#27AE60,color:#fff
| Aspect | Continuous Delivery | Continuous Deployment |
|---|---|---|
| Production Deploy | Manual approval required | Fully automated |
| Release Frequency | Scheduled (daily, weekly) | Every commit (10-100+/day) |
| Human Gate | Product/business approval | Automated quality gates only |
| Risk Profile | Lower (human review) | Requires robust automation |
| Feature Flags | Optional | Essential for incomplete features |
| Rollback Speed | Manual (minutes to hours) | Automated (seconds to minutes) |
| Best For | Regulated industries, B2B SaaS | Consumer web, mobile backends |
Deployment Strategies
Different deployment strategies balance risk, complexity, and downtime. The choice depends on application architecture, traffic patterns, and business requirements.
Strategy Overview
| Strategy | Downtime | Rollback Speed | Resource Cost | Complexity | Risk |
|---|---|---|---|---|---|
| Rolling Update | None | Slow (reverse update) | Low | Low | Medium |
| Blue-Green | None | Instant (switch back) | High (2x infra) | Medium | Low |
| Canary | None | Fast (stop promotion) | Medium | High | Very Low |
| Shadow | None | N/A (no user traffic) | High (duplicate traffic) | High | None |
Blue-Green Deployment
Blue-green deployment maintains two identical production environments. At any time, one (blue) serves live traffic while the other (green) is idle or serving the new version. Traffic is switched atomically between environments.
Architecture
Implementation
# Kubernetes blue-green deployment
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
version: blue # Switch to "green" to cut over
ports:
- port: 80
targetPort: 8080
---
# Blue deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-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
---
# Green deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myapp:1.1.0
ports:
- containerPort: 8080
#!/bin/bash
# Blue-green deployment automation
CURRENT_ENV=$(kubectl get service myapp-service -o jsonpath='{.spec.selector.version}')
NEW_ENV=$([ "$CURRENT_ENV" = "blue" ] && echo "green" || echo "blue")
echo "Current environment: $CURRENT_ENV"
echo "Deploying to: $NEW_ENV"
# Deploy new version to idle environment
kubectl set image deployment/myapp-${NEW_ENV} \
myapp=myapp:${NEW_VERSION}
# Wait for rollout
kubectl rollout status deployment/myapp-${NEW_ENV}
# Run smoke tests against new environment
echo "Running smoke tests..."
NEW_POD_IP=$(kubectl get pods -l version=${NEW_ENV} -o jsonpath='{.items[0].status.podIP}')
curl -f http://${NEW_POD_IP}:8080/health || exit 1
curl -f http://${NEW_POD_IP}:8080/api/test || exit 1
# Switch traffic
echo "Switching traffic to $NEW_ENV"
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"'${NEW_ENV}'"}}}'
# Monitor for 5 minutes
echo "Monitoring new environment..."
sleep 300
# Check error rates
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query?query=rate\(http_errors_total\[5m\]\) | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate too high! Rolling back..."
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"'${CURRENT_ENV}'"}}}'
exit 1
fi
echo "Deployment successful! Old environment ($CURRENT_ENV) is now idle."
Database Challenges
Blue-green deployments become complex with database schema changes:
- Backward-compatible migrations: New version must work with old schema
- Expand-contract pattern: Add new columns/tables (expand), deploy code, remove old columns (contract)
- Database per environment: Avoid shared database, but requires data sync
- Read replicas: Route blue/green to different read replicas of same primary
Canary Deployment
Canary deployment gradually shifts traffic from the old version to the new version, starting with a small percentage and increasing based on automated health metrics. This minimizes blast radius if issues arise.
Progressive Rollout
Implementation with Istio
# Virtual Service for canary routing
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp.example.com
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: myapp
subset: v2
- route:
- destination:
host: myapp
subset: v1
weight: 95
- destination:
host: myapp
subset: v2
weight: 5 # Start with 5% canary traffic
---
# Destination Rule defining subsets
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp
spec:
host: myapp
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
Automated Canary Analysis
interface CanaryMetrics {
errorRate: number;
latencyP99: number;
successRate: number;
}
interface CanaryConfig {
stages: { weight: number; duration: number }[];
thresholds: {
maxErrorRate: number;
maxLatency: number;
minSuccessRate: number;
};
}
class CanaryDeployment {
async executeCanary(config: CanaryConfig): Promise {
for (const stage of config.stages) {
console.log(`Shifting ${stage.weight}% traffic to canary`);
await this.updateTrafficWeight(stage.weight);
// Wait for metrics to stabilize
await this.sleep(stage.duration);
// Collect metrics from both versions
const baselineMetrics = await this.collectMetrics('v1');
const canaryMetrics = await this.collectMetrics('v2');
// Compare metrics
const passed = this.analyzeMetrics(
baselineMetrics,
canaryMetrics,
config.thresholds
);
if (!passed) {
console.error('Canary analysis failed! Rolling back...');
await this.rollback();
return false;
}
console.log(`Stage ${stage.weight}% passed`);
}
console.log('Canary deployment successful!');
return true;
}
private analyzeMetrics(
baseline: CanaryMetrics,
canary: CanaryMetrics,
thresholds: CanaryConfig['thresholds']
): boolean {
// Absolute thresholds
if (canary.errorRate > thresholds.maxErrorRate) {
console.error(`Error rate ${canary.errorRate} exceeds threshold`);
return false;
}
if (canary.latencyP99 > thresholds.maxLatency) {
console.error(`P99 latency ${canary.latencyP99}ms exceeds threshold`);
return false;
}
if (canary.successRate < thresholds.minSuccessRate) {
console.error(`Success rate ${canary.successRate} below threshold`);
return false;
}
// Relative comparison (canary shouldn't be significantly worse)
const errorRateIncrease =
(canary.errorRate - baseline.errorRate) / baseline.errorRate;
if (errorRateIncrease > 0.2) { // 20% increase
console.error('Error rate increased by more than 20%');
return false;
}
const latencyIncrease =
(canary.latencyP99 - baseline.latencyP99) / baseline.latencyP99;
if (latencyIncrease > 0.3) { // 30% increase
console.error('Latency increased by more than 30%');
return false;
}
return true;
}
private async collectMetrics(version: string): Promise {
// Query Prometheus for metrics
const errorRate = await this.queryPrometheus(
`rate(http_requests_total{version="${version}",code=~"5.."}[5m])`
);
const latencyP99 = await this.queryPrometheus(
`histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{version="${version}"}[5m]))`
);
const successRate = await this.queryPrometheus(
`rate(http_requests_total{version="${version}",code=~"2.."}[5m])`
);
return { errorRate, latencyP99, successRate };
}
private async updateTrafficWeight(weight: number): Promise {
// Update Istio VirtualService weights
const vs = await this.k8s.getVirtualService('myapp');
vs.spec.http[0].route[0].weight = 100 - weight; // v1
vs.spec.http[0].route[1].weight = weight; // v2
await this.k8s.updateVirtualService(vs);
}
private async rollback(): Promise {
await this.updateTrafficWeight(0); // Route all traffic to v1
}
}
Feature Flags
Feature flags (feature toggles) decouple deployment from release, allowing code to be deployed to production while features remain hidden until explicitly enabled. This enables progressive rollout, A/B testing, and instant kill switches.
Feature Flag Types
- Release toggles: Enable/disable incomplete features (short-lived)
- Experiment toggles: A/B testing different implementations
- Ops toggles: Kill switches for high-load features
- Permission toggles: Role-based or user-based access
import { LDClient, LDContext } from 'launchdarkly-node-server-sdk';
interface FeatureFlagContext {
userId: string;
userEmail: string;
country: string;
plan: 'free' | 'pro' | 'enterprise';
custom?: Record;
}
class FeatureFlagService {
private client: LDClient;
constructor(sdkKey: string) {
this.client = LDClient.init(sdkKey);
}
async isEnabled(
flagKey: string,
context: FeatureFlagContext,
defaultValue = false
): Promise {
const ldContext: LDContext = {
kind: 'user',
key: context.userId,
email: context.userEmail,
country: context.country,
plan: context.plan,
...context.custom,
};
return await this.client.variation(flagKey, ldContext, defaultValue);
}
async getVariation(
flagKey: string,
context: FeatureFlagContext,
defaultValue: T
): Promise {
const ldContext: LDContext = {
kind: 'user',
key: context.userId,
email: context.userEmail,
country: context.country,
plan: context.plan,
...context.custom,
};
return await this.client.variation(flagKey, ldContext, defaultValue);
}
async getAllFlags(context: FeatureFlagContext): Promise> {
const ldContext: LDContext = {
kind: 'user',
key: context.userId,
email: context.userEmail,
country: context.country,
plan: context.plan,
...context.custom,
};
return await this.client.allFlagsState(ldContext);
}
async close(): Promise {
await this.client.close();
}
}
// Usage in application
const flagService = new FeatureFlagService(process.env.LAUNCHDARKLY_SDK_KEY!);
app.get('/api/search', async (req, res) => {
const user = req.user;
// Simple boolean flag
const useNewSearch = await flagService.isEnabled(
'new-search-algorithm',
{
userId: user.id,
userEmail: user.email,
country: user.country,
plan: user.plan,
}
);
if (useNewSearch) {
return res.json(await newSearchAlgorithm(req.query));
}
return res.json(await legacySearch(req.query));
});
app.get('/api/recommendations', async (req, res) => {
const user = req.user;
// Multi-variant flag for A/B testing
const algorithm = await flagService.getVariation(
'recommendation-algorithm',
{
userId: user.id,
userEmail: user.email,
country: user.country,
plan: user.plan,
},
'control'
);
switch (algorithm) {
case 'collaborative-filtering':
return res.json(await collaborativeFiltering(user.id));
case 'content-based':
return res.json(await contentBased(user.id));
case 'hybrid':
return res.json(await hybridRecommendations(user.id));
default:
return res.json(await legacyRecommendations(user.id));
}
});
Progressive Rollout with Feature Flags
{
"flag": "new-checkout-flow",
"targeting": [
{
"variation": "enabled",
"values": ["internal-employee-segment"]
},
{
"variation": "enabled",
"values": ["beta-tester-segment"]
},
{
"variation": "enabled",
"clauses": [
{
"attribute": "email",
"op": "endsWith",
"values": ["@company.com"]
}
]
},
{
"variation": "enabled",
"clauses": [
{
"attribute": "plan",
"op": "in",
"values": ["enterprise"]
}
]
}
],
"rollout": {
"variations": [
{ "variation": "disabled", "weight": 95000 },
{ "variation": "enabled", "weight": 5000 }
]
},
"offVariation": "disabled"
}
Feature Flag Best Practices
- Clean up old flags: Remove flags after full rollout (technical debt)
- Flag naming: Use descriptive names with dates (checkout-redesign-2026-q2)
- Default to safe: If flag service is down, default to stable behavior
- Monitor flag usage: Track which flags are evaluated, latency, errors
- Test both paths: Ensure both enabled/disabled code paths work
- Avoid nested flags: Complex combinations create testing nightmare
Progressive Delivery
Progressive delivery combines canary deployments, feature flags, and automated analysis to deliver features gradually and safely. Tools like Argo Rollouts and Flagger automate the entire process from traffic shifting to rollback decisions.
Progressive Delivery Workflow
Argo Rollouts Configuration
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 5
strategy:
canary:
# Canary service (for analysis)
canaryService: myapp-canary
# Stable service (production traffic)
stableService: myapp-stable
# Traffic routing
trafficRouting:
istio:
virtualService:
name: myapp
routes:
- primary
# Progressive stages
steps:
- setWeight: 5
- pause: { duration: 5m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency
- setWeight: 10
- pause: { duration: 5m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency
- setWeight: 25
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency
- setWeight: 50
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency
- setWeight: 75
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency
revisionHistoryLimit: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:v1.2.0
ports:
- containerPort: 8080
---
# Analysis Template: Success Rate
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{status=~"2.."}[5m]))
/
sum(rate(http_requests_total[5m]))
---
# Analysis Template: Latency
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: latency
spec:
metrics:
- name: latency-p99
interval: 1m
successCondition: result[0] <= 500
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) * 1000
Flagger for Service Mesh
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
# Service mesh provider
provider: istio
# Deployment strategy
progressDeadlineSeconds: 600
service:
port: 8080
targetPort: 8080
# Canary analysis
analysis:
interval: 1m
threshold: 10
maxWeight: 50
stepWeight: 5
# Metrics
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
# Webhooks for custom checks
webhooks:
- name: load-test
url: http://loadtest-service/
timeout: 5s
metadata:
type: cmd
cmd: "hey -z 1m -q 10 -c 2 http://myapp-canary:8080/"
- name: smoke-test
url: http://smoke-test-service/
timeout: 30s
# Alerting
alerts:
- name: slack
severity: error
providerRef:
name: slack
namespace: flagger
GitOps
GitOps uses Git as the single source of truth for declarative infrastructure and applications. Changes are made via pull requests, and automated agents continuously reconcile the actual state with the desired state in Git.
GitOps Architecture
GitOps Principles
- Declarative: Entire system state described declaratively (YAML manifests)
- Versioned: Desired state stored in Git (versioned, immutable history)
- Pulled automatically: Agents pull changes from Git (no direct cluster access)
- Continuously reconciled: Agents ensure actual state matches desired state
ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-production
namespace: argocd
spec:
# Source repository
source:
repoURL: https://github.com/company/k8s-manifests
targetRevision: main
path: apps/myapp/overlays/production
# Destination cluster
destination:
server: https://kubernetes.default.svc
namespace: production
# Sync policy
syncPolicy:
automated:
prune: true # Delete resources not in Git
selfHeal: true # Revert manual changes
allowEmpty: false
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Health check
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore HPA-managed replicas
GitOps Workflow
#!/bin/bash
# GitOps deployment workflow
# 1. CI builds and pushes image
docker build -t myapp:${VERSION} .
docker push myapp:${VERSION}
# 2. Update manifest in Git
git clone https://github.com/company/k8s-manifests
cd k8s-manifests
# Update image tag in Kustomization
cd apps/myapp/overlays/production
kustomize edit set image myapp=myapp:${VERSION}
# Commit and push
git add .
git commit -m "chore: update myapp to ${VERSION}"
git push origin main
# 3. ArgoCD detects change and syncs automatically
# (or trigger manually)
argocd app sync myapp-production
# 4. Monitor rollout
argocd app wait myapp-production --health --timeout 600
GitOps Benefits
- Audit trail: Every change in Git history
- Disaster recovery: Cluster state can be recreated from Git
- Security: No direct cluster credentials needed for CI/CD
- Drift detection: Manual changes detected and reverted
- Multi-cluster: Same workflow for dev/staging/prod clusters
Testing in Production
Testing in production doesn't mean skipping pre-production testing. It means validating behavior with real user traffic, real data, and real infrastructure that staging can never fully replicate.
Testing Strategies
- Traffic mirroring (shadowing): Duplicate production traffic to new version without affecting users
- Synthetic monitoring: Automated tests running continuously against production
- Chaos testing: Inject failures to validate resilience
- A/B testing: Compare metrics between versions with real users
- Observability-driven development: Rich telemetry enables debugging in production
Traffic Mirroring
# Istio VirtualService for traffic mirroring
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp.example.com
http:
- match:
- uri:
prefix: "/api"
route:
- destination:
host: myapp
subset: stable
weight: 100
mirror:
host: myapp
subset: canary
mirrorPercentage:
value: 100 # Mirror 100% of traffic
Synthetic Monitoring
// Continuous synthetic tests against production
import { chromium } from 'playwright';
class SyntheticMonitor {
async runUserJourney(): Promise {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
try {
// Critical user journey
await page.goto('https://app.example.com');
await page.click('button:has-text("Sign In")');
await page.fill('input[name="email"]', 'synthetic@example.com');
await page.fill('input[name="password"]', process.env.SYNTHETIC_PASSWORD!);
await page.click('button[type="submit"]');
// Verify dashboard loaded
await page.waitForSelector('.dashboard', { timeout: 5000 });
// Perform key action
await page.click('button:has-text("Create")');
await page.fill('input[name="title"]', 'Synthetic Test');
await page.click('button:has-text("Save")');
// Verify success
await page.waitForSelector('.success-message', { timeout: 5000 });
this.reportSuccess('user-journey-complete');
} catch (error) {
this.reportFailure('user-journey-failed', error);
throw error;
} finally {
await browser.close();
}
}
private reportSuccess(metric: string): void {
// Send to monitoring system
this.cloudwatch.putMetricData({
Namespace: 'SyntheticMonitoring',
MetricData: [{
MetricName: metric,
Value: 1,
Unit: 'Count',
Timestamp: new Date(),
}],
});
}
private reportFailure(metric: string, error: any): void {
this.cloudwatch.putMetricData({
Namespace: 'SyntheticMonitoring',
MetricData: [{
MetricName: metric,
Value: 1,
Unit: 'Count',
Timestamp: new Date(),
}],
});
// Alert on-call
this.pagerduty.trigger({
summary: `Synthetic monitor failed: ${metric}`,
severity: 'critical',
details: { error: error.message, stack: error.stack },
});
}
}
// Run every 5 minutes
setInterval(() => new SyntheticMonitor().runUserJourney(), 5 * 60 * 1000);
Chaos Engineering
# Chaos Mesh experiment
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: pod-failure-experiment
spec:
action: pod-failure
mode: one
duration: "30s"
selector:
namespaces:
- production
labelSelectors:
app: myapp
scheduler:
cron: "0 2 * * *" # Run at 2 AM daily
---
# Network delay experiment
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: network-delay-experiment
spec:
action: delay
mode: one
selector:
namespaces:
- production
labelSelectors:
app: myapp
delay:
latency: "200ms"
correlation: "100"
jitter: "50ms"
duration: "5m"
Rollback Strategies
Rollback is the safety net for failed deployments. Different deployment strategies offer different rollback speeds and complexities.
Rollback Decision Framework
Rollback Speed Comparison
| Strategy | Rollback Method | Speed | Complexity | User Impact |
|---|---|---|---|---|
| Blue-Green | Switch load balancer back | Instant (seconds) | Low | None (instant switch) |
| Canary | Set traffic weight to 0% | Fast (< 1 minute) | Low | Minimal (5-10% affected) |
| Rolling Update | Reverse update all pods | Slow (5-15 minutes) | Medium | Gradual (during rollback) |
| Feature Flag | Disable flag | Instant (seconds) | Low | None (code still deployed) |
Rollback Implementation
#!/bin/bash
# Automated rollback script
DEPLOYMENT=$1
NAMESPACE=${2:-production}
echo "Initiating rollback for $DEPLOYMENT in $NAMESPACE"
# Get current and previous revisions
CURRENT_REVISION=$(kubectl rollout history deployment/$DEPLOYMENT -n $NAMESPACE | tail -n 1 | awk '{print $1}')
PREVIOUS_REVISION=$((CURRENT_REVISION - 1))
echo "Rolling back from revision $CURRENT_REVISION to $PREVIOUS_REVISION"
# Execute rollback
kubectl rollout undo deployment/$DEPLOYMENT -n $NAMESPACE --to-revision=$PREVIOUS_REVISION
# Wait for rollback to complete
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE --timeout=300s
if [ $? -eq 0 ]; then
echo "Rollback successful"
# Verify health
READY_REPLICAS=$(kubectl get deployment $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.status.readyReplicas}')
DESIRED_REPLICAS=$(kubectl get deployment $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.spec.replicas}')
if [ "$READY_REPLICAS" = "$DESIRED_REPLICAS" ]; then
echo "All replicas healthy: $READY_REPLICAS/$DESIRED_REPLICAS"
else
echo "WARNING: Only $READY_REPLICAS/$DESIRED_REPLICAS replicas ready"
exit 1
fi
# Notify team
curl -X POST https://hooks.slack.com/services/XXX \
-d "{\"text\":\"✅ Rollback successful: $DEPLOYMENT to revision $PREVIOUS_REVISION\"}"
else
echo "Rollback failed!"
curl -X POST https://hooks.slack.com/services/XXX \
-d "{\"text\":\"❌ Rollback FAILED: $DEPLOYMENT in $NAMESPACE\"}"
exit 1
fi
Database Rollback Challenges
Database changes are the hardest part of rollback. Code can be reverted instantly, but database state cannot.
- Backward-compatible migrations: New code works with old schema, old code works with new schema
- Multi-phase migrations:
- Phase 1: Add new columns (nullable), deploy code that writes to both
- Phase 2: Backfill data, deploy code that reads from new columns
- Phase 3: Remove old columns (after rollback window)
- Database versioning: Store schema version in database, code checks compatibility
- Data replication: Keep old and new columns during transition period
- Forward-fix bias: For schema changes, forward fixes often safer than rollback
-- Phase 1: Add new column (backward compatible)
-- Old code ignores it, new code writes to it
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMP NULL;
-- Phase 2: Backfill data (background job)
-- UPDATE users SET email_verified_at = verified_at WHERE email_verified = true;
-- Phase 3: Deploy new code that reads from email_verified_at
-- Phase 4: After rollback window (e.g., 7 days), remove old column
-- ALTER TABLE users DROP COLUMN email_verified;
Forward Fix vs Rollback
| Factor | Prefer Rollback | Prefer Forward Fix |
|---|---|---|
| Severity | Critical (data loss, security) | Medium (functional bug) |
| Fix Time | Unknown or > 15 minutes | Known and < 10 minutes |
| Database Changes | None or backward-compatible | Irreversible schema changes |
| Root Cause | Unclear, needs investigation | Clear, simple fix available |
| Rollback Risk | Low (tested strategy) | High (complex dependencies) |
Key Takeaways
- CI/CD Pipeline: Automates commit-to-production with stages (build, test, deploy). Trunk-based development enables fast delivery, GitFlow provides structured releases.
- Continuous Integration: Frequent commits, automated testing, fast feedback (< 10 min builds). Produces immutable artifacts that move through environments unchanged.
- Delivery vs Deployment: Delivery requires manual approval to production, deployment is fully automated. Both require code to be always deployable.
- Deployment Strategies: Rolling (gradual replacement), blue-green (instant switch), canary (percentage-based), shadow (duplicate traffic). Trade-offs between speed, cost, and risk.
- Blue-Green: Two identical environments, instant switch, instant rollback. Requires 2x infrastructure. Database migrations are the main challenge.
- Canary: Gradual rollout (1% → 5% → 25% → 100%) with automated metrics analysis. Minimizes blast radius, enables early detection. Auto-rollback on failure.
- Feature Flags: Decouple deployment from release. Enable progressive rollout, A/B testing, kill switches. Essential for trunk-based development. Clean up old flags to avoid debt.
- Progressive Delivery: Combines canary + automated analysis + feature flags. Argo Rollouts and Flagger automate traffic shifting and rollback decisions based on metrics.
- GitOps: Git as single source of truth. Pull model (agent pulls from Git) vs push model (CI pushes to cluster). Benefits: audit trail, disaster recovery, drift detection.
- Testing in Production: Traffic mirroring (no user impact), synthetic monitoring (continuous validation), chaos testing (resilience). Necessary because staging never matches production.
- Rollback: Strategy determines speed (blue-green: instant, canary: fast, rolling: slow). Database migrations constrain rollback. Forward fix vs rollback depends on severity and fix time.