Generated banner for Terraform Skill
Generated logo for Terraform Skill

Terraform Skill

Anton BabenkoCommunityCreated by Anton Babenko

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • Medium risk

From the author

Use when writing, reviewing, or debugging Terraform/OpenTofu modules, tests, CI, scans, or state ops - diagnoses failure mode (identity churn, secrets, blast radius, CI drift, state corruption) with version-aware guards.
— the description by Anton Babenko

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured workflow for managing Terraform and OpenTofu configurations. It emphasizes a diagnose-before-generate approach, requiring explicit context such as runtime versions, provider details, and environment criticality. It includes a response contract to ensure risk-aware planning, validation, and rollback procedures, while offering specific guidance on module hierarchy, state management, security, and testing strategies.

What it does

  • Diagnoses infrastructure failure modes like identity churn, state corruption, and secret exposure.
  • Enforces a response contract requiring version floors, risk assessment, and validation plans.
  • Provides patterns for module development, directory layout, and naming conventions.
  • Offers guidance on CI/CD pipeline stages, including plan artifact review and drift prevention.
  • Facilitates safe state management, including backend selection and locking protocols.
  • Supports semantic code navigation using terraform-ls or fallback search tools.

When to use it

  • When writing or reviewing Terraform/OpenTofu modules and configurations.
  • When debugging CI/CD pipeline failures or infrastructure drift.
  • When planning complex refactors or state migrations.
  • When configuring remote state backends or implementing security compliance checks.

Advantages

  • Reduces risk of destructive changes through mandatory plan-destroy protocols.
  • Improves code maintainability via standardized module and variable contracts.
  • Ensures consistent security practices through integrated policy and secret handling.
  • Provides clear version-aware guidance to prevent common configuration errors.

Usage examples

“I am refactoring a module and worried about resource identity churn. How should I proceed?”
The agent will guide you through using 'moved' blocks, comparing 'count' vs 'for_each' patterns, and validating the plan to ensure no unexpected resource destruction occurs.
“How do I securely manage secrets in my Terraform configuration?”
The agent will advise on using cloud-native secret managers, 'sensitive' variable flags, or 'write_only' arguments, and will check your implementation against security best practices.
“Help me set up a CI/CD pipeline for my infrastructure.”
The agent will provide a structured pipeline template including validation, testing, plan artifact generation, and environment-protected apply stages.
“My state file is locked and I cannot run any operations. What should I do?”
The agent will diagnose the state backend, verify locking mechanisms, and provide recovery steps based on your specific provider and backend configuration.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/antonbabenko/terraform-skill/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip terraform-skill.zip -d .agents/skills

Documentation by Anton Babenko

Terraform Skill for Claude

Diagnose-first guidance for Terraform and OpenTofu. Core file is a workflow; depth lives in references loaded on demand.

Response Contract

Every Terraform/OpenTofu response must include:

  1. Assumptions & version floor — runtime (terraform or tofu), exact version, providers, state backend, execution path (local/CI/Cloud/Atlantis), environment criticality. State assumptions explicitly if the user did not provide them.

  2. Risk category addressed — one or more of: identity churn, secret exposure, blast radius, CI drift, compliance gaps, state corruption, provider upgrade risk, testing blind spots.

  3. Chosen remediation & tradeoffs — what was chosen, what was traded off, why.

  4. Validation plan — exact commands (fmt -check, validate, plan -out, policy check) tailored to runtime and risk tier.

  5. Rollback notes — for any destructive or state-mutating change: how to undo, what evidence to keep.

Never recommend direct production apply without a reviewed plan artifact and approval.

Never run terraform destroy (targeted or full) without first running terraform plan -destroy and showing the user every resource that will be deleted — including implicit dependents pulled in via locals or for_each. Get explicit confirmation before proceeding. Never use -auto-approve on destroy.

Workflow

  1. Capture execution context — runtime+version, provider(s), backend, execution path, environment criticality.

  2. Diagnose failure mode(s) using the routing table below. If intent spans categories, load both references.

  3. Load only the matching reference file(s) — do not preload depth the task does not need.

  4. Propose fix with risk controls — why this addresses the mode, what could still go wrong, guardrails (tests/approvals/rollback).

  5. Generate artifacts — HCL, migration blocks (moved, import), CI changes, policy rules.

  6. Validate before finalizing — run validation commands tailored to risk tier.

  7. Emit the Response Contract at the end.

Diagnose Before You Generate

