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

graph LR A[Commit] --> B[Build] B --> C[Unit Tests] C --> D[Integration Tests] D --> E[Security Scan] E --> F[Package] F --> G[Deploy Staging] G --> H[E2E Tests] H --> I{Manual Gate} I -->|Approved| J[Deploy Prod] I -->|Rejected| K[Rollback] style A fill:#4A90E2,color:#fff style J fill:#27AE60,color:#fff style K fill:#E74C3C,color:#fff
YAML github-actions-pipeline.yml
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
Interview Insight: Discuss trade-offs between branching strategies. Trunk-based development enables faster feedback and reduces merge conflicts but requires discipline and feature flags. GitFlow provides clear release management but can slow down delivery. Modern teams increasingly favor trunk-based with progressive delivery.

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

TypeScript ci-test-optimization.ts
// 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.

Bash artifact-versioning.sh
# 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.

graph LR A[Code Commit] --> B[Build & Test] B --> C[Deploy Staging] C --> D{Continuous
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
Interview Insight: Emphasize that both practices require code to be always deployable. The difference is whether the final push to production is automated. Discuss how feature flags enable continuous deployment even for incomplete features by decoupling deployment from release.

Deployment Strategies

Different deployment strategies balance risk, complexity, and downtime. The choice depends on application architecture, traffic patterns, and business requirements.

Strategy Overview

graph TD A[Deployment Strategies] --> B[Rolling Update] A --> C[Blue-Green] A --> D[Canary] A --> E[Shadow/Dark Launch] B --> B1[Replace instances gradually] B --> B2[No downtime] B --> B3[Slow rollback] C --> C1[Two identical environments] C --> C2[Instant switch] C --> C3[Instant rollback] D --> D1[Gradual traffic shift] D --> D2[Automated metrics] D --> D3[Auto rollback] E --> E1[Duplicate traffic] E --> E2[No user impact] E --> E3[Test in production] style A fill:#34495E,color:#fff style B fill:#3498DB,color:#fff style C fill:#27AE60,color:#fff style D fill:#F39C12,color:#fff style E fill:#9B59B6,color:#fff
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

graph TB LB[Load Balancer] subgraph Blue Environment B1[App v1.0] B2[App v1.0] B3[App v1.0] end subgraph Green Environment G1[App v1.1] G2[App v1.1] G3[App v1.1] end DB[(Shared Database)] LB -->|100% traffic| B1 LB -->|100% traffic| B2 LB -->|100% traffic| B3 LB -.->|0% traffic| G1 LB -.->|0% traffic| G2 LB -.->|0% traffic| G3 B1 --> DB B2 --> DB B3 --> DB G1 -.-> DB G2 -.-> DB G3 -.-> DB style LB fill:#34495E,color:#fff style B1 fill:#3498DB,color:#fff style B2 fill:#3498DB,color:#fff style B3 fill:#3498DB,color:#fff style G1 fill:#27AE60,color:#fff style G2 fill:#27AE60,color:#fff style G3 fill:#27AE60,color:#fff

Implementation

YAML blue-green-deployment.yml
# 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
Bash blue-green-switch.sh
#!/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
Interview Insight: Highlight the instant rollback capability and zero downtime, but discuss the 2x infrastructure cost and database complexity. Blue-green works best for stateless applications or when you can afford duplicate infrastructure.

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

graph LR A[Deploy v2] --> B[1% traffic] B --> C{Metrics OK?} C -->|Yes| D[5% traffic] C -->|No| R1[Rollback] D --> E{Metrics OK?} E -->|Yes| F[25% traffic] E -->|No| R2[Rollback] F --> G{Metrics OK?} G -->|Yes| H[50% traffic] G -->|No| R3[Rollback] H --> I{Metrics OK?} I -->|Yes| J[100% traffic] I -->|No| R4[Rollback] style A fill:#3498DB,color:#fff style J fill:#27AE60,color:#fff style R1 fill:#E74C3C,color:#fff style R2 fill:#E74C3C,color:#fff style R3 fill:#E74C3C,color:#fff style R4 fill:#E74C3C,color:#fff

Implementation with Istio

YAML canary-istio.yml
# 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

TypeScript canary-analysis.ts
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
  }
}
Interview Insight: Emphasize automated analysis and metrics-driven promotion. Discuss how canary reduces blast radius (only 1-5% of users see issues initially) and enables early detection. Mention the importance of both absolute thresholds and relative comparison to baseline.

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
TypeScript feature-flag-service.ts
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

