🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

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.

Shubham3 min read
Share:Tweet

"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

bash
pip install anthropic
# terraform CLI must be installed and on PATH

Module Generator

python
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

python
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 False

Usage

bash
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

hcl
# 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 validate catches syntax and type errors — it does not catch bad security design. Always run tfsec or checkov on 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.
bash
# Always run after generation, before PR
tfsec generated-module/
terraform plan -var-file=prod.tfvars    # Review every resource before apply

More 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

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