Install
openclaw skills install @ivangdavila/terraformWrites, debugs, and refactors Terraform — HCL and modules, plan and apply failures, state surgery, drift, and provider pinning. Use when writing or reviewing HCL, when terraform plan or apply errors out, when a plan shows a permanent diff, an unexplained destroy, or "forces replacement", when renaming, importing, or moving resources between modules and states, when a state lock is stuck or state is lost or corrupted, when for_each fails because a value is not known until apply, when pinning providers or surviving a major version upgrade, or when wiring plan-on-PR, apply-on-merge, drift detection, and policy gates. Covers OpenTofu, tfstate, backends, workspaces, and infrastructure-as-code review. Not for choosing which cloud services to build — see aws, gcp, or azure.
openclaw skills install @ivangdavila/terraformUser preferences and memory live in ~/Clawic/data/terraform/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/terraform/ or ~/clawic/terraform/), move it to ~/Clawic/data/terraform/.
state rm, unintended destroy, interrupted applyaws, gcp, azure) or configuring what runs inside them (→ ansible)| Situation | Play |
|---|---|
| Renamed a resource or module in code | moved block (>=1.1); apply everywhere, delete the block in a later PR → refactoring.md |
| Cloud object exists but is not in state | import block (>=1.5) + plan -generate-config-out=gen.tf; rewrite the draft, merge at zero diff → refactoring.md |
| Stop managing something without destroying it | removed block with destroy = false (>=1.7); state rm only as one-off surgery → refactoring.md |
| Permanent diff on every plan | Find the writer (autoscaler, console, another pipeline, provider normalization) before reaching for ignore_changes → debug.md |
| "Invalid for_each argument" / "Invalid count argument" | Key on values known at plan time, never on resource attributes → expressions.md |
| A variable value is ignored, or an old value keeps winning | Precedence: -var/-var-file > *.auto.tfvars > terraform.tfvars > TF_VAR_ > the declared default → expressions.md |
| "Error: Cycle: ..." | Break the mutual reference into a standalone rule/attachment resource → debug.md |
| "Provider produced inconsistent final plan" | Provider bug: upgrade the provider, then pin and report → debug.md |
| "Saved plan is stale" | State moved between plan and apply; re-plan, re-review, re-apply → ci.md |
| Stuck state lock | Prove the holder is dead, then force-unlock <LOCK_ID> → recovery.md |
| State lost, corrupted, or pushed wrong | Bucket object versions are the only undo; rebuild by import if there are none → recovery.md |
| "Inconsistent dependency lock file" or a checksum error on install | The lock lacks that provider or that platform → providers.md |
| Two regions, two accounts, one config | Provider alias + explicit providers map into modules → providers.md |
| Plan takes minutes | -refresh=false while iterating, then split state by blast radius → performance.md |
| Throttling or "Rate exceeded" during apply | Lower -parallelism (default 10) → performance.md |
| Moving to a remote backend or between backends | Pull a backup, then init -migrate-state → state.md |
| Secret landed in state, a plan file, or a log | Rotate first; state is plaintext and old versions keep it → secrets.md |
| Provider major upgrade (v4 → v5) | Upgrade guide, one canary stack, explained diff per environment → upgrades.md |
| Module design, versioning, or nesting question | Exact pins for third-party, ~> for internal, two levels max → modules.md |
| Resource must never be destroyed, must rotate with another, or keeps diffing on one attribute | lifecycle meta-arguments and what each one costs → lifecycle.md |
A destroy fails or a teardown leaves debris | Deletion protection, retained objects, reverse-order dependencies → lifecycle.md |
| Wiring plan-on-PR and apply-on-merge | OIDC role, saved plan artifact, one concurrency group per state → ci.md |
| Need tests or policy gates | .tftest.hcl (>=1.6), plan-JSON assertions, policy engine → testing.md |
| Need the exact command under pressure | → commands.md |
| Anything else | Smallest possible change; plan -out=tfplan; read the destroy count and every "forces replacement" line before applying |
Depth on demand: debug.md plan/apply symptom→cause chains · state.md backends, locking, layout · refactoring.md moved/import/removed · modules.md design and versioning · expressions.md HCL loops, types, functions · lifecycle.md replacement, protection, ignored drift · providers.md pinning, lock file, aliases · secrets.md sensitive values and state exposure · ci.md pipelines, OIDC, drift detection · testing.md validate, terraform test, policy · upgrades.md CLI and provider majors, OpenTofu · performance.md slow plans and big states · recovery.md after the damage · commands.md incident toolkit.
terraform plan -out=tfplan → review → terraform apply tfplan. A bare apply re-plans against a world that may have changed since you read the diff: you approve one change and execute another.terraform state pull > backup-$(date +%s).tfstate; restore with terraform state push. One wrong state rm orphans a live resource that keeps running and billing with nothing tracking it.2 to add, 0 to change, 2 to destroy can be two replacements rather than two creations plus two deletions. Machine gate: terraform show -json tfplan | jq '[.resource_changes[] | select(.change.actions | index("delete"))] | length' — compare against destroy_gate.required_providers plus a committed .terraform.lock.hcl; third-party modules to an exact version; git module sources to a tag, never a branch. Unpinned means CI breaks on someone else's release day.for_each on stable, human-chosen strings (environment names, logical roles) — never IDs, list indices, or computed values. Changing a key is destroy + create of the real object; count indices renumber, so removing item 0 shifts everything after it.moved, import, and removed blocks put the change in the PR diff and replay in every environment. state mv/state rm are out-of-band: unreviewable, unreplayable, invisible to the next reader.sensitive = true masks CLI output; the value sits in plaintext in the state file, the saved plan, and TF_LOG output. Encrypt the backend, restrict who can read state, and prefer ephemeral values (>=1.10) and write-only arguments (>=1.11).apply -refresh-only accepts reality into state, a normal apply overwrites reality with the code. Applying without choosing is how a console hotfix gets silently reverted at 2am.Read the symbols before the summary:
| Marker | Means | Reaction |
|---|---|---|
+ | create | Expected count matches the change you made? |
~ | update in place | Safe class; still check the attribute is the one you edited |
-/+ | destroy then create | Downtime and a new ID; find the # forces replacement line |
+/- | create then destroy | create_before_destroy is on; unique names will collide |
- | destroy | Needs an explanation you could give in an incident review |
(known after apply) | value unresolved at plan | Anything keyed on it will fail for_each; anything printed from it is unverifiable now |
Note: Objects have changed outside of Terraform | drift detected during refresh | Rule 8 — decide before you apply |
terraform show tfplan re-renders a saved plan; terraform show -json tfplan is the machine-readable form every gate should read (ci.md).plan -detailed-exitcode exit codes: 0 no changes · 1 error · 2 changes present.ignore_changes).count is positional. Reserve it for identical replicas and the enable flag: count = var.enabled ? 1 : 0, referenced as one(aws_x.y[*].id).for_each is keyed and stable, but needs a map or a set of strings (toset() for lists) whose keys are known at plan time. Values may be unknown; keys may not. Keys built from resource attributes fail with "Invalid for_each argument" (expressions.md).count to for_each without one moved block per index destroys and recreates every instance. Get the index→key mapping from terraform state list, not from memory (worked example in refactoring.md).for_each keys — a map that merges in one sensitive input becomes sensitive as a whole and the plan rejects it.required_version in the root module turns "my colleague gets a parse error" into a clear message. Floors for the syntax this skill recommends:
| Feature | Floor |
|---|---|
moved blocks | terraform >=1.1 |
precondition / postcondition, replace_triggered_by | terraform >=1.2 |
optional() object attributes with defaults | terraform >=1.3 |
terraform_data (replaces null_resource) | terraform >=1.4 |
import blocks, check blocks, plan -generate-config-out | terraform >=1.5 |
terraform test with .tftest.hcl | terraform >=1.6 |
removed blocks, for_each in import, mock_provider | terraform >=1.7 |
| Provider-defined functions | terraform >=1.8 |
Ephemeral values and resources, S3 backend use_lockfile | terraform >=1.10 |
| Write-only arguments | terraform >=1.11 |
OpenTofu forked at the 1.6 line: floors above 1.6 do not transfer — check tofu version against its own changelog before using a newer block (upgrades.md).
Before emitting HCL or proposing an apply:
for_each keyed on a plan-time-known string?.terraform.lock.hcl covering every platform in lock_platforms?validation wherever a wrong value is expensive?.tfvars, a variable default, or an unmasked output?0 to add, 0 to change, 0 to destroy?User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/terraform/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| terraform_binary | terraform | tofu | terraform | Command name in every example; tofu switches version-floor checks to the OpenTofu changelog and enables its native state-encryption guidance |
| primary_provider | aws | gcp | azure | other | aws | Which provider's examples, auth model, and backend appear first in explanations |
| backend_type | s3 | gcs | azurerm | tfc | local | s3 | Locking mechanism, versioning advice, and CI credential wiring |
| env_layout | dir-per-env | workspace-per-env | single-state | dir-per-env | Refactoring and CI examples; workspace-per-env turns on the workspace-safety warnings instead of suppressing them |
| lock_platforms | list | linux_amd64, darwin_arm64 | Platforms passed to terraform providers lock and checked in the Output Gates |
| destroy_gate | number (>=0) | 0 | Destroy count above which the agent stops, names every destroyed address, and asks before proposing an apply |
| parallelism | number (1-50) | 10 | Value used in generated plan/apply commands; lower it when the provider throttles |
| plan_summary_detail | full | destructive-only | counts | destructive-only | How much plan output gets surfaced (chat and the PR comment in ci.md): full renders every changed resource, destructive-only posts counts plus every destroyed, replaced, and "forces replacement" line and collapses the rest, counts posts the summary line and the destroy list only |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
state surgery vs declarative blocks, whether -auto-approve is ever acceptable — affects which refactoring path is offered firstplan_summary_detail, HCL-vs-explanation ratio in answers, proactive warnings versus on-demand — affects how every plan and review is reported| Trap | Why it fails | Do instead |
|---|---|---|
| CLI workspaces for dev/prod separation | Same backend, same credentials; the active workspace is invisible CLI state — applying in the wrong one looks identical to the right one | Directory per environment with separate backends and separate cloud roles (state.md) |
Routine -target applies | Leaves the graph partially applied; the next full plan is a surprise diff nobody scoped | Emergencies only, always followed by a clean full plan |
Interpolation in the backend block | Backends cannot read variables or locals — the block is evaluated before anything else exists | Partial config: omit the keys and pass -backend-config=env/prod.tfbackend |
| Provisioners for configuration | Not idempotent, untracked in state; a failed provisioner taints the whole resource | user_data/cloud-init, config management, or terraform_data (>=1.4) |
| Hand-editing state JSON | Serial and lineage mismatch corrupts the backend copy — or worse, the push succeeds | state mv/rm/push on a pulled backup (Core Rules 2) |
ignore_changes = all | Freezes the entire resource forever; future config edits become silent no-ops | Ignore the one attribute, with a comment saying who writes it (lifecycle.md) |
| Treating plan success as apply safety | Plan validates config against state, not against the cloud: quotas, IAM, name collisions, and eventual consistency all surface at apply | Apply early in a sandbox account; keep changes small so failures are attributable |
apply -auto-approve outside CI | Removes the only human checkpoint between a typo and deleted production | Auto-approve only in a pipeline applying a reviewed saved plan |
Committing terraform.tfstate or .terraform/ | Ships every secret in state to git history and leaves everyone on a different copy | Gitignore both; commit .terraform.lock.hcl |
Module source pinned to a branch (?ref=main) | The build changes under you with no diff in your repo | Tag refs (?ref=v1.2.3) or registry versions |
depends_on sprinkled to fix ordering | Hides a missing attribute reference and, at module level, defers every data source inside to apply time | Reference the attribute you actually need; pass explicit values between modules |
~> constraints: exact pins for third-party registry modules (supply-chain surface); pessimistic minor constraints acceptable for internal modules gated by your own CI.upgrades.md).More Clawic skills, get them at https://clawic.com/skills/terraform (install if the user confirms):
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/terraform.