🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI SRE Incident Commander with Claude API

Step-by-step tutorial to build an AI incident commander that takes an alert, gathers context from Kubernetes and AWS, generates a structured runbook, and coordinates the incident response — using Claude API with tool use.

Shubham6 min read
Share:Tweet

Incident response is high-stress, time-critical, and relies on knowledge that is often locked in the heads of a few senior engineers. When your on-call engineer is woken at 3 AM by a PagerDuty alert for a service they rarely touch, they spend the first 10-15 minutes just gathering context before they can even start diagnosing.

An AI incident commander can do that initial context-gathering automatically, produce a structured runbook, and reduce the time from "alert fires" to "engineer has a diagnosis direction."

What We're Building

An AI system that:

  1. Receives an incident alert (from PagerDuty, Opsgenie, or manually)
  2. Automatically gathers cluster state (pods, events, logs)
  3. Checks relevant AWS metrics
  4. Generates a prioritized investigation runbook
  5. Posts a structured incident brief to Slack
  6. Updates a war room thread as the incident progresses

Architecture

Alert (PagerDuty webhook) → FastAPI handler
    → Claude API (tool use loop)
        → kubectl_get, kubectl_describe, kubectl_logs
        → aws_cloudwatch_metrics
        → search_runbooks (vector search over past incidents)
    → Structured incident brief
    → Slack war room post

Step 1: Alert Ingestion

python
import anthropic
import json
import subprocess
import os
import urllib.request
from datetime import datetime
from fastapi import FastAPI, Request
from pydantic import BaseModel
from typing import Optional
 
app = FastAPI()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
SLACK_WEBHOOK = os.getenv("SLACK_INCIDENT_WEBHOOK")
 
 
class Alert(BaseModel):
    title: str
    severity: str  # P1, P2, P3
    service: str
    namespace: Optional[str] = "default"
    details: Optional[str] = ""
    runbook_url: Optional[str] = None
    source: Optional[str] = "manual"
 
 
@app.post("/incident/create")
async def create_incident(alert: Alert):
    """Receive an alert and start the AI incident commander."""
    incident_id = f"INC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
 
    # Post initial Slack message
    post_slack_message(
        f":rotating_light: *Incident {incident_id}* — {alert.severity}\n"
        f"*Service*: {alert.service}\n"
        f"*Alert*: {alert.title}\n"
        f"_AI Incident Commander gathering context..._"
    )
 
    # Run AI analysis
    brief = await run_incident_commander(alert, incident_id)
 
    # Post structured brief
    post_slack_message(brief)
 
    return {"incident_id": incident_id, "brief": brief}

Step 2: Tool Definitions

python
INCIDENT_TOOLS = [
    {
        "name": "kubectl_get",
        "description": "Get Kubernetes resources. Returns output of kubectl get.",
        "input_schema": {
            "type": "object",
            "properties": {
                "resource": {"type": "string"},
                "namespace": {"type": "string", "default": "default"},
                "selector": {"type": "string", "description": "Label selector like app=my-api"}
            },
            "required": ["resource"]
        }
    },
    {
        "name": "kubectl_events",
        "description": "Get recent events in a namespace, sorted by time.",
        "input_schema": {
            "type": "object",
            "properties": {
                "namespace": {"type": "string"},
                "warning_only": {"type": "boolean", "default": True}
            }
        }
    },
    {
        "name": "kubectl_logs",
        "description": "Get logs from a pod.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pod_name": {"type": "string"},
                "namespace": {"type": "string"},
                "lines": {"type": "integer", "default": 50},
                "previous": {"type": "boolean", "default": False},
                "container": {"type": "string"}
            },
            "required": ["pod_name"]
        }
    },
    {
        "name": "check_node_status",
        "description": "Check all nodes for resource pressure, conditions, and capacity.",
        "input_schema": {
            "type": "object",
            "properties": {}
        }
    },
    {
        "name": "get_deployment_history",
        "description": "Check recent deployment history for a service to identify if a recent deploy caused the incident.",
        "input_schema": {
            "type": "object",
            "properties": {
                "deployment_name": {"type": "string"},
                "namespace": {"type": "string"}
            },
            "required": ["deployment_name", "namespace"]
        }
    }
]
 
 
def execute_incident_tool(name: str, input_data: dict) -> str:
    """Execute incident tools — all read-only."""
    try:
        if name == "kubectl_get":
            cmd = ["kubectl", "get", input_data["resource"], "--no-headers"]
            if ns := input_data.get("namespace"):
                cmd.extend(["-n", ns])
            if sel := input_data.get("selector"):
                cmd.extend(["-l", sel])
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            return (result.stdout or result.stderr)[:3000]
 
        elif name == "kubectl_events":
            cmd = ["kubectl", "get", "events",
                   "-n", input_data.get("namespace", "default"),
                   "--sort-by='.lastTimestamp'"]
            if input_data.get("warning_only", True):
                cmd.extend(["--field-selector=type=Warning"])
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            return (result.stdout or result.stderr)[-2000:]
 
        elif name == "kubectl_logs":
            cmd = ["kubectl", "logs", input_data["pod_name"],
                   "-n", input_data.get("namespace", "default"),
                   f"--tail={input_data.get('lines', 50)}"]
            if input_data.get("previous"):
                cmd.append("--previous")
            if c := input_data.get("container"):
                cmd.extend(["-c", c])
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            return (result.stdout or result.stderr)[-3000:]
 
        elif name == "check_node_status":
            result = subprocess.run(
                ["kubectl", "get", "nodes", "-o", "wide"],
                capture_output=True, text=True, timeout=30
            )
            describe_result = subprocess.run(
                ["kubectl", "describe", "nodes"],
                capture_output=True, text=True, timeout=30
            )
            output = result.stdout + "\n\n" + describe_result.stdout
            return output[:4000]
 
        elif name == "get_deployment_history":
            result = subprocess.run(
                ["kubectl", "rollout", "history", "deployment",
                 input_data["deployment_name"],
                 "-n", input_data["namespace"]],
                capture_output=True, text=True, timeout=30
            )
            return result.stdout or result.stderr
 
    except Exception as e:
        return f"Tool error: {str(e)}"
 
    return "Unknown tool"

