terraform

Writes, 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 pl…

Iván

@ivangdavila

What This Skill Does

Writes, debugs, and refactors Terraform and OpenTofu configurations — HCL, modules, state, providers, and CI/CD pipelines. Handles plan/apply failures, state surgery, drift detection, provider pinning, and infrastructure-as-code review.

Replaces manual state manipulation and ad-hoc debugging by providing structured guidance for every Terraform failure mode, refactoring pattern, and CI integration.

When to Use It

  • Debug a terraform plan that shows a permanent diff or unexpected destroy
  • Rename a resource or module and update state without destroying infrastructure
  • Import an existing cloud resource into Terraform state
  • Recover from a stuck state lock or corrupted state file
  • Pin provider versions or upgrade across a major provider version
  • Design a CI pipeline with plan-on-PR, apply-on-merge, and drift detection

Install

$ openclaw skills install @ivangdavila/terraform

User 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/.

When To Use

  • Writing or reviewing HCL: resource design, count vs for_each, module boundaries, variable types and validation
  • Debugging plan/apply failures, permanent diffs, cycles, unknown-at-plan errors, stuck locks, drift
  • Refactoring live infrastructure: renames, module extraction, imports, splitting or merging state
  • Pinning providers, upgrading the CLI or a provider major version, moving between Terraform and OpenTofu
  • Designing backends, environment layout, and CI plan/apply gates with policy, tests, and drift detection
  • Recovering after damage: lost or corrupted state, wrong state rm, unintended destroy, interrupted apply
  • Not for choosing which cloud resources to build (→ aws, gcp, azure) or configuring what runs inside them (→ ansible)

Quick Reference

SituationPlay
Renamed a resource or module in codemoved block (>=1.1); apply everywhere, delete the block in a later PR → refactoring.md
Cloud object exists but is not in stateimport block (>=1.5) + plan -generate-config-out=gen.tf; rewrite the draft, merge at zero diff → refactoring.md
Stop managing something without destroying itremoved block with destroy = false (>=1.7); state rm only as one-off surgery → refactoring.md
Permanent diff on every planFind the writer (autoscaler, console, another pipeline, provider normalization) before reaching for ignore_changesdebug.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 winningPrecedence: -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 lockProve the holder is dead, then force-unlock <LOCK_ID>recovery.md
State lost, corrupted, or pushed wrongBucket object versions are the only undo; rebuild by import if there are none → recovery.md
"Inconsistent dependency lock file" or a checksum error on installThe lock lacks that provider or that platform → providers.md
Two regions, two accounts, one configProvider 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 applyLower -parallelism (default 10) → performance.md
Moving to a remote backend or between backendsPull a backup, then init -migrate-statestate.md
Secret landed in state, a plan file, or a logRotate 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 questionExact 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 attributelifecycle meta-arguments and what each one costs → lifecycle.md
A destroy fails or a teardown leaves debrisDeletion protection, retained objects, reverse-order dependencies → lifecycle.md
Wiring plan-on-PR and apply-on-mergeOIDC 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 pressurecommands.md
Anything elseSmallest 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.

Core Rules

  1. The saved plan is the contract. 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.
  2. Back up before state surgery. 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.
  3. Read the destroy count from the resource lines, not the summary. A replacement is counted once as an add and once as a destroy, so 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.
  4. Pin everything. Providers via 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.
  5. Key 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.
  6. Refactor declaratively. 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.
  7. Nothing sensitive is safe in state. 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).
  8. Drift is a question, not an error. When the cloud changed under you, decide explicitly: 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.

Plan Triage

Read the symbols before the summary:

MarkerMeansReaction
+createExpected count matches the change you made?
~update in placeSafe class; still check the attribute is the one you edited
-/+destroy then createDowntime and a new ID; find the # forces replacement line
+/-create then destroycreate_before_destroy is on; unique names will collide
-destroyNeeds an explanation you could give in an incident review
(known after apply)value unresolved at planAnything keyed on it will fail for_each; anything printed from it is unverifiable now
Note: Objects have changed outside of Terraformdrift detected during refreshRule 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.
  • "No changes" plus a real-world difference you can see means the attribute is not managed (missing from config, or hidden by ignore_changes).

