A plan is a diff you must read
terraform plan reads the configuration and the current state, refreshes what it can see of the real resources and lists the creates, changes, replacements and destroys it would perform. Nothing changes when you run it. The mistake is treating a long plan as noise. Read it the way you would read a code review: each line begins with an action, and the dangerous ones are replacements and destroys on anything that holds data, a name or an address others depend on. With the detailed exit code option the command exits 0 for no changes, 1 for an error and 2 when changes are present, which lets a script tell clean from drift without parsing text.
- Look first at every line that says destroy or replace.
- Check that each create matches an item you intended.
- Use the detailed exit code in scripts, but still read the text.
Saved plans and state hold secrets
Terraform's documentation is blunt about this. State and plan files can contain sensitive values such as initial database passwords or tokens, in plain text. Marking a value sensitive redacts it from the terminal and the web interface; it does not keep the value out of state or the saved plan. Treat a saved plan file as a sensitive artefact, do not give it a configuration file extension, and keep local state out of version control. The recommended protection is remote state with encryption at rest, access control and an audit trail. Terraform 1.10 and later can mark input variables, outputs and resources as ephemeral so their values are not persisted to state or plan files, and 1.11 and later adds write-only arguments for resource attributes; check your version and your provider before relying on either, which are worth knowing if you handle credentials in configuration.
- Never commit local state or a saved plan.
- Sensitive does not mean absent from state.
- Use a remote, encrypted, access-controlled backend. Not every backend supports locking, so check whether yours does.
Protect the resources that hold data
The lifecycle setting prevent_destroy makes Terraform fail any plan that would delete the real object, which suits databases and storage. It has a limit that people miss: it protects the resource only while its block stays in the configuration. If someone deletes the block, the protection goes with it, and the documentation says so. So pair it with review of every plan, with restricted credentials and with backups, rather than treating it as a lock. Use the target option sparingly; the documentation reserves it for exceptional cases because routine use hides drift.
- prevent_destroy is a safety net, not a lock.
- A removed resource block removes the protection.
- Avoid target for routine work.
When a module is worth writing
HashiCorp advises using modules sparingly and says a module should introduce an architectural concept built from several resources, not wrap one resource type. If the best name for your module repeats the resource inside it, use the resource directly. A good module has typed inputs for what really varies, documented outputs, version constraints and no hard-coded provider configuration. It is also worth keeping the hierarchy shallow and combining modules by composition. Write a module when you need the same group of resources more than once, such as a network with a server and a database, and not before.
- Several resources, one concept, used more than once.
- Inputs only for what actually differs.
- Pin versions for Terraform and providers.
Existing resources and where the paid work stops
Bringing resources that already exist under Terraform is a different task from describing new ones. Each remote object must map to exactly one resource address in state, and the documentation recommends import blocks that run during apply over the manual command. You then iterate until the plan shows no changes. That is larger and riskier than a new module, so the fixed module job covers new resources in a test account with a reviewed plan, and the monthly drift review covers differences afterwards. Neither applies to your production account for you.
Sources and limits
- Terraform: plan command Checked 2026-10-11.
- A plan compares configuration with prior state and proposes changes without applying them; -out saves it and -detailed-exitcode returns 0, 1 or 2.
- -target is for exceptional circumstances; a saved plan file stores sensitive data in cleartext.
- Terraform: sensitive data in state Checked 2026-10-11.
- State and plan files can hold sensitive data; sensitive = true redacts output but values are still stored in state and plan files.
- Store state remotely, encrypted and access-controlled.
- Terraform: module development Checked 2026-10-11.
- Use modules sparingly; do not wrap a single resource type.
- Terraform: lifecycle Checked 2026-10-11.
- prevent_destroy fails a plan that would delete the resource but does not protect it if the resource block is removed.
- Terraform: import command Checked 2026-10-11.
- Each remote object should map to one resource address; an import block can replace the manual command.
- Terraform: remote state Checked 2026-10-11.
- Remote backends give shared access; for fully-featured remote backends Terraform can also use state locking.
- Terraform: state locking Checked 2026-10-11.
- Terraform locks state for operations that could write it when the backend supports locking, and not all backends support locking.
- Terraform: ephemeral block Checked 2026-10-11.
- Ephemeral resources are temporary and Terraform does not store them in state or plan files; write-only arguments pass temporary values to managed resources without persisting them.
- Terraform 1.10 changelog Checked 2026-10-11.
- Terraform 1.10.0 (27 November 2024) introduced ephemeral resources and ephemeral input variables and outputs, which are not persisted to the plan or state files.
- Terraform 1.11 changelog Checked 2026-10-11.
- Terraform 1.11.0 (27 February 2025) added write-only attributes to resources, whose values are not saved in state.