| name | tf-helper |
| description | Team conventions, module layout, and AWS GovCloud gotchas for our Terraform codebase. Use whenever the user mentions Terraform, tf files, modules, state backend, tagging, provider versions, GovCloud, region endpoints, or general IaC questions — even if they don't explicitly ask for help. Also use when a `.tf` or `.tfvars` file is in the working directory and the user asks "what should I do next" or "is this right". |
| allowed-tools | Read, Grep, Glob |
tf-helper
Our team gets pulled into Terraform questions from other teams multiple
times a week. Same questions, same answers, different people. This skill
captures the answers once so Claude can deliver them in our voice,
against our conventions, without re-litigating the basics.
When to use this skill
- User asks about module structure, naming, or layout
- User asks about our state backend or region setup
- User asks about tags, provider pinning, or module versions
- User mentions GovCloud, STIG, FIPS endpoints, or compliance-adjacent
IaC questions
- A
.tf / .tfvars / terraform.tfstate* file is in view
Module layout
Every reusable module follows this layout:
modules/<name>/
├── main.tf # resources
├── variables.tf # inputs, with descriptions and type constraints
├── outputs.tf # outputs, with descriptions
├── versions.tf # required_providers + required_version
└── README.md # generated with terraform-docs
Why: one module = one logical unit. If you have two resource groups
that could live apart, they're two modules. Flat > clever.
Module invocations live in environments/<env>/<service>/ — never
nested more than two levels. Deep nesting hides dependencies.
Tagging convention
Every taggable resource carries the team defaults via a default_tags
provider block. Don't set tags on each resource; set them once:
provider "aws" {
region = var.region
default_tags {
tags = {
Owner = "team-name"
Environment = var.environment
CostCenter = var.cost_center
Compliance = "IL2" # or IL4 per service
ManagedBy = "terraform"
}
}
}
Service-specific tags go on the resource. Required: Name (human-
readable), Service (which app this belongs to).
Provider version pins
Pin the minor. Never float the major.
terraform {
required_version = "~> 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
Why: Terraform upgrades across majors have broken us twice. The
minor-pin lets us take patches automatically without surprise breaks.
State backend
S3 + DynamoDB lock, one bucket per environment, key per service:
terraform {
backend "s3" {
bucket = "tf-state-<env>-<account-id>"
key = "<service>/terraform.tfstate"
region = "us-gov-west-1"
dynamodb_table = "tf-state-lock-<env>"
encrypt = true
kms_key_id = "alias/tf-state"
}
}
GovCloud gotcha: state bucket region is us-gov-west-1 or
us-gov-east-1. The ARN prefix is arn:aws-us-gov: not arn:aws:.
See references/govcloud-gotchas.md when the user asks anything about
cross-partition resources, endpoints, or ARNs.
Review discipline
Three things a plan review always checks:
- Destroy lines. Any
- in the plan deserves a human. State
moves, replaces, or deletes are never implicit.
- IAM diffs. Policy changes surface as JSON diffs that are easy
to skim past. Read them.
- Provider drift. If
terraform init upgraded a provider, note
the delta between plans run before and after.
Output format for answers
When explaining "how we do X", lead with the convention, show a
minimal code example, explain why the team chose this, and flag
one gotcha per answer if there's a real one. Don't pad — short
beats comprehensive.
Bundled resources
references/govcloud-gotchas.md — GovCloud-specific endpoint, ARN,
and region weirdness. Read when the question touches GovCloud.