JSON launchdarkly-targeting.json
{
  "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
Interview Insight: Discuss how feature flags enable trunk-based development and continuous deployment even with incomplete features. Mention the kill switch capability for instant rollback without redeployment. Emphasize the importance of flag hygiene to avoid accumulating technical debt.

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

graph TB A[Deploy New Version] --> B[Automated Analysis] B --> C{Initial Health Check} C -->|Pass| D[Shift 10% Traffic] C -->|Fail| R1[Rollback] D --> E[Analyze Metrics] E --> F{Metrics OK?} F -->|Yes| G[Shift 25% Traffic] F -->|No| R2[Rollback] G --> H[Analyze Metrics] H --> I{Metrics OK?} I -->|Yes| J[Shift 50% Traffic] I -->|No| R3[Rollback] J --> K[Analyze Metrics] K --> L{Metrics OK?} L -->|Yes| M[Shift 100% Traffic] L -->|No| R4[Rollback] M --> N[Promote to Stable] style A fill:#3498DB,color:#fff style N fill:#27AE60,color:#fff style R1 fill:#E74C3C,color:#fff style R2 fill:#E74C3C,color:#fff style R3 fill:#E74C3C,color:#fff style R4 fill:#E74C3C,color:#fff

Argo Rollouts Configuration

YAML argo-rollout.yml
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

YAML flagger-canary.yml
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
Interview Insight: Progressive delivery is the evolution of continuous deployment. It automates what DevOps teams previously did manually: gradual rollout with health checks. Emphasize the combination of traffic shifting (canary) + automated analysis (metrics) + automatic rollback. This reduces human toil and reaction time.

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

graph TB DEV[Developer] -->|git push| GIT[Git Repository] GIT -->|webhook| CI[CI Pipeline] CI -->|build & test| REG[Container Registry] CI -->|update manifest| GIT subgraph Kubernetes Cluster ARGO[ArgoCD Agent] API[K8s API Server] PODS[Application Pods] end GIT -->|poll/watch| ARGO ARGO -->|compare & sync| API API -->|deploy| PODS ARGO -->|drift detected| GIT style GIT fill:#34495E,color:#fff style ARGO fill:#F39C12,color:#fff style PODS fill:#27AE60,color:#fff

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

YAML argocd-application.yml
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

Bash gitops-deploy.sh
#!/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
Interview Insight: GitOps inverts the traditional push model (CI pushes to cluster) to a pull model (agent pulls from Git). This improves security (no cluster credentials in CI), enables drift detection, and provides a single source of truth. Discuss the trade-off: added complexity of manifest management but significant operational benefits.

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

YAML traffic-mirroring.yml
# 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

TypeScript synthetic-monitoring.ts
// 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

YAML chaos-experiment.yml
# 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"
Interview Insight: Testing in production is necessary because staging never matches production (traffic patterns, data diversity, infrastructure scale). Emphasize safety mechanisms: traffic mirroring has no user impact, synthetic monitoring uses test accounts, chaos testing runs during low-traffic windows with automatic rollback.

Rollback Strategies

Rollback is the safety net for failed deployments. Different deployment strategies offer different rollback speeds and complexities.

Rollback Decision Framework

graph TD A[Deployment Issue Detected] --> B{Severity} B -->|Critical| C[Immediate Rollback] B -->|High| D{Can Fix Forward Fast?} B -->|Medium/Low| E[Forward Fix] D -->|Yes < 10min| E D -->|No| C C --> F{Deployment Strategy} F -->|Blue-Green| G[Instant Switch] F -->|Canary| H[Stop Promotion] F -->|Rolling| I[Reverse Update] G --> J[Monitor] H --> J I --> J E --> J style A fill:#E74C3C,color:#fff style C fill:#E67E22,color:#fff style E fill:#27AE60,color:#fff style J fill:#3498DB,color:#fff

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

Bash rollback-automation.sh
#!/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
SQL backward-compatible-migration.sql
-- 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)
Interview Insight: The decision to rollback vs. forward fix depends on severity, fix time, and database state. Rollback is safer for critical issues with unclear root cause. Forward fix is better when the issue is understood and fix is quick. Always consider database migrations - they're the primary constraint on rollback strategy.

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.