Failure categorySymptomsPrimary references
Identity churnResource addresses shift after refactor, count index churn, missing moved blocksCode Patterns: count vs for_each, Code Patterns: moved blocks, Code Patterns: LLM mistakes
Secret exposureSecrets in defaults, state, logs, CI artifactsSecurity & Compliance, Code Patterns: write-only, State Management
Blast radiusOversized stacks, shared prod/non-prod state, unsafe appliesState Management, Module Patterns
Destroy cascadeTargeted destroy deletes more than expected; locals referencing a targeted resource make all for_each consumers implicit dependentsResponse Contract: plan-destroy first; State Management: Safe Destroy
CI driftLocal plan ≠ CI plan, apply without reviewed artifact, unpinned versionsCI/CD Workflows, Code Patterns: versions
Compliance gapsMissing policy stage, no approval model, no evidence retentionSecurity & Compliance, CI/CD Workflows
Testing blind spotsPlan-only validation of computed values, set-type indexing, mock/real confusionTesting Frameworks
State corruption / recoveryStuck lock, backend migration, drift reconciliationState Management
Provider upgrade riskBreaking-change provider bump, unpinned modulesCode Patterns: versions, Module Patterns
Provider lifecycleRemoving a provider with resources still in state, orphaned resources, removed block usageState Management: Provider Removal
Bootstrap / orchestration misusenull_resource + local-exec for bootstrap, remote-exec for setup scripts, provisioner stdout leaking secrets in CI logsCode Patterns: Provisioners as Last Resort
Navigation / safe-rename blind spotsCannot locate symbol defs/refs semantically, value-symbol rename done as blind text replace, grep-only refactor missing refs, hallucinated rg shimCode Intelligence
Cross-cloud / provider mapping"What's the Azure/GCP equivalent of X", picking a backend/auth model per cloudState Management: Cross-cloud equivalents

When to Use This Skill

Activate when: creating or reviewing Terraform/OpenTofu configurations or modules, setting up or debugging tests, structuring multi-environment deployments, implementing IaC CI/CD, choosing module patterns or state organization, configuring or migrating remote state backends.

Don't use for: basic HCL syntax questions Claude already knows, provider API reference (link to docs), cloud-platform questions unrelated to Terraform/OpenTofu.

Core Principles

Module Hierarchy
TypeWhen to UseScope
Resource moduleSingle logical group of connected resourcesVPC + subnets, SG + rules
Infrastructure moduleCollection of resource modules for a purposeMultiple resource modules in one region/account
CompositionComplete infrastructureSpans multiple regions/accounts

Flow: resource → resource module → infrastructure module → composition.

Directory Layout
environments/   # prod/ staging/ dev/  — per-env configurations
modules/        # networking/ compute/ data/ — reusable modules
examples/       # minimal/ complete/ — docs + integration fixtures

Separate environments from modules. Use examples/ as both documentation and test fixtures. Keep modules small and single-responsibility.

See Module Patterns for architecture principles, naming conventions, variable/output contracts.

Naming Conventions (summary)
  • Descriptive resource names (aws_instance.web_server, not aws_instance.main)

  • Reserve this for genuine singleton resources only

  • Prefix variables with context (vpc_cidr_block, not cidr)

  • Standard files: main.tf, variables.tf, outputs.tf, versions.tf

See Module Patterns: Variable Naming and Code Patterns: Block Ordering for examples.

Block Ordering (summary)

Resource blocks: count/for_each first → arguments → tags → depends_on → lifecycle. Variable blocks: description → type → default → validation → nullable → sensitive.

See Code Patterns: Block Ordering & Structure for the full rules and examples.

Testing Strategy

Decision Matrix: Which Testing Approach?
SituationApproachToolsCost
Quick syntax checkStatic analysisvalidate, fmtFree
Pre-commit validationStatic + lintvalidate, tflint, trivy, checkovFree
Terraform 1.6+, simple logicNative test frameworkterraform testFree-Low
Pre-1.6, or Go expertiseIntegration testingTerratestLow-Med
Security/compliance focusPolicy as codeOPA, SentinelFree
Cost-sensitive workflowMock providers (1.7+)Native tests + mocksFree
Multi-cloud, complexFull integrationTerratest + real infraMed-High
Native Test Rules (1.6+)

Before writing test code: validate resource schemas via Terraform MCP so assertions target real attributes.

  • command = plan — fast, for input-derived values only

  • command = apply — required for computed values (ARNs, generated names) and set-type nested blocks

  • Set-type blocks cannot be indexed with [0] — use for expressions or materialize via command = apply

  • Common set types: S3 encryption rules, lifecycle transitions, IAM policy statements

See Testing Frameworks for static-analysis pipelines, native-test patterns, Terratest integration, mock providers, and the full LLM-mistake checklist.

Count vs For_Each — Quick Rule

