The Idea
Every time I needed a new AWS resource, I'd spend 20 minutes reading Terraform docs just to get the syntax right for something I'd done before. I wanted to type plain English and get working HCL back. But I also didn't want to just generate code — I wanted to know if it actually deploys. So I tested every resource by running terraform apply against a real AWS account.
How It Works
You describe infrastructure in plain English. The tool sends it to a local Llama 3.2 model via Ollama, which returns four Terraform files. Those files get saved to a generated/ folder, ready for terraform init and terraform apply.
Plain English → Python → Ollama (local) → Parse HCL → main.tf + variables.tf + outputs.tf + tfvars.example
The key piece is the prompt. Getting consistent, parseable HCL out of an LLM required a very specific structure:
prompt = f"""You are a Terraform/OpenTofu expert. Generate production-ready infrastructure code.
USER REQUEST:
{description}
PROVIDER: {provider}
CRITICAL:
- Generate ONLY valid Terraform/HCL code
- NO markdown formatting or code blocks
- Start each file with a comment showing the filename
- Separate files with: ### FILENAME ###
Format your response like this:
### main.tf ###
terraform {{
required_version = ">= 1.0"
required_providers {{
{provider} = {{
source = "hashicorp/{provider}"
version = "~> 5.0"
}}
}}
}}
[rest of main.tf code]
### variables.tf ###
[variables code]
### outputs.tf ###
[outputs code]
### terraform.tfvars.example ###
[example values]
Generate production-ready code now:"""
The ### FILENAME ### markers are what make the response parseable. The script splits on ###, reads the filename, grabs everything after it until the next marker, and writes that to disk. There's also a fallback parser for when the model goes off-script and wraps things in code blocks anyway.
Test Results: 10 Resources Tested
| Resource | Generated | Validated | Deployed |
|---|---|---|---|
| EC2 instance | ✅ | ✅ | ✅ |
| S3 bucket | ✅ | ✅ | ✅ |
| IAM role | ✅ | ✅ | ✅ |
| VPC + subnets | ✅ | ✅ | ✅ |
| Security group | ✅ | ✅ | ✅ |
| RDS instance | ✅ | ✅ | ✅ |
| ALB | ✅ | ✅ | ✅ |
| Lambda | ✅ | ✅ | ✅ |
| ECS task | ✅ | ⚠️ needed fix | ✅ |
| Complex module | ✅ | ⚠️ needed fix | ✅ |
8/10 deployed first try. 2/10 needed minor manual fixes.
The ECS task definition had an incorrect network_mode value for Fargate. The complex multi-resource module had a missing depends_on for the security group. Both were one-line fixes once terraform validate pointed at them.
What It's Good At vs Where It Struggles
Good at:
- Standard single-resource configs (EC2, S3, IAM, RDS) — near-perfect every time
- Wiring dependencies correctly between resources it knows well
- Generating
variables.tfwith descriptions and sensible defaults - Adding tags and naming conventions without being asked
Struggles with:
- Very new AWS resources where Llama's training data is thin
- Complex modules with many interdependent resources — sometimes misses a
depends_on - Provider version pinning — occasionally suggests a deprecated argument from an older AWS provider version
- ECS/EKS specifics — these configs are dense and the model sometimes gets task definition fields wrong
Honest assessment: treat it like a junior engineer who's read all the Terraform docs but hasn't deployed much to production. Good first draft, always needs a review.
The Prompt Engineering That Made It Work
Three things made the difference between garbage output and deployable HCL:
1. Kill the markdown. LLMs love wrapping code in hcl blocks. That breaks the file parser completely. The explicit instruction NO markdown formatting or code blocks eliminated this.
2. Show the exact format in the prompt. Telling the model to use ### main.tf ### as a separator, and including the terraform {} block structure directly in the prompt, anchored the output format. Without this, every response looked slightly different.
3. Demand variables explicitly. Early versions hardcoded values like instance_type = "t3.micro" directly in main.tf. Adding Use variables for configurable values to the requirements section fixed this — now everything configurable lands in variables.tf with proper descriptions.
Why Local Matters for IaC Generation
Your Terraform descriptions contain your architecture. "Create a VPC with private subnets, an RDS cluster for our auth service, and an ECS task that pulls from our private ECR registry" — that's a roadmap of your production infrastructure. Sending that to a cloud API means it leaves your machine.
Running Ollama locally means the description, the generated code, and any sensitive context like account IDs or naming patterns stay on your machine. For anything touching production infrastructure, that's not optional.
Try It
GitHub: https://github.com/ThinkWithOps
Live deploy demo: https://youtu.be/nhhZqrCEhOA
git clone https://github.com/ThinkWithOps/ai-devops-projects
cd ai-devops-projects/05-ai-terraform-generator
pip install -r requirements.txt
# Generate code
python src/terraform_generator.py \
--description "EC2 instance with S3 bucket for logs" \
--provider aws
# Deploy it
cd generated/
cp terraform.tfvars.example terraform.tfvars
terraform init && terraform plan && terraform apply
Project 5 in my AI+DevOps series — all tools run locally with Ollama, zero cloud API costs.
What's your current Terraform workflow? I'm curious whether people are using Copilot for this, manually writing it, or something else entirely — drop it in the comments.
Top comments (0)