Count vs for_each

  • 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).
  • Migrating 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).
  • Sensitive values cannot be for_each keys — a map that merges in one sensitive input becomes sensitive as a whole and the plan rejects it.

Version Floors

required_version in the root module turns "my colleague gets a parse error" into a clear message. Floors for the syntax this skill recommends:

FeatureFloor
moved blocksterraform >=1.1
precondition / postcondition, replace_triggered_byterraform >=1.2
optional() object attributes with defaultsterraform >=1.3
terraform_data (replaces null_resource)terraform >=1.4
import blocks, check blocks, plan -generate-config-outterraform >=1.5
terraform test with .tftest.hclterraform >=1.6
removed blocks, for_each in import, mock_providerterraform >=1.7
Provider-defined functionsterraform >=1.8
Ephemeral values and resources, S3 backend use_lockfileterraform >=1.10
Write-only argumentsterraform >=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).

Output Gates

Before emitting HCL or proposing an apply:

  • Plan saved to a file, and the thing applied is that file?
  • Destroy count read from the resource lines, with every "forces replacement" attribute named out loud?
  • Every for_each keyed on a plan-time-known string?
  • Providers pinned and .terraform.lock.hcl covering every platform in lock_platforms?
  • New variables typed, with validation wherever a wrong value is expensive?
  • No secret in a committed .tfvars, a variable default, or an unmasked output?
  • If this is a refactor: does the plan read 0 to add, 0 to change, 0 to destroy?

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/terraform/config.yaml.

VariableTypeDefaultEffect
terraform_binaryterraform | tofuterraformCommand name in every example; tofu switches version-floor checks to the OpenTofu changelog and enables its native state-encryption guidance
primary_provideraws | gcp | azure | otherawsWhich provider's examples, auth model, and backend appear first in explanations
backend_types3 | gcs | azurerm | tfc | locals3Locking mechanism, versioning advice, and CI credential wiring
env_layoutdir-per-env | workspace-per-env | single-statedir-per-envRefactoring and CI examples; workspace-per-env turns on the workspace-safety warnings instead of suppressing them
lock_platformslistlinux_amd64, darwin_arm64Platforms passed to terraform providers lock and checked in the Output Gates
destroy_gatenumber (>=0)0Destroy count above which the agent stops, names every destroyed address, and asks before proposing an apply
parallelismnumber (1-50)10Value used in generated plan/apply commands; lower it when the provider throttles
plan_summary_detailfull | destructive-only | countsdestructive-onlyHow 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:

  • Tooling: wrappers (Terragrunt, Terramate), pre-commit hooks, plan-summary tooling — affects which pipeline shape gets proposed
  • Conventions: resource and module naming, tagging standard, file split (main/variables/outputs vs per-domain) — affects every generated block
  • Platform: clouds, regions, and accounts in play, and the cross-account role-assumption pattern — affects provider aliases and backend keys
  • Safety posture: appetite for state surgery vs declarative blocks, whether -auto-approve is ever acceptable — affects which refactoring path is offered first
  • Workflow: where apply happens (laptop, CI, managed platform), review gates, who holds production credentials — affects the CI examples
  • Compliance: mandatory policy engine, required tags, encryption and public-access rules — affects the testing and gate recommendations
  • Output format: plan-summary verbosity beyond plan_summary_detail, HCL-vs-explanation ratio in answers, proactive warnings versus on-demand — affects how every plan and review is reported
  • Cadence: drift-detection schedule and provider-upgrade rhythm — affects what gets scheduled versus run on demand

Traps

TrapWhy it failsDo instead
CLI workspaces for dev/prod separationSame backend, same credentials; the active workspace is invisible CLI state — applying in the wrong one looks identical to the right oneDirectory per environment with separate backends and separate cloud roles (state.md)
Routine -target appliesLeaves the graph partially applied; the next full plan is a surprise diff nobody scopedEmergencies only, always followed by a clean full plan
Interpolation in the backend blockBackends cannot read variables or locals — the block is evaluated before anything else existsPartial config: omit the keys and pass -backend-config=env/prod.tfbackend
Provisioners for configurationNot idempotent, untracked in state; a failed provisioner taints the whole resourceuser_data/cloud-init, config management, or terraform_data (>=1.4)
Hand-editing state JSONSerial and lineage mismatch corrupts the backend copy — or worse, the push succeedsstate mv/rm/push on a pulled backup (Core Rules 2)
ignore_changes = allFreezes the entire resource forever; future config edits become silent no-opsIgnore the one attribute, with a comment saying who writes it (lifecycle.md)
Treating plan success as apply safetyPlan validates config against state, not against the cloud: quotas, IAM, name collisions, and eventual consistency all surface at applyApply early in a sandbox account; keep changes small so failures are attributable
apply -auto-approve outside CIRemoves the only human checkpoint between a typo and deleted productionAuto-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 copyGitignore both; commit .terraform.lock.hcl
Module source pinned to a branch (?ref=main)The build changes under you with no diff in your repoTag refs (?ref=v1.2.3) or registry versions
depends_on sprinkled to fix orderingHides a missing attribute reference and, at module level, defers every data source inside to apply timeReference the attribute you actually need; pass explicit values between modules

Where Experts Disagree

  • Vanilla Terraform vs wrapper tooling (Terragrunt and friends): the frontier is duplication — one team with a handful of stacks loses more to wrapper complexity than it saves; once environments × stacks means maintaining dozens of near-identical backend and provider blocks, DRY tooling earns its cost.
  • Exact module pins vs ~> constraints: exact pins for third-party registry modules (supply-chain surface); pessimistic minor constraints acceptable for internal modules gated by your own CI.
  • One shared state vs many micro-states: the frontier is change coupling — resources that always ship together belong in one state; every cross-state reference costs a data-source hop and an ordering problem between pipelines.
  • Terraform vs OpenTofu: the frontier is licensing exposure and feature need, not ideology — OpenTofu is a drop-in for most existing code and adds state encryption; teams already on a managed HashiCorp platform, or depending on features that landed after the fork, pay a migration cost for nothing. Decide once, per organization (upgrades.md).

Related Skills

More Clawic skills, get them at https://clawic.com/skills/terraform (install if the user confirms):

  • aws — provider-specific resource and service guidance
  • devops — pipeline and delivery design around plan/apply gates
  • github-actions — wiring plan-on-PR / apply-on-merge workflows
  • ansible — configuring what lives inside the instances Terraform creates
  • k8s — workloads on the clusters Terraform provisions

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/terraform.

Related skills

AWS | Amazon Web Services

@ivangdavila

Architects, debugs, secures, and cost-optimizes AWS infrastructure — EC2, Lambda, RDS, VPC, IAM, ECS, CloudFront. Use when deploying or reviewing anything on AWS, when a bill jumps or spend has to come down, when an AccessDenied, throttle, timeout, 502/503/504, or unreachable-database error has no obvious cause, when choosing between Lambda, Fargate, EC2, RDS, DynamoDB, SQS, or EventBridge, when hardening IAM policies, S3 exposure, security groups, or secrets, when writing Terraform/CloudFormation/CDK against AWS, when auditing an account you inherited, or when a service quota, cold start, connection limit, or failover is the thing that broke. Covers VPC and subnet design, NAT versus VPC endpoints, Organizations and cross-account roles, backups and disaster recovery, and CLI/SSO profiles. Not for object-storage patterns in depth (`s3`), DynamoDB key modeling (`dynamodb`), Kubernetes manifest authoring (`k8s`), or Terraform language mechanics (`terraform`).

24.0k

bash

@ivangdavila

Writes, debugs, and hardens Bash shell scripts — quoting, arrays, strict mode, traps, argument parsing, and macOS/Linux portability. Use when writing or reviewing any script, one-liner, cron job, deploy script, container entrypoint, or CI step; when a script breaks on spaces in filenames, exits silently, ignores set -e, hangs, or returns the wrong exit code; when quoting, IFS, globs, arrays, heredocs, process substitution, trap, getopts, or mapfile misbehave; when shellcheck flags SC2086 and friends; when unbound variable, bad substitution, command not found, ambiguous redirect, or unexpected end of file show up; when a script works by hand but fails under cron, systemd, sudo, or a CI runner; or when porting between macOS bash 3.2, GNU/Linux, WSL, and POSIX sh. Not for interactive zsh or fish configuration, not for PowerShell, and not for host-level cron, systemd, or permission failures (linux).

34.4k

react

@ivangdavila

Builds, debugs, and reviews React apps: components, hooks, state, Server Components, forms, performance, testing. Use when writing or refactoring components, choosing state management (useState, Context, Zustand, TanStack Query, Redux), when a component rerenders too often, loops infinitely ("too many re-renders"), shows stale or not-updating state, fails with a hydration mismatch or hook-order error, when an effect fires twice or fetches race, when typing lags or long lists scroll slowly, when adopting React 19, Server Actions, or the React Compiler, when testing components with Testing Library, typing props in TypeScript, or reviewing AI-generated React code. Not for React Native (mobile) or Next.js routing and deployment — use react-native or nextjs.

37.8k

linux

@ivangdavila

Debugs and hardens Linux hosts: permissions, disk full, OOM kills, stuck processes, systemd units, cron, networking, SSH, and boot failures. Use when a service starts by hand but fails at boot, a process ignores kill -9, df and du disagree, a box runs out of memory or inodes, sudo or ACLs deny access, SELinux blocks a write, a job works in the shell but not in cron, sshd rejects a key, an upgrade leaves packages half-configured, load is high while the CPU sits idle, or a host needs firewall rules, users, LVM, journald, kernel tuning, or a security baseline. Covers Debian/Ubuntu, RHEL/Fedora, Arch, and Alpine. Not for shell-script syntax (bash) or container build and runtime internals (docker).

84.9k

Kubernetes

@ivangdavila

Debugs Kubernetes workloads and reviews manifests: pods, probes, resources, rollouts, Services, storage, RBAC. Use when a pod is Pending, CrashLoopBackOff, ImagePullBackOff, OOMKilled or stuck Terminating, when a Service or Ingress serves nothing or returns 502/503/504, when cluster DNS is flaky, a rollout hangs or silently ships a broken version, an HPA refuses to scale, a PVC stays unbound, a node goes NotReady or a drain never finishes, when writing or reviewing YAML, Helm charts or kustomize overlays, when tuning requests, limits, QoS, probes and graceful shutdown, or when locking down RBAC, NetworkPolicy, Pod Security and Secrets. Covers kubectl triage, StatefulSets and PVCs, Jobs and CronJobs, autoscaling, admission webhooks, node drains and cluster upgrades. Not for building container images — that is `docker`.

44.5k

MongoDB

@ivangdavila

Designs MongoDB schemas, indexes, and aggregation pipelines, and debugs slow queries, connection errors, and replica set failures. Use when modeling documents, deciding embed vs reference, reading an explain plan, or fixing a COLLSCAN, and when a query times out, a pipeline aborts at the memory limit, a cursor dies mid-loop, the pool exhausts and server selection times out, writes fail with duplicate key or "not writable primary", a secondary lags, the oplog window closes, a shard key hotspots, or WiredTiger cache stalls the cluster. Covers mongosh and Compass, Atlas, Mongoose and driver connection strings, transactions and retry loops, change streams, time-series collections, Atlas Search and vector search, sharding, backups, restores, and upgrades. Not for SQL or relational modeling — normalization instincts actively mislead here.

53.6k