Generate Terraform Modules from Natural Language with Claude API
Build a CLI that turns a plain-English infrastructure request into a working Terraform module — variables, resources, outputs, and a README — using Claude API, then runs terraform validate before handing it back.
"I need an S3 bucket with versioning, a lifecycle rule to move old objects to Glacier after 90 days, and a bucket policy that only allows access from our VPC" is a two-minute description and a twenty-minute Terraform module to write by hand. This tool closes that gap — with a validation step so it never hands you broken HCL.
Setup
pip install anthropic
# terraform CLI must be installed and on PATHModule Generator
import anthropic
import subprocess
import re
import os
from pathlib import Path
client = anthropic.Anthropic()
GENERATE_PROMPT = """You are a senior Terraform engineer. Generate a complete,
production-ready Terraform module for this request:
"{request}"
Requirements:
- Use AWS provider version ~> 5.0
- Include variables.tf with sensible defaults and descriptions
- Include outputs.tf for anything a caller would need to reference
- Follow least-privilege for any IAM policies
- Add comments only where a decision is non-obvious (e.g. why a specific lifecycle rule)
Respond with a JSON object with these exact keys, each containing raw HCL as a string:
{{"main_tf": "...", "variables_tf": "...", "outputs_tf": "...", "readme_md": "..."}}"""
def generate_module(request: str) -> dict:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4000,
messages=[{"role": "user", "content": GENERATE_PROMPT.format(request=request)}]
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = text.split("```")[1].replace("json", "", 1).strip()
import json
return json.loads(text)
def write_module(module: dict, output_dir: str):
os.makedirs(output_dir, exist_ok=True)
Path(output_dir, "main.tf").write_text(module["main_tf"])
Path(output_dir, "variables.tf").write_text(module["variables_tf"])
Path(output_dir, "outputs.tf").write_text(module["outputs_tf"])
Path(output_dir, "README.md").write_text(module["readme_md"])Validation Loop — Never Hand Back Broken HCL
def validate_module(output_dir: str) -> tuple[bool, str]:
"""Run terraform init + validate, return (passed, error_output)."""
init = subprocess.run(
["terraform", "init", "-backend=false"],
cwd=output_dir, capture_output=True, text=True, timeout=60
)
if init.returncode != 0:
return False, init.stderr
validate = subprocess.run(
["terraform", "validate"],
cwd=output_dir, capture_output=True, text=True, timeout=30
)
return validate.returncode == 0, validate.stderr
def generate_and_validate(request: str, output_dir: str, max_retries: int = 3) -> bool:
module = generate_module(request)
write_module(module, output_dir)
for attempt in range(max_retries):
passed, error = validate_module(output_dir)
if passed:
print(f"Module valid after {attempt + 1} attempt(s)")
return True
print(f"Attempt {attempt + 1} failed validation, asking Claude to fix:\n{error[:500]}")
fix_prompt = f"""This Terraform module failed validation with this error:
{error}
Current main.tf:
{Path(output_dir, 'main.tf').read_text()}
Fix the issue and respond with the same JSON format as before,
with the corrected HCL."""
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4000,
messages=[{"role": "user", "content": fix_prompt}]
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = text.split("```")[1].replace("json", "", 1).strip()
import json
module = json.loads(text)
write_module(module, output_dir)
print("Failed to produce a valid module after retries — manual review needed")
return FalseUsage
python tf_generator.py \
"S3 bucket with versioning, lifecycle rule to Glacier after 90 days, \
bucket policy restricting access to VPC endpoint vpce-0abc123"
# Output: ./generated-module/{main.tf,variables.tf,outputs.tf,README.md}
# Running terraform init + validate...
# Module valid after 1 attempt(s)What You Get
# generated-module/main.tf (excerpt)
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
id = "glacier-after-90-days"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER"
}
}
}Guardrails Before You Trust the Output
terraform validatecatches syntax and type errors — it does not catch bad security design. Always runtfsecorcheckovon generated modules before merging.- Never let generated modules apply directly to production — route them through the same PR + plan review as human-written Terraform.
- Pin the generated provider version explicitly; don't let Claude pick "latest."
- Treat this as a first draft generator, not a replacement for someone who understands the module's blast radius.
# Always run after generation, before PR
tfsec generated-module/
terraform plan -var-file=prod.tfvars # Review every resource before applyMore AI DevOps tools? Read our Build AI Terraform drift detector with Claude API and Build AI deployment validator with Claude API and OPA.
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
How to Use AI Agents to Automate Terraform Infrastructure Changes in 2026
AI agents can now plan, review, and apply Terraform changes from natural language. Here's how agentic AI is transforming infrastructure-as-code workflows.
AI Agents for Automated Terraform Code Review — The Future of IaC Quality
How AI agents are automating Terraform code review with security scanning, cost estimation, best practice enforcement, and drift prevention. Covers practical tools, custom LLM pipelines, and CI/CD integration.
AI Agents Making Progressive Delivery Decisions: Where Canary Analysis Is Heading in 2026
Flagger and Argo Rollouts already automate canary promotion against fixed thresholds. The next step is agents that reason about canary metrics the way a human on-call engineer would — accounting for context a static threshold can't capture.