Module 5: Security Scanning and Smart Rollback

Automatic Rollback with Triggers

Automatic Rollback with Triggers

Overview

This capsule covers the last line of defense of the pipeline: automatic rollback when something goes wrong in production. Even if you have code review, security scanning, readiness validation, and human approval, there will always be a deploy that fails in production. The difference between a team that survives those incidents and one that suffers weeks of fallout is how long it takes to revert.

You'll learn to define which metrics to monitor post-deploy, configure automatic rollback triggers, execute the revert without human intervention when the signals are clear, and maintain the right balance between "aggressive rollback" (false positives = unnecessary downtime) and "conservative rollback" (slow response = more affected users).

By the end, you'll have a system where a failed deploy is reverted in less than 5 minutes automatically — and where the rollback explains what happened (which capsule 05 covers).


Why Manual Rollback Fails

MANUAL ROLLBACK IN A REAL INCIDENT:

T+0:    Deploy to production
T+2min: Metrics start to degrade
T+5min: Alert reaches on-call (if configured)
T+8min: On-call opens the laptop
T+12min: Identifies it's post-deploy
T+15min: Decides to roll back
T+18min: Finds the exact command
T+22min: Runs the revert
T+25min: Verifies the metric came back

TOTAL TIME: ~25 minutes of degradation
AFFECTED USERS: variable, potentially thousands

Versus automatic rollback:

AUTOMATIC ROLLBACK:

T+0:    Deploy to production
T+2min: Metrics degrade (error rate >5%)
T+3min: Trigger detects the threshold
T+3min: Automatic rollback executing
T+5min: Metrics stabilized
T+5min: On-call notified of the completed rollback

TOTAL TIME: ~5 minutes of degradation
AFFECTED USERS: an order of magnitude lower

5 min vs 25 min is the difference between an almost-invisible blip and an incident your team will remember.


The Metrics That Trigger Rollback

TIER 1 — Clear symptoms (automatic rollback appropriate):

  ✅ Error rate rises significantly
     → Typical threshold: >2% over baseline
     → Observation time: 2-5 minutes sustained
  
  ✅ Latency p95 rises significantly
     → Typical threshold: 2-3x the baseline
     → Observation time: 3-5 minutes sustained
  
  ✅ Health check fails
     → Threshold: 3+ consecutive failed health checks
     → Observation time: immediate

TIER 2 — Symptoms that require judgment (alert for a human):

  ⚠️  Throughput drops
     → Can be real (bug) or expected (behavior change)
     → Better: alert on-call, not automatic rollback
  
  ⚠️  Memory/CPU rises
     → Can be a memory leak or a change in workload
     → Better: alert on-call

