编程

Terraform

解决 Terraform 卡点:调试 plan/apply 失败、安全重构线上资源、精确钉住 provider 与模块版本。

它能做什么

诊断 HCL 错误与 plan/apply 失败,包括永久 diff、循环依赖、provider 不一致 plan、卡死的 state 锁。使用 `moved`、`import`、`removed` 块引导重构,让资源重命名和模块拆分可在每个环境复现。覆盖 state 手术、后端迁移、drift 决策、provider 钉版本、大版本升级,以及带策略门禁的 CI 编排,同时支持 Terraform 与 OpenTofu,包括 workspace 与 state 布局。

什么时候用它

  • 排查 plan 出现永久 diff、意外 destroy 或 forces replacement
  • 用 `moved`、`import` 块重命名资源或拆分模块
  • 从卡死的 state 锁、丢失的 state 文件或错误的 `state rm` 中恢复
  • 搭建 plan-on-PR、apply-on-merge 流水线,配置 OIDC、saved plan 与策略门禁

技能文档

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 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).

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.

相关技能

调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。

82 次安装3 星标

为 Google Cloud 架构选型、部署调试、安全加固与成本优化,每次建议都附带每月美元估算。

120 次安装4 星标

在 EC2、Lambda、RDS、VPC、IAM 等核心服务上做架构、排查、加固与成本优化,每次建议都给出月度开销与故障域。

作者 Iván138 次安装2 星标

针对你的 Azure 订阅,做架构设计、故障排查、安全加固与成本优化

作者 Iván86 次安装2 星标

定位并修复 YAML 解析错误、隐式类型转换陷阱及各工具链的兼容性问题。

82 次安装2 星标

针对 Node.js 运行时、打包与线上问题,给出可落地的诊断依据与修复规则。

142 次安装5 星标