Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Quick reference for Terraform conventions including file organization, naming, modules, state, security, and anti-patterns. Use when writing or reviewing Terraform code.
.claude/skills/kunanonj-terraform-best-practices/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 4% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 81% | 0% |
terraform/
live/ # Orchestration — providers, backend, module calls
terraform.tf # backend + provider (ONLY place for providers)
variables.tf # all input variables
locals.tf # computed values, remote state refs
outputs.tf # exported values
{resource-group}.tf # module invocations grouped by concern
modules/{name}/ # Reusable — no providers, no hardcoded values
main.tf # locals, data sources
variables.tf # inputs with descriptions + types
outputs.tf # consumed values only
versions.tf # required_providers
{resource}.tf # one file per resource type
envs/{env}/ # Per-environment config
state.config # backend partial config
terraform.tfvars # non-sensitive values
secrets.tfvars # sensitive values (gitignored)| Thing | Convention | Example | |-------|-----------|---------| | Resource prefix | {project}-{service}-{env} | acme-payments-prod | | Variables | snake_case | instance_class | | Locals | snake_case | name_prefix | | Outputs | snake_case | repository_url | | Resources | this (primary) or descriptive | aws_db_instance.this | | Security groups | name_prefix (not name) | "${local.name_prefix}-app-" | | Files | {resource-type}.tf | rds.tf, sg.tf, ecr.tf | | Modules | kebab-case directory | modules/ecs-service/ | | Tags | PascalCase keys | Project, Environment, ManagedBy |
Use modules from the c0x12c Terraform Registry. Each module source follows c0x12c/{name}/aws — see the registry for available modules and versions.
hcl# Calling a registry module — always version-pin module "database" { source = "c0x12c/rds/aws" version = "~> 0.6.6" name = "${local.name_prefix}-db" vpc_id = local.vpc_id subnet_ids = local.private_subnet_ids tags = local.common_tags } # Inside a module — no provider, explicit interface # versions.tf terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" } } } # variables.tf — every var has description + type variable "name" { description = "Resource name prefix" type = string } # outputs.tf — only what consumers need output "endpoint" { description = "Connection endpoint" value = aws_db_instance.this.endpoint }
hcl# Backend config — S3 + DynamoDB locking terraform { backend "s3" {} } # envs/dev/state.config bucket = "{project}-terraform-state" key = "{service}/dev/terraform.tfstate" region = "us-east-1" dynamodb_table = "{project}-terraform-locks" encrypt = true # Init with partial config # terraform init -backend-config=../envs/dev/state.config
hcl# Remote state for cross-stack references data "terraform_remote_state" "infra" { backend = "s3" config = { bucket = "{project}-terraform-state" key = "infra/terraform.tfstate" region = var.region } } locals { vpc_id = data.terraform_remote_state.infra.outputs.vpc_id }
hcl# Sensitive variables variable "db_password" { type = string sensitive = true } # S3 — block public, encrypt, version module "s3" { versioning = true server_side_encryption = { sse_algorithm = "aws:kms" } block_public_access = { block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } } # RDS — encrypt, private subnet, protect resource "aws_db_instance" "this" { storage_encrypted = true deletion_protection = var.env == "prod" publicly_accessible = false # ALWAYS false } # Security groups — source SG, not CIDR resource "aws_security_group_rule" "app_to_db" { source_security_group_id = aws_security_group.app.id # not cidr_blocks from_port = 5432 to_port = 5432 } # Default tags at provider level provider "aws" { default_tags { tags = { Project = var.project Service = var.service Environment = var.env ManagedBy = "terraform" } } }
hcl# WRONG — provider in module # modules/rds/main.tf provider "aws" { region = "us-east-1" } # NEVER in a module # WRONG — no version pin module "rds" { source = "git::https://github.com/{project}/terraform-modules.git//rds" # missing ?ref=vX.Y.Z } # WRONG — hardcoded values resource "aws_s3_bucket" "assets" { bucket = "acme-prod-assets" # use ${local.name_prefix}-assets } # WRONG — secrets in code resource "aws_db_instance" "main" { password = "hunter2" # use var.db_password (sensitive) } # WRONG — wildcard IAM resource "aws_iam_policy" "app" { policy = jsonencode({ Statement = [{ Action = "*", Resource = "*", Effect = "Allow" }] }) } # WRONG — public database resource "aws_db_instance" "main" { publicly_accessible = true # NEVER for databases } # WRONG — no state locking terraform { backend "s3" { # missing dynamodb_table for locking } } # WRONG — all resources in one file # main.tf with 500+ lines of mixed RDS, S3, SQS, IAM... # Split into rds.tf, s3.tf, sqs.tf, iam.tf
yaml# Standard workflow # PR: fmt check → validate → plan (comment on PR) # Merge to main: init → plan → apply # Key rules: # - Never auto-apply on PR # - Always post plan output as PR comment # - Lock state during apply (DynamoDB) # - Inject secrets via CI environment variables # - Pin Terraform version in CI to match team
.tf files or committed .tfvars* on *)terraform import in automation (use import blocks)description on variables and outputscount for conditional resources (use for_each with a set)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 24,844 | 20,817 | -16% | 1 | 1 | 0% | 5,615 | 6,689 | +19% | 0 | 0 | — |
case-02 | fail→pass | 19,835 | 20,914 | +5% | 1 | 1 | 0% | 4,496 | 7,139 | +59% | 0 | 0 | — |
case-03 | fail→pass | 18,420 | 17,088 | -7% | 1 | 1 | 0% | 3,984 | 5,729 | +44% | 0 | 0 | — |
case-04 | pass→pass | 8,329 | 8,647 | +4% | 1 | 1 | 0% | 1,688 | 3,839 | +127% | 0 | 0 | — |
case-05 | pass→pass | 10,331 | 6,051 | -41% | 1 | 1 | 0% | 1,938 | 3,068 | +58% | 0 | 0 | — |
case-06 | pass→pass | 8,826 | 10,956 | +24% | 1 | 1 | 0% | 1,822 | 4,181 | +129% | 0 | 0 | — |
case-07 | pass→pass | 9,486 | 5,785 | -39% | 1 | 1 | 0% | 1,818 | 2,978 | +64% | 0 | 0 | — |
case-08 | pass→pass | 7,712 | 5,580 | -28% | 1 | 1 | 0% | 1,417 | 2,949 | +108% | 0 | 0 | — |
case-09 | pass→pass | 10,970 | 4,676 | -57% | 1 | 1 | 0% | 2,198 | 2,699 | +23% | 0 | 0 | — |
case-10 | pass→pass | 8,626 | 5,867 | -32% | 1 | 1 | 0% | 1,512 | 2,927 | +94% | 0 | 0 | — |
case-11 | pass→pass | 10,875 | 1,159 | -89% | 1 | 1 | 0% | 1,931 | 2,085 | +8% | 0 | 0 | — |
case-12 | pass→pass | 10,932 | 5,487 | -50% | 1 | 1 | 0% | 1,994 | 3,038 | +52% | 0 | 0 | — |
case-13 | fail→pass | 13,290 | 3,137 | -76% | 1 | 1 | 0% | 2,376 | 2,464 | +4% | 0 | 0 | — |
case-14 | fail→pass | 11,228 | 7,092 | -37% | 1 | 1 | 0% | 1,791 | 3,235 | +81% | 0 | 0 | — |
case-15 | fail→pass | 10,467 | 1,952 | -81% | 1 | 1 | 0% | 1,737 | 2,239 | +29% | 0 | 0 | — |
case-16 | pass→pass | 10,058 | 6,466 | -36% | 1 | 1 | 0% | 1,928 | 3,059 | +59% | 0 | 0 | — |
case-17 | pass→pass | 2,752 | 2,620 | -5% | 1 | 1 | 0% | 439 | 2,247 | +412% | 0 | 0 | — |
case-18 | fail→fail | 12,670 | 6,230 | -51% | 1 | 1 | 0% | 2,264 | 3,100 | +37% | 0 | 0 | — |
case-19 | pass→pass | 9,419 | 6,290 | -33% | 1 | 1 | 0% | 1,797 | 3,052 | +70% | 0 | 0 | — |
case-20 | pass→pass | 8,490 | 5,178 | -39% | 1 | 1 | 0% | 1,626 | 2,803 | +72% | 0 | 0 | — |
case-21 | pass→pass | 9,644 | 5,951 | -38% | 1 | 1 | 0% | 1,592 | 2,944 | +85% | 0 | 0 | — |
case-22 | pass→pass | 10,858 | 5,914 | -46% | 1 | 1 | 0% | 1,951 | 2,993 | +53% | 0 | 0 | — |
case-23 | pass→pass | 5,712 | 2,539 | -56% | 1 | 1 | 0% | 1,076 | 2,428 | +126% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.