šŸŽ‰ DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Code Reviewer for Kubernetes Manifests with Claude

Build a CLI tool that reviews Kubernetes YAML manifests with Claude — catching missing resource limits, security issues, hardcoded secrets, anti-patterns, and suggesting fixes before kubectl apply.

DevOpsBoys6 min read
Share:Tweet

Kubernetes YAML is error-prone. Missing resource limits cause OOMKills. Hardcoded secrets end up in Git. Running as root causes security violations. This tool catches all of these before kubectl apply — using Claude to review manifests and explain issues like a senior engineer would.


What It Reviews

  • Resource limits and requests missing
  • Security context: root containers, privilege escalation
  • Hardcoded secrets and tokens in env vars
  • Missing liveness/readiness probes
  • Latest image tags
  • Missing namespace specification
  • Overly permissive RBAC
  • Missing pod disruption budgets for critical apps
  • Anti-patterns: NodePort services, host networking, hostPath volumes

CLI Tool Structure

k8s-review/
ā”œā”€ā”€ main.py           # CLI entry point
ā”œā”€ā”€ reviewer.py       # Claude-powered review engine
ā”œā”€ā”€ checkers.py       # Static pre-checks
ā”œā”€ā”€ formatter.py      # Output formatting
└── requirements.txt

Static Pre-Checks (Fast, No AI)

python
# checkers.py — fast rules before hitting Claude
import yaml
from dataclasses import dataclass
from typing import List
 
 
@dataclass
class Issue:
    severity: str  # critical, warning, info
    resource: str
    message: str
    fix: str
 
 
def check_manifest(doc: dict) -> List[Issue]:
    issues = []
    kind = doc.get("kind", "")
    name = doc.get("metadata", {}).get("name", "unknown")
    resource_id = f"{kind}/{name}"
 
    if kind in ("Deployment", "DaemonSet", "StatefulSet"):
        containers = (
            doc.get("spec", {})
            .get("template", {})
            .get("spec", {})
            .get("containers", [])
        )
 
        for c in containers:
            cname = c.get("name", "unnamed")
 
            # Missing resource limits
            limits = c.get("resources", {}).get("limits", {})
            requests = c.get("resources", {}).get("requests", {})
            if not limits.get("cpu"):
                issues.append(Issue("critical", f"{resource_id}/{cname}",
                    "Missing CPU limit — pod can consume all node CPU",
                    "Add resources.limits.cpu (e.g., '500m')"))
            if not limits.get("memory"):
                issues.append(Issue("critical", f"{resource_id}/{cname}",
                    "Missing memory limit — pod will be OOMKilled unpredictably",
                    "Add resources.limits.memory (e.g., '512Mi')"))
 
            # Latest tag
            image = c.get("image", "")
            if image.endswith(":latest") or ":" not in image:
                issues.append(Issue("warning", f"{resource_id}/{cname}",
                    f"Image '{image}' uses :latest — non-deterministic deploys",
                    "Pin to a specific digest: image: nginx:1.27.0"))
 
            # Hardcoded secrets in env
            for env in c.get("env", []):
                val = str(env.get("value", "")).lower()
                key = env.get("name", "").lower()
                if any(s in key for s in ("password", "secret", "token", "key", "api_key")):
                    if env.get("value"):  # hardcoded (not valueFrom)
                        issues.append(Issue("critical", f"{resource_id}/{cname}",
                            f"Hardcoded secret in env var '{env['name']}'",
                            "Use secretKeyRef: valueFrom.secretKeyRef.name/key"))
 
            # Missing probes
            if not c.get("readinessProbe"):
                issues.append(Issue("warning", f"{resource_id}/{cname}",
                    "Missing readiness probe — traffic routed before app is ready",
                    "Add readinessProbe with httpGet or exec"))
            if not c.get("livenessProbe"):
                issues.append(Issue("info", f"{resource_id}/{cname}",
                    "Missing liveness probe — hung pods won't be restarted",
                    "Add livenessProbe"))
 
        # Security context
        pod_spec = doc.get("spec", {}).get("template", {}).get("spec", {})
        for c in containers:
            sc = c.get("securityContext", {})
            if sc.get("privileged"):
                issues.append(Issue("critical", f"{resource_id}/{c['name']}",
                    "Container runs privileged — full host access",
                    "Remove securityContext.privileged or set to false"))
            if sc.get("allowPrivilegeEscalation") is not False:
                issues.append(Issue("warning", f"{resource_id}/{c['name']}",
                    "allowPrivilegeEscalation not disabled",
                    "Add: securityContext.allowPrivilegeEscalation: false"))
 
    return issues

Claude Review Engine

python
# reviewer.py
import anthropic
import yaml
from checkers import check_manifest, Issue
from typing import List
 
client = anthropic.Anthropic()
 
