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.