src/skills/infra-iac-terraform/SKILL.md
Infrastructure as Code with HashiCorp Terraform
npx skillsauth add agents-inc/skills infra-iac-terraformInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Declarative infrastructure using HCL. Pin provider versions in
required_providersand commit.terraform.lock.hcl. Use remote backends with state locking for team collaboration. Preferfor_eachovercountfor non-identical resources. Usemovedblocks for refactoring,importblocks for adopting existing infrastructure. Validate inputs withvalidationblocks and infrastructure withprecondition/postcondition. Keep modules flat, composable, and single-purpose. Runterraform fmtandterraform validatebefore every commit.
<critical_requirements>
All code must follow project conventions in CLAUDE.md
(You MUST pin provider versions with constraints in required_providers and commit .terraform.lock.hcl to version control)
(You MUST use a remote backend with state locking for any shared or production infrastructure)
(You MUST use for_each with a map/set for non-identical resources -- count causes index-shift destruction on removal)
(You MUST never store secrets in .tf files, .tfvars, or state -- use environment variables (TF_VAR_*) or your secrets manager)
(You MUST run terraform plan and review the diff before every terraform apply -- never apply blindly)
</critical_requirements>
Detailed Resources:
Auto-detection: Terraform, OpenTofu, HCL, .tf files, terraform init, terraform plan, terraform apply, terraform fmt, terraform validate, required_providers, terraform block, resource block, data source, module block, variable block, output block, locals, backend configuration, remote state, state locking, moved block, import block, for_each, count, dynamic block, lifecycle, precondition, postcondition, .terraform.lock.hcl, tfvars, provider configuration
When to use:
.tf)moved, import, and removed blocksWhen NOT to use:
Key patterns covered:
for_each, count, depends_on, lifecycle)moved, import, and removed blocksprecondition, postcondition, check blocks)Terraform is a declarative infrastructure-as-code tool. You describe the desired end-state; Terraform determines the steps to reach it. The HCL configuration language is designed to be human-readable and machine-parseable.
Core principles:
OpenTofu compatibility: OpenTofu is an open-source fork (MPL 2.0) that is syntax-compatible with Terraform 1.5.x. The patterns in this skill apply to both tools. OpenTofu uses .tofu file extensions for OpenTofu-only features and adds native state encryption.
Pin Terraform version and all provider versions. Commit .terraform.lock.hcl to version control.
# terraform.tf
terraform {
required_version = ">= 1.9.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Allows 5.x, blocks 6.0
}
}
}
Why this matters: Without version constraints, terraform init on different machines downloads different provider versions, causing inconsistent plans and mysterious drift. The lock file pins exact versions and cryptographic hashes.
Version constraint syntax: = 1.0.0 (exact), >= 1.0.0 (minimum), ~> 1.0 (allows 1.x, blocks 2.0), >= 1.0, < 2.0 (range).
See examples/core.md for full provider configuration with aliases and default tags.
Resources follow a standard argument ordering: meta-arguments first, resource arguments next, nested blocks after, lifecycle last.
resource "aws_instance" "web" {
count = var.instance_count # Meta-argument first
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = "web-${count.index}"
}
lifecycle { # Lifecycle block last
create_before_destroy = true
}
}
Key meta-arguments: count, for_each, depends_on, provider, lifecycle. Place meta-arguments at the top, separated from resource arguments by a blank line.
See examples/core.md for argument ordering and naming conventions.
Use for_each with a map or set for non-identical resources. count uses numeric indices -- removing an item from the middle shifts all subsequent indices, causing unnecessary destruction and recreation.
# for_each with a map -- stable keys, safe removal
resource "aws_iam_user" "team" {
for_each = toset(var.team_members) # ["alice", "bob", "carol"]
name = each.value
}
# Removing "bob" only destroys bob's user -- alice and carol are untouched
# count -- index-based, dangerous on removal
resource "aws_iam_user" "team" {
count = length(var.team_members)
name = var.team_members[count.index]
}
# Removing "bob" (index 1) shifts carol from index 2 to 1 -- carol gets destroyed and recreated
When count is acceptable: Identical resources where the only difference is the count (e.g., count = var.enable_feature ? 1 : 0 for conditional creation).
See examples/patterns.md for for_each with maps, sets, and conditional patterns.
Every variable needs type, description, and validation where constraints exist. Use named locals for derived values.
variable "environment" {
type = string
description = "Deployment environment (dev, staging, production)"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "instance_type" {
type = string
description = "EC2 instance type for the application server"
default = "t3.micro"
}
Key rules: Always set type and description. Use validation blocks to catch invalid input at plan time. Never use default for secrets -- force the caller to provide them.
See examples/core.md for complex variable types, sensitive variables, and output definitions.
Never use local state for shared infrastructure. Remote backends provide locking, versioning, and team collaboration.
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # State locking
}
}
Critical: Backend configuration cannot use variables or locals -- values must be literal or passed via -backend-config flags during terraform init. Use partial configuration for dynamic values.
State file hierarchy: Organize state keys by environment and layer (e.g., prod/network/, prod/compute/, staging/network/) to minimize blast radius.
See examples/state.md for backend configuration, partial config, and state organization.
Modules are the unit of reuse. Keep modules flat, single-purpose, and composable.
modules/
vpc/
main.tf # Resources
variables.tf # Inputs
outputs.tf # Outputs
README.md # Usage docs (makes it public-facing)
compute/
main.tf
variables.tf
outputs.tf
# Root module calling child modules
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = var.environment
}
module "compute" {
source = "./modules/compute"
subnet_ids = module.vpc.private_subnet_ids
environment = var.environment
}
Key principle: Keep the module tree flat. Deeply nested modules (module calling module calling module) are hard to debug and reuse. Prefer composition at the root level.
See examples/modules.md for module versioning, registry sources, and internal vs published modules.
Refactor infrastructure without destroying resources.
# Rename a resource -- state updated, infrastructure untouched
moved {
from = aws_instance.web_server
to = aws_instance.app_server
}
# Adopt existing infrastructure into Terraform management
import {
to = aws_s3_bucket.logs
id = "my-existing-bucket-name"
}
# Remove from Terraform management without destroying the resource
removed {
from = aws_instance.legacy
lifecycle {
destroy = false
}
}
Always run terraform plan after adding these blocks to verify Terraform interprets the refactoring correctly. Look for "move" and "import" messages in the plan output.
See examples/state.md for module refactoring with moved blocks and bulk imports.
Control how Terraform manages resource lifecycle.
resource "aws_db_instance" "main" {
# ... configuration ...
lifecycle {
prevent_destroy = true # Block accidental deletion
create_before_destroy = true # Zero-downtime replacement
ignore_changes = [tags] # External process manages tags
}
}
When to use each:
prevent_destroy -- databases, DNS zones, state buckets (critical resources)create_before_destroy -- load balancers, instances behind ASGs (zero-downtime)ignore_changes -- auto-scaling managed attributes, externally tagged resourcesreplace_triggered_by -- force replacement when a dependency changes that Terraform does not detectSee examples/patterns.md for lifecycle combinations and replace_triggered_by.
Validate assumptions before provisioning and guarantees after.
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
lifecycle {
precondition {
condition = data.aws_ami.ubuntu.architecture == "x86_64"
error_message = "AMI must be x86_64 architecture."
}
postcondition {
condition = self.public_ip != ""
error_message = "Instance must have a public IP assigned."
}
}
}
Precondition vs postcondition vs check:
precondition -- validates assumptions before creation (blocks plan)postcondition -- validates guarantees after creation (blocks apply)check block -- validates infrastructure state without blocking operations (warnings only)See examples/patterns.md for check blocks and variable validation patterns.
Generate repeated nested blocks from collections. Use sparingly -- overuse hurts readability.
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
When to use: Reusable modules where the number of nested blocks varies per caller. When not to use: Write nested blocks literally when the set is small and fixed. Dynamic blocks cannot generate meta-argument blocks (lifecycle, provisioner).
See examples/patterns.md for dynamic block patterns with conditionals.
</patterns><red_flags>
High Priority:
.terraform.lock.hcl in version control -- different team members get different provider versions, causing plan drift and mysterious failures.tf or .tfvars files -- committed to version control, visible in state file; use TF_VAR_* environment variables or your secrets managercount for non-identical resources -- removing an item shifts indices, destroying and recreating unrelated resourcesterraform apply without reviewing the plan -- auto-approve in production is how you delete databasesversion = ">= 5.0" without an upper bound allows major version upgrades that break everythingMedium Priority:
depends_on when an expression reference suffices -- depends_on causes overly conservative plans; let Terraform infer dependencies from expressionsignore_changes = all -- Terraform will never update the resource again, even for intentional changes; be specific about which attributes to ignoredescription on variables and outputs -- undocumented inputs/outputs make modules unusable for anyone but the authorGotchas & Edge Cases:
-backend-config during terraform initprevent_destroy does not prevent destruction if you remove the resource block entirely -- it only prevents terraform destroy on the resource while the block existsfor_each keys must be known at plan time -- they cannot reference resource attributes that are computed during applymoved blocks are processed once during terraform plan/apply -- remove them after the migration is applied to keep configuration cleanterraform state subcommands (mv, rm, pull, push) bypass safety checks -- use moved/removed blocks instead for auditable, reviewable refactoringdata sources are read during planning by default -- if they depend on resources being created in the same apply, use depends_on to defer the readsensitive = true on variables prevents the value from appearing in plan output but does NOT encrypt it in state -- state encryption is a separate concernterraform fmt only formats .tf files in the current directory -- use terraform fmt -recursive to format all subdirectoriestoset() deduplicates -- if your list has duplicates, for_each = toset(var.list) silently drops them.tfvars files are auto-loaded only if named terraform.tfvars or *.auto.tfvars -- other filenames require explicit -var-file flag</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST pin provider versions with constraints in required_providers and commit .terraform.lock.hcl to version control)
(You MUST use a remote backend with state locking for any shared or production infrastructure)
(You MUST use for_each with a map/set for non-identical resources -- count causes index-shift destruction on removal)
(You MUST never store secrets in .tf files, .tfvars, or state -- use environment variables (TF_VAR_*) or your secrets manager)
(You MUST run terraform plan and review the diff before every terraform apply -- never apply blindly)
Failure to follow these rules will cause state corruption, accidental resource destruction, secret exposure, and non-reproducible infrastructure.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events