REVIEW_PROMPT = """You are a senior DevOps engineer reviewing Kubernetes manifests.
After the static checks listed, look for:
1. Architectural issues (wrong service type for use case, anti-patterns)
2. Operational risks (missing PDB for critical deployments, no HPA)
3. Security posture (RBAC overpermissioning, network exposure)
4. Performance (wrong resource ratios, missing affinity rules)
 
For each issue found, give: severity (critical/warning/info), what's wrong, and the exact YAML fix.
Be specific and actionable. Focus on issues the static checker didn't catch."""
 
 
def ai_review(manifest_yaml: str, static_issues: List[Issue]) -> str:
    static_summary = "\n".join(
        f"- [{i.severity.upper()}] {i.resource}: {i.message}"
        for i in static_issues
    )
 
    yaml_block = "```yaml\n" + manifest_yaml + "\n```"
    static_note = static_summary if static_summary else "No issues found by static checker."
    user_content = (
        f"Review this Kubernetes manifest:\n\n{yaml_block}\n\n"
        f"Static checker already found:\n{static_note}\n\n"
        "Find additional issues the static checker missed."
    )
 
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        system=REVIEW_PROMPT,
        messages=[{"role": "user", "content": user_content}],
    )
    return response.content[0].text

CLI Entry Point

python
# main.py
import sys
import yaml
import click
from pathlib import Path
from checkers import check_manifest
from reviewer import ai_review
from rich.console import Console
from rich.table import Table
from rich import print as rprint
 
console = Console()
 
SEVERITY_COLORS = {
    "critical": "red",
    "warning": "yellow",
    "info": "blue",
}
 
 
@click.command()
@click.argument("manifest", type=click.Path(exists=True))
@click.option("--ai/--no-ai", default=True, help="Enable AI review (uses Claude API)")
@click.option("--fail-on", default="critical", type=click.Choice(["critical", "warning", "info"]))
def review(manifest: str, ai: bool, fail_on: str):
    """Review a Kubernetes manifest for issues."""
    content = Path(manifest).read_text()
 
    # Parse all YAML documents (multi-doc files)
    docs = list(yaml.safe_load_all(content))
    all_issues = []
 
    for doc in docs:
        if doc:
            issues = check_manifest(doc)
            all_issues.extend(issues)
 
    # Display static issues
    if all_issues:
        table = Table(title="Static Check Results", show_lines=True)
        table.add_column("Severity", style="bold", width=10)
        table.add_column("Resource", width=30)
        table.add_column("Issue", width=50)
        table.add_column("Fix", width=40)
 
        for issue in all_issues:
            color = SEVERITY_COLORS.get(issue.severity, "white")
            table.add_row(
                f"[{color}]{issue.severity.upper()}[/{color}]",
                issue.resource,
                issue.message,
                issue.fix,
            )
        console.print(table)
    else:
        console.print("[green]āœ“ No static check issues found[/green]")
 
    # AI review
    if ai:
        console.print("\n[cyan]Running AI review...[/cyan]")
        ai_feedback = ai_review(content, all_issues)
        console.print("\n[bold]AI Review:[/bold]")
        console.print(ai_feedback)
 
    # Exit code
    severity_order = ["info", "warning", "critical"]
    fail_level = severity_order.index(fail_on)
    highest = max(
        (severity_order.index(i.severity) for i in all_issues),
        default=-1
    )
    if highest >= fail_level:
        console.print(f"\n[red]āŒ Review failed ({fail_on}+ issues found)[/red]")
        sys.exit(1)
    else:
        console.print(f"\n[green]āœ… Review passed[/green]")
 
 
if __name__ == "__main__":
    review()

Usage

bash
# Install
pip install anthropic pyyaml click rich
 
# Review a manifest
python main.py deployment.yaml
 
# Fail CI on critical issues only (default)
python main.py deployment.yaml --fail-on critical
 
# Skip AI review (just static checks)
python main.py deployment.yaml --no-ai
 
# Review whole directory
for f in k8s/*.yaml; do python main.py "$f"; done

GitHub Actions Integration

yaml
# .github/workflows/k8s-review.yaml
name: Kubernetes Manifest Review
on:
  pull_request:
    paths:
    - 'k8s/**/*.yaml'
    - 'helm/**'
 
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    - run: pip install anthropic pyyaml click rich
    - name: Review changed manifests
      env:
        ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      run: |
        # Get changed YAML files
        CHANGED=$(git diff --name-only origin/main...HEAD | grep '\.yaml$' || true)
        for f in $CHANGED; do
          echo "Reviewing $f..."
          python k8s-review/main.py "$f" --fail-on critical
        done

Sample Output

Static Check Results
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Severity │ Resource                │ Issue                            │ Fix                      │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ CRITICAL │ Deployment/api/app      │ Missing memory limit             │ Add resources.limits...  │
│ WARNING  │ Deployment/api/app      │ Image uses :latest tag           │ Pin to nginx:1.27.0      │
│ CRITICAL │ Deployment/api/app      │ Hardcoded secret in env 'API_KEY'│ Use secretKeyRef         │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

AI Review:
Beyond the static issues, I notice this Deployment has replicas: 1 with no
PodDisruptionBudget — a node drain will take this service down completely.
For a production API, add:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: api

Also, the Service type is NodePort which exposes a random port on every node.
Use ClusterIP with an Ingress instead.

āŒ Review failed (critical+ issues found)

Shift manifest review left. Catching these issues in PR review is 100x cheaper than debugging a production OOMKill or secret leak.

šŸ”§

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