ScenarioUseWhy
Boolean condition (create / don't)count = condition ? 1 : 0Optional singleton toggle
Items may be reordered or removedfor_each = toset(list)Stable resource addresses
Reference by keyfor_each = mapNamed access
Multiple named resourcesfor_eachBetter identity stability

Never use list index as long-lived identity — removing a middle element reshuffles every address after it. For the decision matrix, safe migration playbook, moved block patterns, and known-at-plan failure cases, see Code Patterns: count vs for_each.

Locals for Dependency Management

Using try() in a local to prefer a conditional resource's attribute over its parent is a specialized but high-value pattern — it forces correct deletion order without explicit depends_on. Common use: VPC + secondary CIDR associations + subnets.

See Code Patterns: Locals for Dependency Management for the full pattern and worked example.

Module Development

Standard layout:

my-module/
├── README.md       # Usage documentation
├── main.tf         # Primary resources
├── variables.tf    # Typed inputs with descriptions
├── outputs.tf      # Output values
├── versions.tf     # required_version + required_providers
├── examples/
│   ├── minimal/
│   └── complete/
└── tests/
    └── module_test.tftest.hcl   # or Go for Terratest

Variable contracts: always description, always explicit type, use validation for complex constraints, use sensitive = true for secrets, prefer optional() with typed defaults (1.3+) over untyped map(any).

Output contracts: always description, mark sensitive outputs, expose stable subsets (not whole provider objects).

See Module Patterns for the full contract patterns, module release checklist, and LLM-mistake checklist.

CI/CD

Pipeline stages: validate → test → plan → apply (with environment protection).

Cost control: mock providers on PR validation, real-cloud integration only on main or scheduled, tag test resources, auto-cleanup.

Drift prevention: pin runtime and providers, commit .terraform.lock.hcl, apply the reviewed plan artifact from the plan stage (do not re-run plan inside the apply job), run policy/security stage on every path to apply.

See CI/CD Workflows for GitHub Actions, GitLab CI, and Atlantis templates plus the LLM-mistake checklist.

Security & Compliance

Essential checks:

trivy config .
checkov -d .

Don't: store secrets in variables or .tfvars, use default VPC, skip encryption, open security groups to 0.0.0.0/0, use inline ingress/egress blocks in aws_security_group.

Do: source secrets from a cloud secret manager (AWS Secrets Manager / Azure Key Vault / GCP Secret Manager) or use write_only arguments on 1.11+, create dedicated VPCs, enforce encryption at rest and TLS, least-privilege SGs, use separate aws_vpc_security_group_{ingress,egress}_rule resources (e.g. AWS provider v5+).

Marking a variable sensitive = true masks display only — the value still lives in state. Use write_only / *_wo on 1.11+, or keep secret material out of Terraform entirely via runtime lookups.

See Security & Compliance for trivy/checkov pipelines, state-file hardening, compliance mappings, and the LLM-mistake checklist.

State Management

Never use local state in teams or production. Remote backends provide automatic locking, encryption, versioning, audit logging, and safe collaboration.

Choosing a Remote Backend

AWS example (Azure azurerm / GCP gcs / TF Cloud syntax: see State Management: Choosing a Remote Backend):

terraform {
  backend "s3" {
    bucket        = "my-terraform-state"
    key           = "prod/vpc/terraform.tfstate"
    region        = "us-east-1"
    encrypt       = true
    use_lockfile  = true   # Native S3 locking, 1.10+
  }
}

On Terraform < 1.10, use dynamodb_table = "terraform-state-lock" instead of use_lockfile. Azure Storage, GCS, and Terraform Cloud all offer built-in locking - see the State Management reference for syntax. For choosing among backends and their locking models, see Choosing a Remote Backend.

State Organization
PatternUse WhenExample Path
Per environmentDifferent teams per envprod/terraform.tfstate, staging/...
Per componentIndependent lifecyclesprod/vpc/, prod/eks/, prod/rds/
Hybrid (recommended)Both benefitsprod/networking/, prod/compute/, staging/networking/

Split state when: different teams, different update cadences, or >500 resources. Combine when: tightly coupled resources, <100 resources, same lifecycle.

See State Management for locking, migration, multi-team isolation, disaster recovery, and the LLM-mistake checklist.

Version Management

ComponentStrategyExample
Terraform runtimePin minorrequired_version = "~> 1.9"
ProvidersPin majorversion = "~> 5.0"
Modules (prod)Pin exactversion = "5.1.2"
Modules (dev)Allow patchversion = "~> 5.1"

Commit .terraform.lock.hcl intentionally. Keep provider/runtime upgrades in a separate PR from functional changes. See Code Patterns: Version Management for constraint syntax and upgrade workflow.

Modern Terraform Features (1.0+)

FeatureMin versionCommon use
try()0.13+Safe fallbacks, replaces element(concat())
nullable = false1.1+Prevent null silently overriding defaults
moved blocks1.1+Refactor without destroy/recreate
optional() with defaults1.3+Typed object attributes
import blocks1.5+Declarative imports, reviewable in VCS
check blocks1.5+Runtime assertions
Native terraform test1.6+Built-in test framework
Mock providers1.7+Cost-free unit testing
removed blocks1.7+Declarative resource removal
Provider-defined functions1.8+Provider-specific transformations (requires provider to declare functions)
Cross-variable validation1.9+Reference other var.* in validation blocks
write_only arguments1.11+Secrets never stored in state
S3 native lock-file1.10+State locking without DynamoDB

Before emitting a feature, verify the runtime floor. See Code Patterns: Feature Guard Table for the full table with common LLM error patterns per feature.

Runtime-Specific Guidance

  • Terraform 1.0-1.5 (OpenTofu starts at 1.6): Terratest for integration, static analysis + plan validation only (no native tests).

  • 1.6+: native terraform test / tofu test available — migrate simple unit tests, keep Terratest for complex integration.

  • 1.7+: mock providers cut test cost — mock for unit tests, real runs for final integration.

  • 1.10+: S3 native lock-file (use_lockfile) is the correct default for new configurations — DynamoDB locking is no longer required.

  • 1.11+: write_only arguments for secret handling keep credentials out of state.

  • Terraform vs OpenTofu: both supported. For licensing, governance, and feature delta, see Quick Reference: Terraform vs OpenTofu.

Code Intelligence (terraform-ls)

Semantic navigation for HCL. terraform-ls is optional; without it every row below degrades to a disclosed rg + Read fallback.

Self-contained terraform-ls layer of a generic code-intelligence discipline - apply the rows below directly. Recommended companion: the code-intelligence plugin (same antonbabenko/agent-plugins marketplace) carries the generic discipline (position anchoring, degradation gate, disclosure format, anti-phantom-shim) and ships /code-intelligence:doctor for readiness. If it is installed, defer to its generic protocol; this skill stays fully self-contained without it.

GoalUseTradeoff
Find definition / all referencesterraform-ls goToDefinition / findReferencesNeeds init + a position anchor
Rename value symbol (var/local/output/provider alias)Manual: findReferences -> per-file fresh Read -> edit -> validateNo rename provider
Rename resource/module addressmoved block + plan shows 0 destroyText rename forces destroy/recreate
Exact text / known name / .tfvars / non-HCLrg + ReadNo semantic scope

✅ Supported: goToDefinition, findReferences, documentSymbol, hover, workspaceSymbol. ❌ Unsupported: goToImplementation, call hierarchy, rename provider. Do not call these then report their absence as a finding.

  • ✅ Prereq: local terraform/tofu on PATH, terraform init run; cold start may need one retry.

  • ✅ LSP calls are position-anchored (file:line:character) - anchor with rg first, never symbol-name-only.

  • ❌ Do not claim "LSP broken, using rg" until the Degradation Gate passes; disclose any tool substitution on the first line.

Depth: Code Intelligence.

Reference Files

Progressive disclosure — essentials here, depth on demand:

  • Testing Frameworks — static analysis, native tests, Terratest, mock providers

  • Module Patterns — structure, variable/output contracts, terraform_remote_state rules, release checklist

  • CI/CD Workflows — GitHub Actions, GitLab CI, Atlantis, cost control

  • Security & Compliance — trivy/checkov, secrets handling, compliance mappings

  • State Management — backends, locking, migration, multi-team, recovery

  • Code Patterns — block ordering, count/for_each deep dive, modern features, version management, locals

  • Code Intelligence - terraform-ls capabilities, position-anchored calls, manual rename, degradation gate

  • Quick Reference — command cheat sheets, flowcharts, troubleshooting

License

Apache License 2.0. See LICENSE for full terms.

Copyright © 2026 Anton Babenko

Files

references/module-patterns.md

Module Development Patterns

Part of: terraform-skill Purpose: Best practices for Terraform/OpenTofu module development

This document provides detailed guidance on creating reusable, maintainable Terraform modules. For high-level principles, see the main skill file.


Table of Contents

  1. Module Hierarchy

  2. Architecture Principles

  3. Module Structure

  4. Variable Best Practices

  5. Output Best Practices

  6. Common Patterns

  7. Anti-patterns to Avoid

  8. Testing Philosophy & Patterns


Module Hierarchy

Module Type Classification
TypeWhen to UseScopeExample
Resource ModuleSingle logical group of connected resourcesTightly coupled resources that always work togetherVPC + subnets, Security group + rules, IAM role + policies
Infrastructure ModuleCollection of resource modules for a purposeMultiple resource modules in one region/accountComplete networking stack, Application infrastructure
CompositionComplete infrastructureSpans multiple regions/accounts, orchestrates infrastructure modulesMulti-region deployment, Production environment

Hierarchy: Resource → Resource Module → Infrastructure Module → Composition

Resource Module

Characteristics:

  • Smallest building block

  • Single logical group of resources

  • Highly reusable across projects

  • Minimal external dependencies

  • Clear, focused purpose

Examples:

modules/
├── vpc/                    # Resource module
│   ├── main.tf            # VPC + subnets + route tables
│   ├── variables.tf
│   └── outputs.tf
├── security-group/         # Resource module
│   ├── main.tf            # Security group + rules
│   ├── variables.tf
│   └── outputs.tf
└── rds/                    # Resource module
    ├── main.tf            # RDS instance + subnet group
    ├── variables.tf
    └── outputs.tf
Infrastructure Module

Characteristics:

  • Combines multiple resource modules

  • Purpose-specific (e.g., "web application infrastructure")

  • May span multiple services

  • Region or account-specific

  • Moderate reusability

Examples:

modules/
└── web-application/        # Infrastructure module
    ├── main.tf            # Orchestrates multiple resource modules
    ├── variables.tf
    ├── outputs.tf
    └── README.md

# main.tf contents:
module "vpc" {
  source = "../vpc"
}

module "alb" {
  source = "../alb"
  vpc_id = module.vpc.vpc_id
}

module "ecs" {
  source = "../ecs"
  vpc_id = module.vpc.vpc_id
  subnets = module.vpc.private_subnet_ids
}
Composition

Characteristics:

  • Highest level of abstraction

  • Complete environment or application

  • Combines infrastructure modules

  • Environment-specific (dev, staging, prod)

  • Not reusable (environment-specific values)

Examples:

environments/
├── prod/                   # Composition
│   ├── main.tf            # Complete production environment
│   ├── backend.tf         # Remote state configuration
│   ├── terraform.tfvars   # Production-specific values
│   └── variables.tf
├── staging/                # Composition
│   ├── main.tf
│   ├── backend.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── dev/                    # Composition
    ├── main.tf
    ├── backend.tf
    ├── terraform.tfvars
    └── variables.tf
Decision Tree: Which Module Type?
Question 1: Is this environment-specific configuration?
├─ YES → Composition (environments/prod/, environments/staging/)
└─ NO  → Continue

Question 2: Does it combine multiple infrastructure concerns?
├─ YES → Infrastructure Module (modules/web-application/)
└─ NO  → Continue

Question 3: Is it a focused group of related resources?
└─ YES → Resource Module (modules/vpc/, modules/rds/)
File Organization Standards

Required files in all modules:

main.tf        # Resource definitions, module calls, data sources
variables.tf   # Input variable declarations
outputs.tf     # Output value declarations
versions.tf    # Provider and Terraform version constraints
README.md      # Usage documentation

Conditional files:

terraform.tfvars  # ONLY at composition level (NEVER in modules)
locals.tf         # For complex local value calculations
data.tf           # Optional: Data sources (if main.tf gets large)
backend.tf        # ONLY at composition level (remote state config)

Required structure for Terraform Registry publishing; keeps navigation consistent across modules.


Architecture Principles

1. Smaller Scopes = Better Performance + Reduced Blast Radius

Faster plan/apply, isolated failures, parallel team development.

Example:

# ❌ BAD - One massive composition with everything
environments/prod/
  main.tf  # 2000 lines, manages VPC, EC2, RDS, S3, IAM, everything
  # Takes 10+ minutes to plan
  # One mistake affects entire infrastructure

# ✅ GOOD - Separated by concern
environments/prod/
  networking/     # VPC, subnets, route tables
  compute/        # EC2, ASG, ALB
  data/           # RDS, ElastiCache
  storage/        # S3, EFS
  iam/            # IAM roles, policies
2. Always Use Remote State
  • ❌ local terraform.tfstate — no locking, no backup, no team access

  • ✅ remote backend — locking, versioning, encryption, audit log

terraform {
  backend "s3" {
    bucket       = "my-terraform-state"
    key          = "prod/networking/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true   # Terraform 1.10+; native S3 locking
    # Pre-1.10 runtime: use dynamodb_table = "terraform-locks" instead
  }
}
3. Use terraform_remote_state Sparingly — Only at True Ownership Boundaries

Pattern: Connect separately-owned compositions via remote state data sources. Reserve it for genuine team/lifecycle boundaries, not as convenient glue inside a single-team stack.

Use it when ALL of these are true:

  • Consumer and producer are owned by different teams or have different release cadences

  • The producer's state is already split for lifecycle reasons (networking vs. compute vs. data)

  • You cannot reasonably pass the same values as module inputs

Do NOT use it when:

  • You control both stacks and can wire via module outputs

  • You're reading values that would be better served by a cloud data source (e.g., aws_vpc by tag)

  • You're reaching across >2 remote states in one composition — that is a signal to reshape boundaries, not add more wiring

Common LLM mistakes:

  • reaches for terraform_remote_state as default integration pattern

  • chains many terraform_remote_state reads, creating hidden cross-stack coupling

  • reads values that can drift at the provider level (use cloud data sources instead)

At real boundaries, outputs from one stack become typed inputs to another — teams release independently without shared mutable state.

Example:

# environments/prod/networking/outputs.tf
output "vpc_id" {
  description = "ID of the production VPC"
  value       = aws_vpc.this.id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

# environments/prod/compute/main.tf
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "prod/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

module "ec2" {
  source = "../../modules/ec2"

  vpc_id     = data.terraform_remote_state.networking.outputs.vpc_id
  subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
  • ✅ document which outputs are consumed externally; version outputs, never break downstream consumers silently

  • ✅ prefer cloud data sources (aws_vpc by tag) over terraform_remote_state for provider-managed resources

4. Keep Resource Modules Simple

Principles:

  • Don't hardcode values

  • Use variables for all configurable parameters

  • Use data sources for external dependencies

  • Focus on single responsibility

Example:

# ❌ BAD - Hardcoded values in resource module
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # Hardcoded
  instance_type = "t3.large"               # Hardcoded
  subnet_id     = "subnet-12345678"        # Hardcoded

  tags = {
    Environment = "production"             # Hardcoded
  }
}

# ✅ GOOD - Parameterized resource module
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = var.ami_id != "" ? var.ami_id : data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id

  tags = var.tags
}
Cross-cloud resource map
ResourceAWSAzureGCP
Networkaws_vpcazurerm_virtual_networkgoogle_compute_network
Subnetaws_subnetazurerm_subnetgoogle_compute_subnetwork
Compute instanceaws_instanceazurerm_linux_virtual_machinegoogle_compute_instance
Managed relational DBaws_db_instance / aws_rds_clusterazurerm_*_flexible_servergoogle_sql_database_instance
Object storageaws_s3_bucketazurerm_storage_account + azurerm_storage_containergoogle_storage_bucket
5. Composition Layer: Environment-Specific Values Only

Pattern: Compositions provide concrete values, modules provide abstractions

# ✅ GOOD - Composition with environment-specific values
# environments/prod/main.tf

module "vpc" {
  source = "../../modules/vpc"

  cidr_block           = "10.0.0.0/16"
  availability_zones   = ["us-east-1a", "us-east-1b", "us-east-1c"]
  enable_nat_gateway   = true
  single_nat_gateway   = false  # HA for production

  tags = {
    Environment = "production"
    ManagedBy   = "Terraform"
    CostCenter  = "engineering"
  }
}

module "rds" {
  source = "../../modules/rds"

  instance_class       = "db.r5.xlarge"  # Production sizing
  allocated_storage    = 500             # Production sizing
  multi_az             = true            # HA for production
  backup_retention     = 30              # Long retention for prod

  vpc_id               = module.vpc.vpc_id
  subnet_ids           = module.vpc.private_subnet_ids

  tags = {
    Environment = "production"
  }
}

Module Structure

Standard Layout
my-module/
├── README.md                # Usage documentation
├── LICENSE                  # MIT or Apache 2.0 (for public modules)
├── .pre-commit-config.yaml  # Pre-commit hooks configuration
├── main.tf                  # Primary resources
├── variables.tf             # Input variables with descriptions
├── outputs.tf               # Output values
├── versions.tf              # Provider version constraints
├── examples/
│   ├── simple/              # Minimal working example
│   └── complete/            # Full-featured example
└── tests/                   # Test files
    └── module_test.tftest.hcl  # Or .go
File Role
  • README.md — module purpose, first file users see

  • LICENSE — legal terms for public modules (MIT or Apache 2.0)

  • .pre-commit-config.yaml — automated validation before commits

  • main.tf — primary resources, keep focused

  • variables.tf — all inputs, with descriptions

  • outputs.tf — all outputs, with descriptions

  • versions.tf — pinned provider versions

  • examples/ — docs + test fixtures

  • tests/ — automated tests

License Files
  • ✅ Public modules / open-source projects — include LICENSE (MIT = permissive; Apache 2.0 = permissive + patent grant)

  • ❌ Private internal modules / environment-specific configs — optional

  • ❌ Do NOT store LICENSE templates in this skill; generate them on demand from user preference

Terraform vs OpenTofu Preference

HCL is identical; choice affects commands, README, CI invocations, binary references only. Ask before generating if not specified.

Inference signals (when a project already exists):

  • required_version constraint or comments pinning the runtime

  • CI pipelines invoking terraform vs tofu explicitly

  • .terraform.lock.hcl provenance (check commit history / init script)

  • ❌ .terraform/ working directory — both runtimes share it, not a differentiator

If signals are mixed, ask the user rather than guessing, or show both command variants in docs.

Document the chosen runtime in the module README:

## Requirements

| Name | Version |
|------|---------|
| [terraform/tofu] | >= 1.7.0 |
| aws | ~> 5.0 |

Variable Best Practices

Complete Example
variable "instance_type" {
  description = "EC2 instance type for the application server"
  type        = string
  default     = "t3.micro"

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

variable "tags" {
  description = "Tags to apply to all resources"
  type        = map(string)
  default     = {}
}

variable "enable_monitoring" {
  description = "Enable CloudWatch detailed monitoring"
  type        = bool
  default     = true
}
Key Principles
  • ✅ Always include description - Helps users understand the variable

  • ✅ Use explicit type constraints - Catches errors early

  • ✅ Provide sensible default values - Where appropriate

  • ✅ Add validation blocks - For complex constraints

  • ✅ Use sensitive = true - For secrets (Terraform 0.14+)

Variable Naming
# ✅ Good: Context-specific
var.vpc_cidr_block          # Not just "cidr"
var.database_instance_class # Not just "instance_class"
var.application_port        # Not just "port"

# ❌ Bad: Generic names
var.name
var.type
var.value
Provider Requirements and Alias Passing
  • ✅ Child module declares aliased providers: configuration_aliases = [aws.primary, aws.replica]

  • ✅ Caller passes them explicitly: providers = { aws.primary = aws.<caller-alias> } on the module block

  • ❌ Default provider inheritance applies ONLY to a single unaliased provider — never for aliases

Child module — declare aliases in versions.tf, bind per resource:

# modules/replicated-s3/versions.tf
terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = "~> 5.0"
      configuration_aliases = [aws.primary, aws.replica]
    }
  }
}

# in any resource:
provider = aws.primary

Caller — pass the providers map on the module block:

module "bucket" {
  source      = "./modules/replicated-s3"
  bucket_name = "app-data"

  providers = {
    aws.primary = aws.us_east_1
    aws.replica = aws.eu_west_1
  }
}

❌ DON'T — missing providers map on the module call:

module "bucket" {
  source      = "./modules/replicated-s3"
  bucket_name = "app-data"
  # MISSING: providers = { aws.primary = ..., aws.replica = ... }
  # Plan fails: "No configuration for provider aws.primary"
}

Output Best Practices

Complete Example
output "instance_id" {
  description = "ID of the created EC2 instance"
  value       = aws_instance.this.id
}

output "instance_arn" {
  description = "ARN of the created EC2 instance"
  value       = aws_instance.this.arn
}

output "private_ip" {
  description = "Private IP address of the instance"
  value       = aws_instance.this.private_ip
  sensitive   = false  # Explicitly document sensitivity
}

output "connection_info" {
  description = "Connection information for the instance"
  value = {
    id         = aws_instance.this.id
    private_ip = aws_instance.this.private_ip
    public_dns = aws_instance.this.public_dns
  }
}
Key Principles
  • ✅ Always include description - Explain what the output is for

  • ✅ Mark sensitive outputs - Use sensitive = true

  • ✅ Return objects for related values - Groups logically related data

  • ✅ Document intended use - What should consumers do with this?


Common Patterns

Iteration: for_each vs count

Use for_each with stable keys whenever a collection has meaningful identity — removing or reordering an element leaves unrelated addresses untouched. Reserve count for optional singletons (0 or 1) and cases where keys cannot be known at plan time.

For the decision matrix, migration playbook, and known-at-plan failure patterns, see Code Patterns: count vs for_each.

✅ DO: Separate Root Module from Reusable Modules
# Root module (environment-specific)
prod/
  main.tf          # Calls modules with prod-specific values
  variables.tf     # Environment-specific variables

# Reusable module
modules/webapp/
  main.tf          # Generic, parameterized resources
  variables.tf     # Configurable inputs

Root modules are environment-specific; reusable modules are generic.

✅ DO: Use Locals for Computed Values
locals {
  common_tags = merge(
    var.tags,
    {
      Environment = var.environment
      ManagedBy   = "Terraform"
    }
  )

  instance_name = "${var.project}-${var.environment}-instance"
}

resource "aws_instance" "app" {
  tags = local.common_tags
  # ...
}
✅ DO: Version Your Modules
# In consuming code
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # Pin to major version

  # module inputs...
}

Prevents unexpected breaking changes from upstream major bumps.


Anti-patterns to Avoid

❌ DON'T: Hard-code Environment-Specific Values
# Bad: Module is locked to production
resource "aws_instance" "app" {
  instance_type = "m5.large"  # Should be variable
  tags = {
    Environment = "production" # Should be variable
  }
}

Fix: Make everything configurable:

resource "aws_instance" "app" {
  instance_type = var.instance_type
  tags          = var.tags
}
❌ DON'T: Create God Modules
# Bad: One module does everything
module "everything" {
  source = "./modules/app-infrastructure"

  # Creates VPC, EC2, RDS, S3, IAM, CloudWatch, etc.
}

Problem: Hard to test, hard to reuse, hard to maintain.

Fix: Break into focused modules:

module "networking" {
  source = "./modules/vpc"
}

module "compute" {
  source = "./modules/ec2"
  vpc_id = module.networking.vpc_id
}

module "database" {
  source = "./modules/rds"
  vpc_id = module.networking.vpc_id
}
❌ DON'T: Use count or for_each in Root Modules for Different Environments
# Bad: All environments in one root module
resource "aws_instance" "app" {
  for_each = toset(["dev", "staging", "prod"])

  instance_type = each.key == "prod" ? "m5.large" : "t3.micro"
}

Problem: Can't have separate state files, blast radius is huge.

Fix: Use separate root modules:

environments/
  dev/
    main.tf
  staging/
    main.tf
  prod/
    main.tf
❌ DON'T: Use terraform_remote_state Everywhere

Use module outputs when possible. Reserve remote state for ownership boundaries between teams. See Use terraform_remote_state Sparingly for the full rule set.


Module Naming Conventions

Public Modules

Follow the Terraform Registry convention:

terraform-<PROVIDER>-<NAME>

Examples:
terraform-aws-vpc
terraform-aws-eks
terraform-google-network
Private Modules

Use organization-specific prefixes:

<ORG>-terraform-<PROVIDER>-<NAME>

Examples:
acme-terraform-aws-vpc
acme-terraform-aws-rds

Module Release Checklist

Before publishing or handing off a reusable module:

  • [ ] Runtime and provider choice explicit (Terraform vs OpenTofu, version floor in required_version)

  • [ ] Public vs private scope decided (affects naming + license)

  • [ ] examples/ directory with at least minimal and complete

  • [ ] Tests written (native terraform test on 1.6+, or Terratest) — see testing-frameworks.md

  • [ ] README documents all inputs/outputs (Description → Usage → Inputs → Outputs → Requirements)

  • [ ] Module source pinned with version in consumer code

  • [ ] pre-commit-terraform hooks configured (terraform_fmt, terraform_validate, terraform_tflint, terraform_docs), pinned to a specific rev

  • [ ] LICENSE present for public modules (MIT or Apache-2.0)

  • [ ] .gitignore excludes .terraform/, *.tfstate*, *.tfvars, override files, and editor artifacts


Module Testing — Pointer