Step 3: The Incident Commander Agent Loop

python
async def run_incident_commander(alert: Alert, incident_id: str) -> str:
    """
    Run the AI incident commander — tool use loop with structured output.
    """
    system_prompt = f"""You are an expert SRE acting as Incident Commander for a production incident.
 
Incident ID: {incident_id}
Severity: {alert.severity}
Service: {alert.service}
Namespace: {alert.namespace}
Alert: {alert.title}
Details: {alert.details}
 
Your job:
1. Gather context systematically — do not guess, verify with tools
2. Check pods, events, logs, and deployment history in that order
3. Form a hypothesis about the root cause
4. Generate an actionable runbook
 
Focus on: 
- Is there a recent deployment that could have caused this?
- Are pods healthy? Any CrashLoopBackOff, OOMKilled, Pending?
- What do the warning events show?
- What do the pod logs show around the time of the alert?
 
You have read-only access — diagnosis only, no remediation actions."""
 
    messages = [
        {
            "role": "user",
            "content": f"Alert received: {alert.title}. Service: {alert.service} in namespace {alert.namespace}. Start incident investigation."
        }
    ]
 
    for iteration in range(10):
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=4000,
            system=system_prompt,
            tools=INCIDENT_TOOLS,
            messages=messages
        )
 
        messages.append({"role": "assistant", "content": response.content})
 
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return format_incident_brief(incident_id, alert, block.text)
            break
 
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_incident_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            messages.append({"role": "user", "content": tool_results})
 
    return "Incident analysis timed out. Manual investigation required."
 
 
def format_incident_brief(incident_id: str, alert: Alert, analysis: str) -> str:
    """Format the AI analysis as a Slack-friendly incident brief."""
    return f"""
:red_circle: *INCIDENT BRIEF — {incident_id}*
*Severity*: {alert.severity} | *Service*: {alert.service}
 
{analysis}
 
---
_Generated by AI Incident Commander at {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}_
_This is an AI-assisted first pass — verify all findings before taking action._
"""
 
 
def post_slack_message(message: str):
    """Post to Slack webhook."""
    if not SLACK_WEBHOOK:
        print(f"[SLACK] {message}")
        return
    payload = json.dumps({"text": message}).encode()
    req = urllib.request.Request(
        SLACK_WEBHOOK,
        data=payload,
        headers={"Content-Type": "application/json"}
    )
    urllib.request.urlopen(req)

Example Incident Brief Output

🔴 INCIDENT BRIEF — INC-20260709-031542
Severity: P1 | Service: payment-api

## Root Cause (High Confidence)
The payment-api deployment was rolled out at 03:08 UTC with image tag v2.4.1.
Within 3 minutes, all 3 pods entered CrashLoopBackOff.

Pod logs show: "FATAL: Cannot connect to payment-processor:50051 — connection refused"

The new image attempts to connect to payment-processor via gRPC, but the
payment-processor Service was not updated to expose port 50051 in the same release.
The Service still only exposes port 50050.

## Evidence
- kubectl rollout history: deployment v2.4.1 rolled out at 03:08 UTC
- All 3 pods: CrashLoopBackOff (5 restarts)
- pod logs: gRPC connection error on port 50051
- payment-processor Service: port 50050 only

## Immediate Actions
1. Roll back: `kubectl rollout undo deployment/payment-api -n production`
2. Verify rollback: `kubectl rollout status deployment/payment-api -n production`
3. Confirm pods recover: `kubectl get pods -n production -l app=payment-api`

## Root Fix Required
Update payment-processor Service to add port 50051, then re-deploy v2.4.1 with the
Service change included in the same release.

More AI + DevOps automation? Read our LLM tool use agents for DevOps and AI-powered incident response with LLM runbooks.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments