Most of a terraform plan is routine: tags change, a size is raised, a new rule appears. A few lines are different, because applying them removes data that cannot be rebuilt or opens a service to the whole internet. This guide shows where those lines hide in the output, what each marker means, and a fixed order of reading that takes a couple of minutes. The short answer to the question in the title: read the action on every resource that holds data, read the reason behind every replacement, and read the source address of every firewall rule that was added or changed. The summary count at the bottom tells you how much to read, not whether it is safe.
Plan first, and apply what you reviewed
Running terraform plan shows what Terraform proposes without carrying it out. Without an output option, the result is described by HashiCorp as a speculative plan: a description of the effect, with no intent to apply it (HashiCorp, plan command). Plain terraform apply generates a fresh plan and asks you to approve it, so the thing you approve is whatever the world looks like at that moment.
That is a good reason to save the plan to a file when a review matters. The same page describes the output option, which writes the plan into a file that can later be passed to apply, and says this two-step flow is mostly meant for automation (HashiCorp, plan command). If a colleague reads the plan on Tuesday and a different plan is generated on Wednesday, the review covered something else. Saving the file closes that gap.
Read the symbols before the names
Each resource block in the output starts with a comment and a symbol in the leftmost column. The sample output in the documentation shows the shapes: a plus for a new object, a minus for one that is removed, a tilde for an in-place change, and a combined minus and plus for a replacement (HashiCorp, plan command). The comment spells it out in plain words, for example that the object is new or is going away.
Two details are easy to miss:
- A destroy can come with a parenthetical explanation. In the documentation's example, a bucket is destroyed because it is no longer in the configuration at all (HashiCorp, plan command). Someone deleted a block, perhaps while tidying a file, and the plan quietly turned that into a deletion of a real object.
- The in-place symbol is the calm one. A tilde on a tag or a description is rarely worth a debate. The same tilde on an encryption setting is a different matter, and we come back to it below.
Replacement is destroy plus create
A replacement is the line that surprises people most. If the provider's API refuses to edit an argument on a live object, Terraform removes that object first and builds a fresh one carrying the new arguments (HashiCorp, lifecycle). For a web server built from an image, that is fine: the new one comes up and nothing was stored on the old one. For a database, a storage bucket or a disk, the new object starts empty.
The plan marks the guilty argument with a trailing comment saying it forces replacement. The documentation's example shows an image identifier changing from one value to another with that comment next to it (HashiCorp, plan command). So the order of reading is: find every resource with a replacement symbol, then look at which argument carries the forces replacement comment, then ask what kind of thing it is.
A name change is the classic trap. A bucket or database whose name is edited in the configuration often cannot be renamed, so the plan shows a replacement and the old contents are gone once it runs. If you meant to rename it, the data has to be copied first.
What the lifecycle settings do and do not promise
Two settings in the lifecycle block, create_before_destroy and prevent_destroy, change what the plan does, and neither is a substitute for reading it. The create-first setting reverses the order, building the new object before removing the old one (HashiCorp, lifecycle). That helps with downtime. It does not help with data, because the old object is still removed at the end and the new one is still empty.
The prevent_destroy setting is stronger: when it is true, Terraform refuses to produce a plan that removes the object and reports an error instead (HashiCorp, lifecycle). The same page lists database instances and storage as the sort of thing it suits, and warns to use it sparingly because it blocks some changes entirely. It also has a limit worth knowing: if the whole resource block is deleted from the configuration, the setting goes with it, and Terraform will destroy the real infrastructure anyway (HashiCorp, lifecycle). So a plan that shows a destroy for a block someone removed is exactly the case the protection does not cover.
A replacement that cascades from another resource
Sometimes the forces replacement comment sits on an argument whose new value reads as known after apply. That means the value is another resource's identifier, and that other resource is also on its way out and back in, so the identifier does not exist yet. The effect travels downhill: a replaced network can force every object attached to it to be replaced too. If one of those attached objects holds data, you have a data loss that began somewhere far from the thing you edited. Follow the chain upward until you find the root change, and decide whether that root change is worth it.
Firewall and security rules
Network rules are the other place worth slowing down. For every rule that is added or changed, read the source. A source meaning every address on the internet is expected for a public web port and suspect almost anywhere else, especially remote administration, databases and caches. Unchanged rules, outbound rules, and rules restricted to internal addresses are not the target of this pass. The plan is also silent about rules that already exist in the cloud account outside Terraform, so a clean plan is not a statement about the network as a whole.
Encryption set to off
Look for encryption arguments in create and update blocks. A value moving from true to false is a weakening that an in-place tilde can hide in plain sight. A plan that shows no encryption argument at all tells you nothing, since providers apply defaults that are not printed. Do not infer safety, or danger, from a line that is missing.
A reading order that fits in one pass
- Glance at the summary count at the bottom, which counts the objects to add, change and destroy (HashiCorp, plan command). Use it to size the review, and note any destroy you did not expect.
- Scan the headers for destroys and replacements. For each one, write down what the resource is.
- For those that hold data, read the reason: removed from configuration, or a forces replacement argument. Decide whether the data survives.
- For replacements, check whether the reason is a literal value you typed or a value known after apply from another resource.
- Read each added or changed firewall rule for its source and port.
- Read encryption changes.
- Only then approve, and apply the saved plan file rather than generating a new one.
To test your own habit, take an old plan and see whether you can answer three questions about it from memory after one reading: what data could be lost, what is now reachable from outside, and what was weakened. If you cannot name them, the plan was skimmed.
When this does not apply
What we cover is reading the text of one plan. It does not replace reading the configuration files, checking the state, or confirming that backups exist and restore, none of which a plan shows. Hidden attributes and provider defaults are not visible in the output, so a clean-looking plan can still hide a risky default. Resources built by modules look the same in the plan but may be owned by another team. Other tools wrap Terraform and print plans in their own format, and the markers described here follow the HashiCorp documentation we cite, so check your version if the output looks different. Projects that apply through a pipeline with automatic approval need the protection settings and policy checks more than a human reader. We did not run experiments for this guide; it restates documentation and common review practice, so the examples are illustrations and not measurements.
When a plan is long, the same reading order can be shared with a reviewer who sees the pull request, and the habit of asking what could be lost is close to what we describe in Kubernetes manifest settings that affect safety.
Getting the plan checked automatically
If you would like a quick pass over a plan before apply, our terraform-plan-risk-review skill takes the pasted text of one plan and reports four kinds of change: a database, bucket or disk that goes away or is rebuilt, an everywhere-source firewall rule on a port that is not meant for the web, encryption turned off, and a rebuild caused by an identifier that changed upstream. Each problem gets one line naming the resource and a suggested fix. Harmless look-alikes such as a tag edit are left out, and a plan with none of the four gets a one-line answer saying so. Configuration files, state and backups are outside what it can see.
Read this post as Markdown: /blog/terraform-plan-read-before-apply.md · Atom feed.
