| name | terraform |
| description | Design and manage infrastructure as code with Terraform — modules, remote state (S3 + DynamoDB), Stacks (deployments), test framework, preconditions/postconditions, moved/removed blocks, and CI/CD plan/apply separation. Use when user asks to write Terraform config, set up remote state, design modules, manage state, or automate infrastructure. Do NOT use for Kubernetes (use kubernetes), Docker (use docker), or CI/CD pipeline design (use ci-cd). |
| license | MIT |
| compatibility | opencode |
| metadata | {"workflow":"infrastructure","audience":"devops","version":"2.0"} |
Terraform Architect
Design infrastructure as code with Terraform 1.10+ features: Stacks, test framework, provider-defined functions, and state management.
Workflow
Step 1: Determine project structure
| Scale | Structure | State Strategy |
|---|
| Personal | Single main.tf | Remote backend, optional workspaces |
| Team (2-5) | envs/{dev,prod}/modules/ | Directory-per-environment, separate backends |
| Platform team | infra/{networking,compute,data,iam}/ per repo | Per-component state, terraform_remote_state |
Step 2: Bootstrap remote backend
# backend.tf
terraform {
backend "s3" {
bucket = "tf-state-{account}-{region}"
key = "{env}/{component}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "tf-state-lock"
}
required_version = ">= 1.10"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
Step 3: Design modules
Single responsibility: one module = one domain.
modules/
├ networking/
│ main.tf, variables.tf, outputs.tf
├ compute/
│ main.tf, variables.tf, outputs.tf
└ database/
main.tf, variables.tf, outputs.tf
environments/
├ prod/
│ backend.tf -> key = "prod/compute/terraform.tfstate"
│ main.tf module "compute" { source = "../../modules/compute" }
│ terraform.tfvars
└ dev/
Step 4: Use preconditions/postconditions
resource "aws_db_instance" "main" {
allocated_storage = 100
engine = "postgres"
engine_version = "16.3"
instance_class = "db.r6g.large"
lifecycle {
postcondition {
condition = self.engine == "postgres"
error_message = "Only PostgreSQL is supported"
}
}
}
data "aws_iam_policy_document" "example" {
statement {
actions = ["s3:GetObject"]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["true"]
}
condition {
test = "IpAddress"
variable = "aws:SourceIp"
values = var.allowed_ips
}
}
lifecycle {
precondition {
condition = length(var.allowed_ips) > 0
error_message = "At least one allowed IP must be specified"
}
}
}
Step 5: Use moved and removed blocks for refactoring
# Instead of manual state mv, code-review it:
moved {
from = aws_s3_bucket.old
to = module.storage.aws_s3_bucket.main
}
# To remove a resource from state without destroying it:
removed {
from = aws_instance.legacy
lifecycle {
destroy = false # Keep the resource alive
}
}
Terraform Stacks (2025+)
Stacks enable deployment of multiple configurations with shared state:
# stacks/stack.hcl
stack "dev" {
source = "./infrastructure"
path = "dev"
}
stack "prod" {
source = "./infrastructure"
path = "prod"
}
Provider-defined Functions (1.10+)
# Built-in providers now expose functions
result = provider::aws::arn_parse("arn:aws:s3:::my-bucket")
# or
result = provider::aws::arn_build("s3", "my-bucket", "", "us-east-1")
Testing Framework
# tests/example.tftest.hcl
run "create_bucket" {
command = apply
variables {
bucket_name = "test-bucket-${run_id}"
}
assert {
condition = aws_s3_bucket.main.bucket == "test-bucket-${run_id}"
error_message = "Bucket name mismatch"
}
}
run "verify_encryption" {
command = apply
assert {
condition = aws_s3_bucket.main.server_side_encryption_configuration[0].rule[0].apply_server_side_encryption_by_default[0].sse_algorithm == "AES256"
error_message = "Bucket must have AES256 encryption"
}
}
terraform test
State Management Rules
| Rule | Why |
|---|
| Remote state always | Local is single-player |
| S3 + DynamoDB | Standard state storage + locking |
| Versioning on state bucket | Rollback bad apply |
| KMS encryption | State files contain secrets |
| Per-environment isolation | destroy in dev should never touch prod |
| Per-component state | Change IAM should not re-evaluate RDS |
| Directory-per-environment preferred | Workspaces share backend — too easy to select prod by accident |
moved blocks over state rm/mv | Code-reviewed, reversible, self-documenting |
Provider Caching (CI speedup)
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
mkdir -p $TF_PLUGIN_CACHE_DIR
CI/CD Pipeline
name: Terraform
on: [pull_request, push]
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform fmt -check
- run: terraform validate
- run: terraform plan -out=tfplan
- uses: actions/upload-artifact@v4
with: { name: tfplan, path: tfplan }
apply:
if: github.ref == 'refs/heads/main'
needs: [plan]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
{ }
Emergency State Surgery
| Situation | Command |
|---|
| Remove resource from state | terraform state rm <address> |
| Import existing resource | terraform import <address> <id> |
| Move resource (refactoring) | terraform state mv <from> <to> |
| Unlock stuck state | terraform force-unlock <lock-id> |
| Rollback corrupted state | Restore previous S3 version |
| List resources | terraform state list |
| Show resource details | terraform state show <address> |
Production Checklist
Anti-Patterns
| Anti-pattern | Fix |
|---|
| Local state in team project | Remote state (S3 + DynamoDB) |
| One giant state file | Split by component |
| Workspaces for env isolation | Directory-per-environment |
Manual state mv instead of moved blocks | Code-reviewed moved blocks |
latest provider version | Pin ~> 5.0 |
Running apply from laptop | CI/CD with saved plan |
| No locking | DynamoDB table for state lock |
| Secrets in state outputs | Mark sensitive = true, use external secrets manager |
| No preconditions/postconditions | Add for security-critical resources |
Sources
- Terraform Documentation (developer.hashicorp.com/terraform)
- Terraform Stacks — HCP documentation
- Terraform Test Framework — developer.hashicorp.com
- HashiCorp Learn: moved blocks
- AWS Well-Architected IaC patterns
- Gruntwork — Terraform best practices