n4nAI

Automating infrastructure-as-code reviews with AI agents

A practical guide to building an AI agent infrastructure-as-code review pipeline for Terraform, with runnable code and verification steps for DevOps.

n4n Team2 min read548 words

Audio narration

Coming soon — every post will get a voice note here.

Running an AI agent infrastructure-as-code review on every pull request catches misconfigured S3 buckets, missing tags, and risky IAM policies before they reach production. This how-to builds a self-hosted reviewer that ingests Terraform diffs, queries a language model for targeted feedback, and writes comments back to the PR. You will end up with a deployable service, not a vague architecture diagram.

Step 1: Scaffold the review service

Stand up a minimal FastAPI app that receives GitHub webhooks. Use a separate endpoint for the PR review event to avoid parsing unrelated traffic.

mkdir iac-reviewer && cd iac-reviewer
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn openai pydantic requests

Create main.py with a webhook receiver:

from fastapi import FastAPI, Request, HTTPException
import hmac, os

app = FastAPI()
WEBHOOK_SECRET = os.environ["GH_WEBHOOK_SECRET"]

@app.post("/webhook")
async def webhook(req: Request):
    body = await req.body()
    sig = req.headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), body, "sha256").hexdigest()
    if not hmac.compare_digest(sig, expected):
        raise HTTPException(status_code=401, detail="bad signature")
    event = req.headers.get("X-GitHub-Event")
    if event != "pull_request":
        return {"ok": True}
    payload = await req.json()
    if payload["action"] not in ("opened", "synchronize"):
        return {"ok": True}
    # launch review asynchronously in real code
    return {"ok": True}

Run it locally with uvicorn main:app --port 8000. The AI agent infrastructure-as-code review logic plugs into the branch where we currently return {"ok": True}.

Step 2: Extract Terraform changes from the PR

Clone the repo at the PR head and collect the .tf files that changed. Use the GitHub diff API to avoid a full clone when possible, but a shallow clone is simpler to reason about.

import subprocess, tempfile, os, json

def clone_and_diff(repo_url: str, base_sha: str, head_sha: str) -> list[str]:
    tmp = tempfile.mkdtemp()
    subprocess.run(["git", "clone", "--depth", "1", repo_url, tmp], check=True)
    subprocess.run(["git", "-C", tmp, "fetch", "--depth", "1", "origin", base_sha, head_sha], check=True)
    diff = subprocess.run(
        ["git", "-C", tmp, "diff", f"{base_sha}...{head_sha}", "--name-only", "-- '*.tf'"],
        capture_output=True, text=True, check=True
    )
    files = [os.path.join(tmp, f) for f in diff.stdout.splitlines() if f]
    return files

Read each file and embed the content in the prompt. For large modules, truncate to the first 200 lines per file; the agent should flag truncation explicitly.

Step 3: Build the agent prompt

The model needs strict output formatting. Use a system prompt that enforces JSON and lists concrete review dimensions: security, idempotency, cost, and tag hygiene.

SYSTEM_PROMPT = """You are a senior Terraform reviewer. Given a unified diff or file contents,
return a JSON array of findings. Each finding has:
  - file: string (relative path)
  - line: integer (1-based, approximate is fine)
  - severity: "blocker" | "warning" | "info"
  - message: string (actionable, under 240 chars)
If the code is clean, return []. Do not invent line numbers beyond the provided snippet."""

def build_user_prompt(files: list[tuple[str, str]]) -> str:
    chunks = []
    for path, content in files:
        chunks.append(f"### {path}\n{content[:8000]}")
    return "\n\n".join(chunks)

The AI agent infrastructure-as-code review quality depends entirely on this contract. Validate the schema before posting anything.

Step 4: Call the model with fallback

Use the OpenAI Python client pointed at an OpenAI-compatible gateway. We route through n4n.ai’s OpenAI-compatible endpoint; it honors client routing directives and falls back automatically when a provider is rate-limited, so the review does not stall during upstream outages.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",
    api_key=os.environ["N4N_API_KEY"],
)

def review(files: list[tuple[str, str]]) -> list[dict]:
    resp = client.chat.completions.create(
        model="anthropic/claude-3.5-sonnet",  # or any of 240+ models
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": build_user_prompt(files)},
        ],
        response_format={"type": "json_object"},
        temperature=0.1,
    )
    data = json.loads(resp.choices[0].message.content)
    return data.get("findings", [])

Set response_format only if the target model supports JSON mode. Otherwise, instruct the model in the system prompt and parse defensively.

Step 5: Post findings as PR comments

Map findings to GitHub review comments using the commit SHA from the webhook. Use the REST API with a personal access token scoped to pull_requests: write.

from pydantic import BaseModel

class Finding(BaseModel):
    file: str
    line: int
    severity: str
    message: str

def post_review(repo: str, pr_num: int, sha: str, findings: list[Finding]):
    headers = {"Authorization": f"Bearer {os.environ['GH_TOKEN']}"}
    comments = [
        {
            "path": f.file,
            "line": f.line,
            "body": f"[{f.severity.upper()}] {f.message}",
        }
        for f in findings
    ]
    payload = {
        "commit_id": sha,
        "event": "REQUEST_CHANGES" if any(f.severity == "blocker" for f in findings) else "COMMENT",
        "comments": comments,
    }
    url = f"https://api.github.com/repos/{repo}/pulls/{pr_num}/reviews"
    r = requests.post(url, json=payload, headers=headers)
    r.raise_for_status()

Wire this into the webhook handler from Step 1. Extract repo, pr_num, and sha from the payload:

repo = payload["repository"]["full_name"]
pr_num = payload["number"]
sha = payload["pull_request"]["head"]["sha"]
files = clone_and_diff(payload["repository"]["clone_url"],
                       payload["pull_request"]["base"]["sha"], sha)
findings = [Finding(**f) for f in review([(f, open(f).read()) for f in files])]
post_review(repo, pr_num, sha, findings)

Step 6: Verify the pipeline end to end

You need a public endpoint for GitHub to reach. Use ngrok locally or deploy the service to a container.

ngrok http 8000

Register the webhook in your test repo:

  • Payload URL: https://<ngrok-id>.ngrok.io/webhook
  • Content type: application/json
  • Secret: same as GH_WEBHOOK_SECRET
  • Events: Pull requests

Create a deliberately broken Terraform file in a branch:

resource "aws_s3_bucket" "data" {
  bucket = "my-unencrypted-bucket"
  # missing acl, encryption, and versioning
}

Open a PR. Within seconds, the service should post a comment similar to:

[
  {
    "file": "main.tf",
    "line": 1,
    "severity": "blocker",
    "message": "S3 bucket missing server-side encryption configuration (aws_s3_bucket_server_side_encryption_configuration)."
  }
]

Check the service logs for the model response and the GitHub API status code. A 201 from the reviews endpoint confirms success. If you see 422, the line number is likely outside the diff hunk; clamp line numbers to the file length.

Tuning the AI agent infrastructure-as-code review

Once the loop runs, tighten the prompt to your org’s policies. Add a rule that every aws_instance must have a tags block with owner and cost_center. Bump temperature to 0 for deterministic output in CI. Cache the system prompt by sending cache_control hints if your gateway forwards them; n4n.ai forwards provider cache-control hints, which cuts latency on repeated reviews.

The agent will not replace human judgment on architecture, but it removes the toil of spotting the same twenty misconfigurations. Ship it as a required check and let engineers override blockers with a written justification.

Tagsiacterraformcode-reviewdevops

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All ai agents in devops & sre posts →