From ct
Deep Terraform/OpenTofu operational intuition — state-file mechanics and locking, refactoring blocks (import/moved/removed), lifecycle gotchas, count-vs-for_each migration, backend migration, post-1.5.5 fork divergence. Load for state surgery, refactoring blocks, lifecycle interactions, backend migration, or TF vs OpenTofu choices. Skip for ordinary resource authoring, basic HCL, "what is Terraform", or first-time provider setup. Triggers on: "terraform state mv", "import block", "moved block", "removed block", "create_before_destroy", "force-unlock", "init -migrate-state", "OpenTofu vs Terraform", "tofu fork".
How this skill is triggered — by the user, by Claude, or both
Slash command
/ct:terraform-opentofuThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Concise operational pointers for state surgery, refactoring without recreate, lifecycle pitfalls, and the post-fork TF↔OTF feature divergence.
Concise operational pointers for state surgery, refactoring without recreate, lifecycle pitfalls, and the post-fork TF↔OTF feature divergence.
Assumes you already know HCL syntax, what providers and modules are, and how plan / apply work. This skill covers the operational layer — the parts models tend to gloss over: state mechanics, locking, declarative refactor blocks, count/for_each migration mechanics, and the fork landscape since August 2023.
Load when the question is about:
terraform state mv|rm|pull|push, manual JSON edits)import {} (TF 1.5+), moved {} (TF 1.1+), removed {} (TF 1.7+ / OTF 1.7+)prevent_destroy, ignore_changes, create_before_destroy, replace_triggered_by, pre/postconditionscount ↔ for_each migrations and index-shift recreate cascadesinit -migrate-state / -reconfigure), state locking (DynamoDB → S3 native).terraform.lock.hcl) and version pinning-exclude, ephemeral resourcesterraform_remote_state security implicationsDo NOT load for: writing your first resource block, choosing a provider, basic variable/output authoring, or "how do I install Terraform" — those don't need this skill.
tofu mirrors terraform. .tf files are unmodified. Existing state files load in either.pbkdf2, aws_kms, gcp_kms, azure_keyvault, openbao. Configured via terraform { encryption {} } block or TF_ENCRYPTION env. Includes fallback {} for migration from unencrypted state.backend, module source, and encryption blocks during init (must be statically determinable — no resource/data refs).for_each (OTF 1.9): dynamic provider iteration; each iterated provider must have an alias. Depends on early evaluation.-exclude flag (OTF 1.9): inverse of -target; applies everything except listed addresses.enabled lifecycle meta-arg (OTF 1.11): alternative to count = var.flag ? 1 : 0 for zero-or-one resources.removed {} block ships in both TF 1.7 and OTF 1.7. import {} block (TF 1.5) works identically in OTF.required_providers HCL is identical. Some HashiCorp-published providers may carry BUSL constraints.terraform.tfstate is JSON. Contains plaintext secrets for resource attributes (DB passwords, generated keys, random_password.result). sensitive = true only hides from CLI output — does NOT encrypt in state.pg, http (GitLab), kubernetes.LockID. The name is hardcoded; not configurable.use_lockfile = true on the S3 backend uses S3 conditional writes to create a .tflock object alongside state. DynamoDB args (dynamodb_table, etc.) deprecated 1.11; both can be configured simultaneously during transition. Requires s3:DeleteObject permission to clear the lock after run.terraform force-unlock LOCK_ID — only after physically verifying no apply is running. Force-unlock during a live apply will corrupt state. The lock ID appears in the error message of the blocked command.encrypt = true + KMS for at-rest. For client-side end-to-end use OpenTofu 1.7+ encryption {} block.Prefer declarative refactor blocks (moved, removed, import) over CLI state subcommands. The CLI commands remain for ad-hoc fixes:
terraform state list — enumerate addresses.terraform state show ADDRESS — inspect attributes for one resource.terraform state mv SRC DST — rename in state (now superseded by moved {} for code-reviewed changes).terraform state rm ADDRESS — drop from state without destroying real infra (now superseded by removed { lifecycle { destroy = false } }).terraform state pull > state.json then terraform state push state.json — manual JSON surgery. Always copy state.json before editing; a corrupt push can be unrecoverable. Increment the serial field on push.terraform refresh is deprecated as a standalone command; use terraform plan -refresh-only (TF 0.15.4+) which produces a reviewable plan before mutating state. Auto-applying a refresh with bad credentials can mark all resources as deleted — never run unattended in CI.moved {} (TF 1.1+, OTF 1.6+): rename without recreate.
moved { from = aws_instance.web; to = aws_instance.frontend }
Use cases: rename, wrap into nested module, split a module out, count→for_each migration (one block per index→key mapping). Multiple moved chains follow transitively. Safe to leave indefinitely; remove after all environments have applied at least once.import {} (TF 1.5, Jun 2023): declarative replacement for terraform import CLI.
import { to = aws_instance.example; id = "i-1234567890abcdef0" }
Run terraform plan -generate-config-out=generated.tf to scaffold a resource block. The generated file always needs hand cleanup — defaults are spelled out, dependencies aren't inferred, and provider-specific quirks (e.g., AWS tag merging) come through verbatim. for_each on import is TF 1.7+ for bulk imports.
Import is a plan-time operation, so it shows up in terraform plan output; remove the block after first successful apply, or leave it (it's idempotent — won't re-import).removed {} (TF 1.7, OTF 1.7): remove from state without destroying infra.
removed {
from = aws_s3_bucket.legacy
lifecycle { destroy = false }
}
Without destroy = false the resource is destroyed (equivalent to deleting the resource block normally). With it, real infra survives — used for handoff to another tool, splitting state files, or "stop managing this." Reviewable in PR; leaves audit trail in git history.prevent_destroy = true: only blocks destroy and replace-via-destroy. In-place updates that don't recreate still apply. Removing the entire resource block from config bypasses it (the lifecycle isn't read for removed config). Comment out and apply when an intentional teardown is needed.ignore_changes = [tags, ami] / ignore_changes = all: drift on listed attributes is silently accepted on update. create plans still consider them. Cannot reference data sources or other meta-args. Common abuse: pinning AMI to dodge replacement — preferable to use replace_triggered_by against a version variable.create_before_destroy = true: replacement creates new before destroying old. Propagates automatically to dependents — you cannot set it false on a resource depended on by a create_before_destroy = true resource (would create cycles). Breaks on resources with globally unique names (LBs, RDS identifiers, S3 buckets, security groups): use name_prefix instead of name, or rename first in a separate apply. Destroy-time provisioners are skipped.replace_triggered_by = [aws_db_instance.main.id] (TF 1.2+): force replacement when a referenced attribute changes. Cleaner than null_resource triggers.precondition / postcondition (TF 1.2+): assertion blocks on resources, data sources, and outputs. Postcondition failure prevents downstream resources from running. Use for invariants (e.g., AMI in expected region) not for input validation (use variable validation blocks).count = N: integer-indexed (module.X[0]). Removing a middle item shifts indices — all trailing instances are destroyed-and-recreated. Catastrophic for stateful resources (DBs, EBS volumes).for_each = toset(var.list) / for_each = var.map: keyed by string (module.X["key"]). Removing a key only destroys that one. Default choice for stable identities.count = var.enabled ? 1 : 0: canonical conditional pattern. OpenTofu 1.11+ has lifecycle { enabled = var.flag } as a cleaner alternative.terraform state mv 'aws_instance.c[0]' 'aws_instance.c["small"]' per index.moved {} block per index→key.
moved { from = aws_instance.c[0]; to = aws_instance.c["small"] }
moved { from = aws_instance.c[1]; to = aws_instance.c["tiny"] }
required_providers block in terraform {}. Constraint syntax: ~> 5.0 means >=5.0, <6.0; ~> 5.30 means >=5.30, <5.31; >= 5.0 open-ended; 5.30.0 exact..terraform.lock.hcl: records exact resolved version + SHA-256 checksums of the provider zip per platform. Commit it. Drift between contributors otherwise.terraform init -upgrade: re-resolves within constraints, updates lockfile.terraform providers lock -platform=linux_amd64 -platform=darwin_arm64: pre-populate multi-platform checksums (lock file from one OS only contains that OS's hashes — CI on Linux + dev on macOS will conflict otherwise).source = "hashicorp/aws" + version = "~> 5.0". version is registry-only.source = "git::https://github.com/X/Y.git//path?ref=v1.2.3". Subpath after //. Use ?ref= with a tag, not a branch — ?ref=main re-fetches default branch on every init. Note tags are mutable in git; pin to commit SHA (?ref=abc123def) for true immutability.for_each on the call (TF 0.13+) iterates module instances.terraform workspace new|select|list|delete. Each workspace has separate state in the same backend. terraform.workspace interpolates the name.terraform init — one mistype in workspace select applies dev changes to prod. Stronger isolation: separate working directories with separate backend prefixes (and ideally separate AWS accounts / GCP projects per env). Common production pattern.backend block, then terraform init -migrate-state. Prompts to copy existing state to the new backend; -force-copy skips the prompt for CI.terraform init -reconfigure: discards prior backend state metadata. Use when you genuinely want a fresh backend (and have already moved state out of band, or are starting over).init -migrate-state per hop, applying between each.data "terraform_remote_state" "vpc" {
backend = "s3"
config = { bucket = "...", key = "vpc/terraform.tfstate", region = "..." }
}
tfe_outputs data source which scopes to outputs only.aws_ssm_parameter, aws_secretsmanager_secret) — narrower blast radius.terraform fmt -recursive: canonicalize formatting.terraform validate: syntax + schema check (no API calls).terraform plan -out=tfplan then terraform apply tfplan: idempotent — apply runs exactly the saved plan, no re-evaluation. Required for any CI workflow that gates apply on plan review.terraform plan -refresh=false: skip the API refresh phase. Faster but stale.terraform plan -refresh-only: only reconcile state with reality, no config diff.terraform plan -target=ADDRESS: escape hatch; emits a "partial plan" warning. Not for routine use — leads to undetected drift across the rest of the graph.tofu plan -exclude=ADDRESS: inverse of -target.TF_LOG=DEBUG terraform plan 2> tf.log: verbose log. Levels: ERROR < WARN < INFO < DEBUG < TRACE. TRACE includes raw provider HTTP. Set TF_LOG_PATH=/tmp/tf.log to redirect (TF_LOG_PATH is ignored if TF_LOG is unset). TF_LOG=JSON emits trace-level structured logs. Provider-only logging via TF_LOG_PROVIDER (TF 0.15+).terraform graph | dot -Tpng > graph.png: DOT-format dependency graph for resource ordering.tflint (rules), checkov / tfsec / terrascan (security), terraform-compliance (BDD).terragrunt.hcl per leaf module + a root terragrunt.hcl with remote_state {} (DRY backend config) and generate {} blocks for provider injection.include "root" { path = find_in_parent_folders() } walks up to inherit parent config.dependency "vpc" { config_path = "../vpc" } blocks read another stack's outputs without terraform_remote_state. mock_outputs = {...} lets plan succeed before the dependency has been applied (mock_outputs_allowed_terraform_commands = ["plan"]).terragrunt run --all apply (formerly run-all) walks the dependency DAG. Use --terragrunt-non-interactive and --terragrunt-parallelism N to bound concurrency.--queue-include-dir filters.terraform plan -detailed-exitcode in CI: exit 0 no changes, 1 error, 2 drift detected.driftctl (CloudSkiff): scans cloud APIs and compares to state — catches resources created out-of-band, not just attribute drift. Maintenance has slowed; verify project state before adopting..terraform.lock.hcl not committed → every contributor resolves a different provider patch version on first init → diffs that aren't in any .tf file.count removal from middle of list → trailing resources recreated. Switch to for_each first via moved blocks.create_before_destroy = true on a resource with a globally unique name → second create fails on the duplicate name. Switch to name_prefix first.?ref=main → silent breaking change on every init. Pin tag (?ref=v1.2.3) or commit SHA.prevent_destroy = true blocking a legitimate teardown → comment out, apply, restore. Don't comment out and forget.terraform_remote_state consumers receive read access to the entire upstream state — not just outputs. Audit who can read each state bucket.-refresh-only in CI with broken credentials → silently marks all resources deleted. Always require human review of refresh plans.import {} blocks: harmless idempotently, but reviewers will keep asking. Remove after first apply lands in main.depends_on papering over a missing implicit dependency → fix the reference instead. depends_on is for non-data dependencies (IAM eventual consistency, side-effect ordering).Terraform language docs (developer.hashicorp.com/terraform):
import block and generating configurationmoved blockremoved blocklifecycle meta-argumentterraform_remote_state data sourceHashiCorp blog:
OpenTofu docs (opentofu.org/docs):
for_each, -exclude)Terragrunt (terragrunt.gruntwork.io/docs):
Before recommending a non-trivial state operation (state mv|rm|push, force-unlock, backend migration, removed block):
removed {} requires TF 1.7+ / OTF 1.7+).terraform state pull > backup-$(date +%s).tfstate) before any push/rm/mv.moved / removed / import) over CLI state subcommands when a code-review trail is wanted.State surgery without a backup is a one-way door.
npx claudepluginhub pvillega/claude-templates --plugin ctDiagnoses Terraform/OpenTofu failures (identity churn, secrets, blast radius, CI drift, state corruption) with version-aware guards and a structured response contract.
Guides Terraform/OpenTofu IaC operations: project layout, state management, module design, plan/apply safety, CI/CD pipelines, and secrets handling.
Provides current Terraform/OpenTofu compatibility guidance covering HCL, state, modules, providers, testing, stacks, and CLI changes across versions 1.7–1.15 and 1.7–1.12 respectively.