Terraform expert
Writes safe, modular, idiomatic Terraform - remote state layout, module structure, plan-review discipline, version pinning, and blast-radius control. Use when someone asks "structure my Terraform repo", "how should I split state between environments", "terraform plan wants to destroy and recreate my database", "should I use count or for_each", or "how do I import existing infrastructure". Do NOT use for CI pipeline authoring in GitHub Actions - use github-actions instead; for Kubernetes manifests the infrastructure hosts, use kubernetes-basics; for managing app secrets outside IaC, use secrets-hygiene.From its SKILL.md
npx -y skills add SkillMedev/skills --skill terraform-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.8 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it
Terraform Expert
The costly failure mode in Terraform is not syntax - it is an unreviewed apply that destroys a production database because a rename read as destroy-and-recreate, or two engineers corrupting shared state because it lived on a laptop. This skill structures Terraform so every change is a reviewed plan with a known blast radius, and state is a locked, encrypted, per-environment artifact.
Operating procedure
State comes first because everything else is recoverable from code; state is not.
Step 1: Gather inputs
- Cloud and providers in play, and whether infrastructure already exists (import path) or is greenfield.
- Environments - dev/staging/prod at minimum? Any per-region splits?
- Team size and apply model - local applies, CI-driven, or Terraform Cloud? Default for more than one engineer: CI-driven with plan output posted to the PR.
- Criticality tiers - which resources are irreplaceable (databases, DNS zones, KMS keys)? These get
prevent_destroy.
Step 2: Set up state before writing resources
- Remote backend always - S3 + DynamoDB locking, GCS, or Terraform Cloud. Local state for shared infrastructure guarantees eventual corruption or loss.
- One state file per environment. Never share state across environments: a bad apply in dev must be physically unable to touch prod.
- State stores secrets and all attribute values in plaintext - encrypt the backend at rest and restrict read access as tightly as the secrets it holds.
- Never edit state by hand. Refactors use
terraform state mv; adopting existing infrastructure usesimport(or import blocks). A hand-edited state file diverges from reality silently.
Step 3: Lay out the repository
modules/ # reusable building blocks, no env-specific values
vpc/
main.tf # resources
variables.tf # typed inputs with validation
outputs.tf # what consumers may depend on
ecs-service/
...
environments/ # thin per-env roots - wiring only
dev/
main.tf # module calls + env values
backend.tf # this env's state config
terraform.tfvars
staging/
prod/
Rules the skeleton encodes:
- Environments are thin: they call modules and pass variables. If an environment root contains a raw
resourceblock, that resource belongs in a module. - A module hardcodes zero environment values; everything env-specific arrives via
variables.tf. - Modules expose only deliberate
outputs.tf- consumers depending on undeclared internals break on refactor.
Step 4: Write idiomatic resources
variable "instance_count" {
type = number
default = 2
validation {
condition = var.instance_count > 0
error_message = "instance_count must be positive."
}
}
resource "aws_instance" "app" {
for_each = var.instances # stable keys, not count indexes
instance_type = each.value.type
tags = merge(local.common_tags, { Name = "app-${each.key}" })
}
for_eachovercountwhenever items have stable identities. Withcount, deleting item 0 renumbers every following item and Terraform destroys and recreates all of them.- Tag everything through one
common_tagslocal - untagged resources are unattributable in the bill and invisible to cleanup. - Mark sensitive outputs
sensitive = true. - Secrets come from a secrets manager via data sources; never hardcode them (they land in state and git either way, but hardcoding puts them in both).
Step 5: Pin versions and enforce plan review
- Pin providers and modules with
~>constraints and commit.terraform.lock.hcl. An unpinned provider means every teammate and CI run can resolve different code. - Every apply is preceded by a
terraform planreviewed by a human - in a PR for teams. The review question is always: does the plan contain anydestroyorreplaceyou did not intend? lifecycle { prevent_destroy = true }on every Step 1 irreplaceable resource. It turns a catastrophic apply into an explicit error.
Step 6: Operate - upgrades, drift, blast radius
- Provider upgrades: one minor version at a time, read the upgrade guide, plan in dev first. A plan full of forced replacements after a bump means stop and read, not apply.
- Drift: run plan on a schedule (daily in CI is a reasonable default); reconcile manual console changes back into code or revert them. Unreconciled drift compounds until plans become unreadable.
- Blast radius: when a single state's plan routinely exceeds a few hundred resources or takes many minutes, split it into independently-applied stacks (networking / data / compute) connected by remote state outputs or data sources. Smaller states mean smaller worst-case applies.
Worked contrast
Bad - environment root that will drift from staging within a month:
# environments/prod/main.tf
resource "aws_instance" "app" { # raw resource in an env root
count = 3 # count: delete one, recreate the rest
ami = "ami-0abc123" # hardcoded, differs from staging by accident
instance_type = "m5.large"
}
Good - the same intent through the module contract:
# environments/prod/main.tf
module "app" {
source = "../../modules/app-service"
instances = { a = { type = "m5.large" }, b = { type = "m5.large" }, c = { type = "m5.large" } }
env = "prod"
}
Prod and staging now differ only in the values they pass, and the diff between them is reviewable in one screen.
Deliverable
Produce a Terraform setup containing: the remote backend configuration with locking and encryption per environment; the modules/ + environments/ skeleton above populated for the actual stack; pinned provider constraints and a committed lockfile; prevent_destroy on every named irreplaceable resource; and a documented plan-review workflow (who reviews, where plan output lands).
Do NOT
- Do not keep shared state locally or share one state file across environments - the failure is corruption or cross-env damage, and both are unrecoverable by git.
- Do not apply without a reviewed plan; the plan is the only thing standing between a rename and a destroyed database.
- Do not use
countfor collections with identity - removals renumber and recreate everything after them. - Do not hand-edit state; use
state mvandimport. - Do not leave providers unpinned or the lockfile uncommitted.
- Do not put raw resources in environment roots - that is how prod and staging quietly diverge.
Quality bar
terraform validateand a clean review ofplanpass in every environment.- No environment root contains a
resourceblock; no module contains an environment-specific literal. - Every irreplaceable resource carries
prevent_destroy; a test plan proves a rename does not schedule a replace on them. - Backend is remote, locked, encrypted; states are one-per-env.
- All providers pinned with
~>, lockfile committed.
Escalation
Route CI pipeline mechanics (plan-on-PR, apply-on-merge jobs) to github-actions, workload manifests to kubernetes-basics, and secret storage policy to secrets-hygiene. For compliance-audited infrastructure (SOC 2 change-management evidence), pair with soc2-evidence-helper.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.