Build an AI Kubernetes Runbook Generator with Claude API
Step-by-step tutorial to build a tool that automatically generates operational runbooks for any Kubernetes resource using Claude API — with real examples for Deployments, StatefulSets, and CronJobs.
Runbooks are critical for on-call engineers, but writing them is tedious and they go out of date the moment the infrastructure changes. Every time you update a Deployment's resource limits or change a CronJob schedule, the runbook should update too — but it never does.
What if the runbook was generated automatically from the actual running state of your Kubernetes resources?
That is what we are building: a tool that reads your Kubernetes resources, understands their configuration, and generates complete operational runbooks using Claude API. When the resource changes, regenerate the runbook.
What We're Building
A Python CLI that:
- Reads any Kubernetes resource (Deployment, StatefulSet, CronJob, etc.)
- Collects related resources (Services, ConfigMaps, HPAs, PDBs)
- Sends the full context to Claude API
- Outputs a complete operational runbook in Markdown
Setup
pip install anthropic kubernetes python-dotenv clickCreate .env:
ANTHROPIC_API_KEY=sk-ant-your-key
Step 1: Kubernetes Resource Collector
from kubernetes import client, config
from kubernetes.client.rest import ApiException
import json
import yaml
def load_config():
try:
config.load_incluster_config()
except config.ConfigException:
config.load_kube_config()
def get_resource_context(namespace: str, resource_type: str, name: str) -> dict:
"""
Collect a Kubernetes resource and all related resources.
Returns everything needed to write a meaningful runbook.
"""
load_config()
context = {
"primary": {},
"related": {},
"errors": []
}
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()
autoscaling_v2 = client.AutoscalingV2Api()
policy_v1 = client.PolicyV1Api()
batch_v1 = client.BatchV1Api()
try:
if resource_type.lower() == "deployment":
resource = apps_v1.read_namespaced_deployment(name, namespace)
context["primary"] = _serialize_resource(resource)
# Get pods
label_selector = _make_selector(resource.spec.selector.match_labels)
pods = core_v1.list_namespaced_pod(namespace, label_selector=label_selector)
context["related"]["pods"] = [_serialize_resource(p) for p in pods.items[:3]]
# Get HPA if it exists
try:
hpa = autoscaling_v2.read_namespaced_horizontal_pod_autoscaler(name, namespace)
context["related"]["hpa"] = _serialize_resource(hpa)
except ApiException:
pass
# Get PDB if it exists
try:
pdbs = policy_v1.list_namespaced_pod_disruption_budget(namespace)
for pdb in pdbs.items:
if pdb.spec.selector and _matches_labels(
pdb.spec.selector.match_labels,
resource.spec.selector.match_labels
):
context["related"]["pdb"] = _serialize_resource(pdb)
break
except ApiException:
pass
elif resource_type.lower() == "statefulset":
resource = apps_v1.read_namespaced_stateful_set(name, namespace)
context["primary"] = _serialize_resource(resource)
label_selector = _make_selector(resource.spec.selector.match_labels)
pods = core_v1.list_namespaced_pod(namespace, label_selector=label_selector)
context["related"]["pods"] = [_serialize_resource(p) for p in pods.items[:3]]
elif resource_type.lower() == "cronjob":
resource = batch_v1.read_namespaced_cron_job(name, namespace)
context["primary"] = _serialize_resource(resource)
# Get recent Jobs from this CronJob
jobs = batch_v1.list_namespaced_job(namespace)
related_jobs = [
j for j in jobs.items
if j.metadata.owner_references and
any(ref.name == name for ref in j.metadata.owner_references)
]
context["related"]["recent_jobs"] = [
_serialize_resource(j) for j in sorted(
related_jobs,
key=lambda j: j.metadata.creation_timestamp or "",
reverse=True
)[:5]
]
# Get related Services (for all types)
try:
services = core_v1.list_namespaced_service(namespace)
matching = []
primary_labels = context["primary"].get("spec", {}).get("selector", {}) or \
context["primary"].get("spec", {}).get("template", {}).get("metadata", {}).get("labels", {})
for svc in services.items:
if svc.spec.selector and _matches_labels(svc.spec.selector, primary_labels):
matching.append(_serialize_resource(svc))
if matching:
context["related"]["services"] = matching
except ApiException:
pass
# Get ConfigMaps and Secrets referenced in env
try:
containers = (context["primary"].get("spec", {})
.get("template", {})
.get("spec", {})
.get("containers", []))
cm_names = set()
for container in containers:
for env_from in container.get("envFrom", []):
if "configMapRef" in env_from:
cm_names.add(env_from["configMapRef"]["name"])
cms = {}
for cm_name in cm_names:
try:
cm = core_v1.read_namespaced_config_map(cm_name, namespace)
# Don't include data values (may be sensitive), just keys
cms[cm_name] = {"keys": list((cm.data or {}).keys())}
except ApiException:
pass
if cms:
context["related"]["configmaps"] = cms
except Exception:
pass
except ApiException as e:
context["errors"].append(f"Could not fetch {resource_type}/{name}: {e.reason}")
return context
def _serialize_resource(resource) -> dict:
"""Convert K8s resource object to dict, removing status noise."""
data = resource.to_dict()
# Clean up managed fields noise
if "metadata" in data:
data["metadata"].pop("managed_fields", None)
data["metadata"].pop("annotations", None)
return data
def _make_selector(labels: dict) -> str:
return ",".join(f"{k}={v}" for k, v in (labels or {}).items())
def _matches_labels(selector: dict, resource_labels: dict) -> bool:
if not selector or not resource_labels:
return False
return all(resource_labels.get(k) == v for k, v in selector.items())Step 2: Runbook Generator
import anthropic
import os
from dotenv import load_dotenv
load_dotenv()
def build_runbook_prompt(resource_type, name, namespace, primary_resource, related_resources):
json_fence = "```json"
bash_fence = "```bash"
close_fence = "```"
return (
f"Generate a complete operational runbook for the following Kubernetes resource.\n\n"
f"## Resource Information\n\n"
f"**Type:** {resource_type}\n**Name:** {name}\n**Namespace:** {namespace}\n\n"
f"## Resource Configuration\n{json_fence}\n{primary_resource}\n{close_fence}\n\n"
f"## Related Resources\n{json_fence}\n{related_resources}\n{close_fence}\n\n"
"---\n\n"
"Generate a runbook with these sections:\n\n"
"## 1. Overview — what this resource does, business criticality, key dependencies\n"
"## 2. Normal Operation — exact kubectl commands to verify health, expected pod count\n"
"## 3. Common Issues and Fixes — CrashLoopBackOff, PVC issues, HPA not scaling\n"
"## 4. Scaling Operations — manual scale commands, HPA behavior\n"
"## 5. Deployment / Update Procedure — safe rollout and rollback commands\n"
"## 6. Emergency Procedures — restart pods, take offline, restore from backup\n"
f"## 7. Useful Commands Reference\n{bash_fence}\n# Commands with exact names\n{close_fence}\n\n"
"Make the runbook specific — use actual name, namespace, labels. Include exact kubectl commands."
)
def generate_runbook(
namespace: str,
resource_type: str,
name: str,
output_format: str = "markdown"
) -> str:
"""Generate a complete operational runbook using Claude API."""
print(f"Collecting K8s context for {resource_type}/{name}...")
context = get_resource_context(namespace, resource_type, name)
if context["errors"] and not context["primary"]:
return f"Error: {chr(10).join(context['errors'])}"
prompt = build_runbook_prompt(
resource_type=resource_type,
name=name,
namespace=namespace,
primary_resource=json.dumps(context["primary"], indent=2, default=str)[:4000],
related_resources=json.dumps(context["related"], indent=2, default=str)[:3000]
)
print("Generating runbook with Claude API...")
anthropic_client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
message = anthropic_client.messages.create(
model="claude-sonnet-5",
max_tokens=3000,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def save_runbook(content: str, name: str, resource_type: str, output_dir: str = "runbooks"):
"""Save runbook to a Markdown file."""
import os
os.makedirs(output_dir, exist_ok=True)
filename = f"{output_dir}/{resource_type}-{name}-runbook.md"
with open(filename, "w") as f:
f.write(content)
print(f"Runbook saved: {filename}")
return filenameStep 3: CLI Entry Point
import click
@click.command()
@click.argument("resource_type")
@click.argument("name")
@click.option("--namespace", "-n", default="default", help="Kubernetes namespace")
@click.option("--output", "-o", default="runbooks", help="Output directory")
@click.option("--print", "print_output", is_flag=True, help="Print to stdout instead of saving")
def main(resource_type, name, namespace, output, print_output):
"""
Generate an operational runbook for a Kubernetes resource.
Examples:
python runbook_gen.py deployment my-api -n production
python runbook_gen.py statefulset postgres -n databases
python runbook_gen.py cronjob nightly-backup -n production
"""
runbook = generate_runbook(namespace, resource_type, name)
if print_output:
print(runbook)
else:
save_runbook(runbook, name, resource_type, output)
if __name__ == "__main__":
main()Usage Examples
# Generate runbook for a production deployment
python runbook_gen.py deployment payment-api -n production
# Generate runbook for a StatefulSet
python runbook_gen.py statefulset postgres -n databases
# Generate runbook for a CronJob and print to stdout
python runbook_gen.py cronjob nightly-cleanup -n production --print
# Generate and save to custom directory
python runbook_gen.py deployment api-gateway -n staging -o docs/runbooksExample Output (Excerpt)
Here is what the generated runbook looks like:
Runbook: payment-api (production)
Overview: payment-api handles payment processing for the checkout flow. Currently runs 3 replicas with HPA configured to scale 3-10 based on CPU. Depends on: postgres (StatefulSet), redis-cache (Deployment), stripe-webhook Service. Business Criticality: HIGH — downtime directly impacts revenue.
Normal Operation — verify healthy state:
kubectl get deployment payment-api -n production
# Expected: READY 3/3
kubectl get pods -n production -l app=payment-api
# All pods should show STATUS=Running, RESTARTS=0Common Issues — CrashLoopBackOff:
Symptom: kubectl get pods -n production shows RESTARTS > 3. Likely cause: Missing STRIPE_SECRET_KEY env var or failed connection to postgres.
# Check logs from previous container
kubectl logs -n production -l app=payment-api --previous
# Verify secret exists
kubectl get secret payment-secrets -n productionAdding to Your CI/CD Pipeline
Regenerate runbooks automatically on every deploy:
# .github/workflows/update-runbooks.yml
- name: Update runbooks after deploy
run: |
pip install anthropic kubernetes click
python runbook_gen.py deployment ${{ env.SERVICE_NAME }} \
-n ${{ env.NAMESPACE }} \
-o docs/runbooks
git add docs/runbooks/
git commit -m "Auto-update runbook for ${{ env.SERVICE_NAME }}"
git push
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Now your runbooks stay current with your actual infrastructure automatically.
More AI + Kubernetes tools? Check out Build an AI deployment health checker and AI-powered incident response with LLM runbooks.
Today I Fixed
Short real fixes from production — posted daily
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
Build an AI Deployment Health Checker with Claude API and Kubernetes
Step-by-step tutorial to build an AI-powered deployment health checker using Claude API and the Kubernetes Python client. Automatically diagnose failing pods, check resource limits, and get plain-English explanations of what's wrong.
Build an AI Kubernetes Deployment Readiness Checker with Claude API
Build a Python CLI tool using Claude API that analyzes Kubernetes YAML manifests before deployment — catches missing resource limits, root containers, and security issues with a go/no-go score.
Build an AI Kubernetes YAML Explainer and Validator with Claude API
Step-by-step tutorial to build a tool that takes any Kubernetes YAML manifest, explains what it does in plain English, catches misconfigurations and security issues, and suggests improvements — using Claude API.