TIER 3 — Indirect symptoms (don't trigger rollback):

  ❌ Business metrics (signups, conversions)
     → Too much noise from user behavior
  
  ❌ Errors from a third party (Stripe, AWS)
     → It's not our deploy

The rule: automatic rollback only on Tier 1. Other metrics → human alert that decides.


Monitoring Setup

Before configuring rollback, you need monitoring with metrics accessible via an API. Typical stack:

APPLICATION METRICS:
  → Prometheus + Grafana (open source, self-hosted)
  → Datadog (paid, full-featured)
  → New Relic (paid, focus on APM)
  → CloudWatch (AWS native)

LOGS / ERRORS:
  → Sentry (errors)
  → Loki / Splunk / Elasticsearch (logs)

INFRASTRUCTURE:
  → Same tools, cluster metrics

Any of them works. The important thing is to have an API accessible from the workflow that says: "in the last N minutes, was the error rate >X%?"


The Workflow with Rollback

# .github/workflows/deploy-with-rollback.yml
name: Deploy with Auto-Rollback

on:
  push:
    branches: [main]

permissions:
  contents: read
  deployments: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    outputs:
      previous_release: ${{ steps.deploy.outputs.previous_release }}
      new_release: ${{ steps.deploy.outputs.new_release }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture previous release
        id: previous
        run: |
          # Your mechanism: git tag, kubectl, etc.
          PREV=$(./scripts/get_current_release.sh production)
          echo "previous=$PREV" >> $GITHUB_OUTPUT
      
      - name: Deploy
        id: deploy
        run: |
          NEW=$(./scripts/deploy.sh production)
          echo "previous_release=${{ steps.previous.outputs.previous }}" >> $GITHUB_OUTPUT
          echo "new_release=$NEW" >> $GITHUB_OUTPUT
      
      - name: Initial smoke tests
        run: ./scripts/smoke_tests.sh https://app.example.com

  monitor-and-rollback:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      
      - run: pip install requests anthropic
      
      - name: Monitor metrics for 10 minutes
        id: monitor
        env:
          METRICS_API_URL: ${{ secrets.METRICS_API_URL }}
          METRICS_API_TOKEN: ${{ secrets.METRICS_API_TOKEN }}
          DEPLOY_TIME: ${{ needs.deploy.outputs.new_release }}
        run: |
          python scripts/monitor_post_deploy.py
          echo "rollback_needed=$?" >> $GITHUB_OUTPUT
      
      - name: Execute rollback if needed
        if: steps.monitor.outputs.rollback_needed == '1'
        env:
          PREVIOUS_RELEASE: ${{ needs.deploy.outputs.previous_release }}
        run: |
          echo "🚨 Rolling back to $PREVIOUS_RELEASE"
          ./scripts/rollback.sh production "$PREVIOUS_RELEASE"
      
      - name: Notify team of rollback
        if: steps.monitor.outputs.rollback_needed == '1'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: python scripts/notify_rollback.py
      
      - name: Notify success
        if: steps.monitor.outputs.rollback_needed == '0'
        run: echo "✅ Deploy stable, no rollback needed"

The Monitoring Script

"""scripts/monitor_post_deploy.py

Monitors post-deploy metrics. Returns exit 1 if a rollback is needed.
"""
import json
import os
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
import requests


@dataclass
class MetricThreshold:
    name: str
    threshold: float
    description: str
    sustained_minutes: int = 3  # how long it must be over the threshold


THRESHOLDS = [
    MetricThreshold(
        name="error_rate",
        threshold=0.02,  # 2%
        description="Error rate > 2% sustained for 3 min",
        sustained_minutes=3,
    ),
    MetricThreshold(
        name="latency_p95",
        threshold=2.0,  # 2x baseline
        description="Latency p95 > 2x baseline sustained for 5 min",
        sustained_minutes=5,
    ),
    MetricThreshold(
        name="health_check_failures",
        threshold=3,
        description="3+ consecutive failed health checks",
        sustained_minutes=1,
    ),
]


def fetch_metric(name: str, window_minutes: int = 1) -> float | None:
    """Get a metric from the monitoring system."""
    api_url = os.environ.get("METRICS_API_URL")
    api_token = os.environ.get("METRICS_API_TOKEN")
    
    if not api_url:
        # Mock for testing
        # In real production, query Prometheus/Datadog/etc.
        return mock_metric(name)
    
    headers = {"Authorization": f"Bearer {api_token}"}
    params = {
        "metric": name,
        "window": f"{window_minutes}m",
    }
    
    try:
        r = requests.get(f"{api_url}/query", headers=headers, params=params, timeout=10)
        if r.ok:
            data = r.json()
            return data.get("value")
    except Exception as e:
        print(f"WARNING: failed to fetch {name}: {e}", file=sys.stderr)
    
    return None


def mock_metric(name: str) -> float:
    """Mock for demo — in production replace with a real API."""
    # Simulates normal values by default
    return {
        "error_rate": 0.005,
        "latency_p95": 1.0,
        "health_check_failures": 0,
    }.get(name, 0.0)


def check_threshold(threshold: MetricThreshold) -> tuple[bool, float]:
    """Check whether the metric is sustainedly over the threshold.
    
    Returns (is_over_threshold, current_value).
    """
    samples = []
    samples_needed = threshold.sustained_minutes
    
    for i in range(samples_needed):
        value = fetch_metric(threshold.name, window_minutes=1)
        if value is not None:
            samples.append(value)
        else:
            samples.append(0)  # assume normal if there's no data
        
        if i < samples_needed - 1:
            time.sleep(60)  # wait 1 min between samples
    
    # All samples over the threshold
    all_over = all(s > threshold.threshold for s in samples)
    avg_value = sum(samples) / len(samples) if samples else 0
    
    return all_over, avg_value


def main() -> int:
    """Monitor for 10 minutes and decide whether a rollback is needed."""
    print(f"=== Post-deploy monitoring ({datetime.utcnow().isoformat()}) ===\n")
    
    # Wait 1 min after the deploy so the metrics stabilize
    print("Waiting 60s for metrics to stabilize...")
    time.sleep(60)
    
    # Check each threshold
    for threshold in THRESHOLDS:
        print(f"Monitoring {threshold.name}...")
        is_over, value = check_threshold(threshold)
        
        if is_over:
            print(f"  🚨 ALERT: {threshold.description}")
            print(f"  Current value: {value}")
            print(f"  Action: ROLLBACK")
            
            # Save rollback info for the following steps
            with open("rollback_reason.json", "w") as f:
                json.dump({
                    "metric": threshold.name,
                    "threshold": threshold.threshold,
                    "current_value": value,
                    "description": threshold.description,
                    "timestamp": datetime.utcnow().isoformat(),
                }, f, indent=2)
            
            return 1  # exit 1 → rollback needed
        
        print(f"  ✅ OK: {threshold.name} = {value}")
    
    print("\n✅ Monitoring complete. Deploy stable.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

The Rollback Script

#!/bin/bash
# scripts/rollback.sh — execute a rollback to the previous release
set -euo pipefail

ENV="${1:?'Usage: rollback.sh <env> <previous_release>'}"
PREVIOUS_RELEASE="${2:?'Usage: rollback.sh <env> <previous_release>'}"

echo "[rollback] Rolling back $ENV to $PREVIOUS_RELEASE..."

# Adjust according to your platform:

# Kubernetes
# kubectl rollout undo deployment/app -n $ENV --to-revision=$PREVIOUS_RELEASE

# Heroku
# heroku releases:rollback v$PREVIOUS_RELEASE --app $ENV

# AWS ECS
# aws ecs update-service --cluster $ENV --service app \
#   --task-definition app:$PREVIOUS_RELEASE

# Vercel / Netlify
# vercel rollback $PREVIOUS_RELEASE

# Custom deploy script
./scripts/deploy.sh "$ENV" --release "$PREVIOUS_RELEASE"

echo "[rollback] Rollback to $PREVIOUS_RELEASE complete"

# Verify smoke tests
./scripts/smoke_tests.sh https://app.example.com

echo "[rollback] Verification complete"

Post-Rollback Notification

"""scripts/notify_rollback.py

Notify the team of the automatic rollback with context.
"""
import json
import os
import sys
from pathlib import Path
import requests
from anthropic import Anthropic


def generate_notification(reason: dict) -> str:
    """Generate a notification message with context."""
    client = Anthropic()
    
    prompt = f"""The system just did an automatic rollback in production.

CONTEXT:
{json.dumps(reason, indent=2)}

Generate a BRIEF message for Slack that:
1. Starts with "🚨 Auto-rollback executed"
2. Explains which metric triggered the rollback
3. Says what on-call does now (verify stabilization, investigate root cause)
4. Is professional and actionable, maximum 100 words

Output: only the message, with no additional text.
"""
    
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=300,
        messages=[{"role": "user", "content": prompt}],
    )
    
    return response.content[0].text.strip()


def send_slack(message: str, webhook: str):
    """Send to Slack."""
    payload = {"text": message, "username": "Auto-Rollback Bot", "icon_emoji": ":rotating_light:"}
    r = requests.post(webhook, json=payload, timeout=10)
    if not r.ok:
        print(f"WARNING: Slack failed: {r.status_code}", file=sys.stderr)


def main() -> int:
    reason_file = Path("rollback_reason.json")
    if not reason_file.exists():
        print("ERROR: rollback_reason.json not found", file=sys.stderr)
        return 1
    
    reason = json.loads(reason_file.read_text())
    message = generate_notification(reason)
    
    print("--- Notification ---")
    print(message)
    print("---")
    
    webhook = os.environ.get("SLACK_WEBHOOK")
    if webhook:
        send_slack(message, webhook)
        print("Notification sent to Slack")
    
    # Additional: PagerDuty for urgent incidents
    pd_token = os.environ.get("PAGERDUTY_TOKEN")
    if pd_token:
        # Create an incident in PagerDuty
        # ...
        pass
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

Calibrating the Thresholds

The threshold values are specific to your system. Calibrate them based on:

DATA TO COLLECT:
1. Normal baseline: what's the typical error rate? Latency p95?
2. Historical variability: how much does it naturally fluctuate?
3. Previous false positives: were there alerts that weren't real?

GENERAL RULE:
- Threshold = baseline_max + 2-3x standard deviation
- Sustained time: enough to rule out spikes (2-5 min typical)
- When in doubt, a more permissive threshold (rollback only if clear)

Calibration example

If your normal error rate is 0.5% with a standard deviation of 0.2%:

  • Baseline + 3σ = 0.5% + 0.6% = ~1.1%
  • Conservative threshold: 2% (clearly over baseline)
  • Sustained: 3 min (rules out momentary spikes)

Common Pitfalls

Error 1: Aggressive rollback (false positives)

Symptom: Frequent rollbacks on deploys that had no real bug. The team loses confidence.

Why it happens: Thresholds too low or observation time too short.

How to fix it: Increase thresholds and/or sustained time. Better to be conservative with rollback than with a human alert.

Error 2: Conservative rollback (false negatives)

Symptom: A bug reaches production, metrics degrade, but the rollback doesn't trigger because "it didn't reach the threshold".

Why it happens: A threshold too high or a window too long.

How to fix it: Calibrate with historical data. Review past incidents — which thresholds would have detected them?

Error 3: Not capturing the previous release before the deploy

Symptom: You need to revert but you don't know to which version.

Why it happens: The deploy script doesn't save the previous release before applying the new one.

How to fix it: An explicit step that captures the current release before the deploy and exposes it as a job output.

Error 4: Rollback without verifying it stabilized

Symptom: Rollback executed, but the metrics are still bad because the rollback also failed.

Why it happens: The rollback script runs without verifying post-rollback smoke tests.

How to fix it: Smoke tests after the rollback. If the rollback itself fails, escalate to a human (PagerDuty).

Error 5: Static thresholds without updating

Symptom: Thresholds configured 1 year ago when the system was different. They're no longer appropriate.

Why it happens: Lack of periodic review.

How to fix it: Quarterly review of thresholds. Compare with recent incidents — would they have triggered correctly?


Diagnosis

Question 1: Does your pipeline have automatic rollback configured?

Without automatic rollback, the first incident in production takes 25+ minutes. With rollback, ~5 minutes.

Question 2: Which metrics trigger your rollback?

Tier 1: error rate, latency p95, health checks. If you only monitor "is the app up?", you're missing important signals.

Question 3: How long does your rollback take from detection to restored service?

Target: <5 minutes. If it's more, optimize the rollback script (image cache, etc.).

Question 4: Do you capture the "previous release" before the deploy?

If not, you can't revert programmatically. The capture step is essential.

Question 5: Are the thresholds calibrated to your system or are they generic?

Generic = false positives or false negatives. Calibrated to the real baseline = precise.


Exercises

Exercise 1: Capture previous release (Easy)

Implement a get_current_release.sh script that returns the release currently deployed in production. Adapt it to your platform (Kubernetes, Heroku, etc.).

Exercise 2: Basic monitoring with thresholds (Medium)

Implement monitor_post_deploy.py with:

  1. A mock of metrics (the mock_metric function)
  2. Thresholds for error_rate, latency, health checks
  3. A sustained time check (samples every minute)
  4. Exit code 1 if a rollback is needed

Test it by simulating metrics that exceed the threshold.

Exercise 3: Complete pipeline with rollback (Hard)

Combine:

  1. Deploy
  2. Capture previous release
  3. Monitor post-deploy (10 min)
  4. Automatic rollback if needed
  5. Notification with context

Test it in a staging environment — force a "fake fail" to verify the rollback.


Summary

  • Manual rollback = 25 min, automatic rollback = 5 min — an order of magnitude difference
  • Tier 1 metrics trigger rollback: error rate, latency p95, health checks
  • Tier 2 metrics alert a human: throughput, memory, CPU
  • Sustained time avoids false positives from momentary spikes
  • Capture the previous release BEFORE the deploy — necessary to revert programmatically
  • Verify smoke tests post-rollback — the rollback itself can fail
  • Calibrate thresholds to your system, don't use generic values

Next capsule: 05 — Smart post-rollback diagnosis. The rollback works, but it doesn't teach. The module's final capsule: when the rollback runs, Claude Code analyzes what happened, identifies the root cause, and suggests a fix. The rollback becomes systematized learning.


Additional Resources

  1. Site Reliability Engineering: Effective Troubleshooting — A chapter from Google's book
  2. Argo Rollouts — Automatic rollback in Kubernetes
  3. Spinnaker Canary Analysis — For combined canary + rollback
  4. Datadog Synthetics — Continuous monitoring of endpoints
  5. Prometheus Alertmanager — Open source alerting
  6. Honeycomb Observability — Modern observability for distributed systems