Module testing (what to test, tiered layers, mocking, idempotency, cost control, strategy by module type) is canonical in Testing Frameworks. Module-specific rules that belong with the module contract:

  • Every reusable module must exercise its validation blocks in tests — reject cases are as important as happy paths.

  • Tier tests by module role: resource modules → input validation + attribute assertions; infrastructure modules → composition + cross-module wiring; compositions → smoke-plan + production-like values + remote-state connectivity.

  • Mock providers (1.7+) for unit tests; reserve real cloud runs for main-branch or scheduled jobs.


LLM Mistake Checklist — Modules

Common model mistakes to correct when generating or reviewing modules:

  • bundles unrelated resources into one "god module" instead of splitting by single responsibility

  • hardcodes environment-specific values (instance_type = "m5.large", Environment = "production") inside a reusable module

  • accepts untyped map(any) / any for core module inputs instead of typed objects with optional() defaults

  • exposes entire provider or resource objects as outputs, leaking the whole contract instead of a stable subset

  • omits description on inputs and outputs, forcing consumers to read the implementation

  • uses this for multiple resources of the same type — reserve this for genuine singletons only

  • reaches for terraform_remote_state inside a single team's stack instead of wiring via module outputs

  • floats module sources (no version pin) in consumer code

  • pushes environment-specific policy (prod-only allowlists, region pins) into primitive/resource modules where it cannot be overridden

  • omits configuration_aliases in a multi-provider child module's required_providers — callers cannot pass aliased providers

  • drops the providers = { aws = aws.region } map from the module call on multi-region or multi-account deploys — resources land on the default provider


Back to: Main Skill File

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Medium risk

  • Instructions that use the network
  • Downloads piped into a shell

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Anton Babenko
Publisher
Anton Babenko (Community)
Source
antonbabenko/terraform-skill · skills/terraform-skill · commit 9c1e92d
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Anton Babenko.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download terraform-skill.zip