Terraform dry run: how plan, apply, destroy, and state mv each behave
There is no terraform dry-run command. Treat plan, apply, destroy, and state mv as if they all preview a change the same way to get the best results.
terraform dry-run flag, only different commands that each preview changes their own way.plan, apply, destroy, and state mv -dry-run do (and don't) confirm before anything actually happens.Don't mistake a Terraform dry run as something that will get the same results as terraform plan. That definition undersells how differently Terraform previews four distinct operations: planning a change, applying one, tearing infrastructure down, rewriting state bindings, and bringing an existing resource under management for the first time.
This article walks through each of those four commands: what a dry run actually reports for it and what it deliberately doesn't tell you.
It closes with what happens once dry runs stop being an occasional courtesy check and become routine, high-frequency work across an enterprise team running many of them a day against the same infrastructure.
What is a Terraform dry run?
"Terraform dry run" isn't a command in Terraform's own vocabulary; it's shorthand for a family of behaviors spread across several different terraform commands.
Separate this, from the start, from the OpenTofu or Terraform code DRY (Don't Repeat Yourself), the code-reuse principle tools like Terragrunt are built around; the two share four letters and nothing else.
A dry run is any operation that computes and reports what Terraform would do against real infrastructure without actually calling a cloud provider's mutating API.
Every command covered below shares that same underlying contract: it reads current provider data and compares that against configuration and state, then reports what changed.
Where they diverge is in what gets diffed and what gets written in the process (state, in some cases, immediately).
Terraform plan dry run: the default preview
terraform plan is the dry run everything else in this article gets compared against. It refreshes what Terraform knows about existing resources, evaluates your configuration files and any data sources, and produces an execution plan: a list of every resource instance it would add, change, or destroy, and why. None of that touches a mutating API endpoint.
Run terraform plan against a configuration like that after changing the ingress port, for example, and Terraform reports exactly one resource changing, nothing else.
Add -out=app.tfplan and the same preview gets written to a binary plan file rather than only printed to the terminal, which is more important than it sounds: a saved plan is the one artifact that locks in exactly what a later apply will do, independent of whatever plan might compute if you ran it again five minutes from now.
Terraform plan dry run documentation: what HashiCorp's docs actually promise
It pays to be precise about what the official terraform plan command reference actually commits to, rather than what's commonly assumed.
-refresh=falseskips the read-only refresh step entirely, useful when you already trust the state and want a faster preview.-refresh-onlydoes close to the opposite: it checks for drift against real infrastructure without proposing any configuration changes at all, the documented way to check whether anything has changed underneath you as its own question.-jsonemits the plan in a machine-readable format, the format most CI pipelines and policy tools actually consume rather than the human-readable summary.
None of that requires any backend configuration beyond what init already set up, and none of it is contingent on which backend you're using. The behavior is identical whether state lives in a local file or a remote backend; only where the read happens changes.
Terraform apply dry run: reviewing before you commit
apply doesn't have a bare dry-run flag of its own. The dry run already happened: it's whatever plan you ran a moment ago. The discipline that actually counts here is applying the exact file that preview produced, terraform apply app.tfplan, rather than running apply on its own and trusting that Terraform computes the identical plan a second time.
It usually will, but if the state changed in between (a teammate applied something, a pipeline ran, a resource drifted), Terraform refuses to apply a stale saved plan and tells you to re-plan instead, which is precisely the safeguard that a bare terraform apply doesn't get: run without a saved file, it generates a fresh plan on the spot and asks for confirmation before touching anything, and -auto-approve skips that confirmation entirely.
Reserve -auto-approve for pipelines that already gate on a separately reviewed plan file; using it on an ad hoc apply throws away the one review step the dry run was supposed to buy you.
Terraform destroy dry run: previewing a teardown
terraform plan -destroy is the dry run for the highest-stakes command in the list: The Terraform destroy command lists every resource your current configuration would remove, without removing anything yet.
Observation
Treat that output differently than an ordinary plan diff. An engineer skimming forty added tags moves on quickly; an engineer skimming forty resources scheduled for destruction should read every line. Terraform's plan diff communicates what changes, not the blast radius of what relies on it.
-target narrows a destroy preview to a specific module or resource, which is genuinely useful for troubleshooting a single stuck dependency. It's a worse habit for routine cleanups: removing the resource from configuration and running a normal, untargeted plan -destroy catches dependent resources that a narrowly targeted preview would simply never show you.
Terraform state mv -dry-run: previewing a state rewrite
terraform state mv rebinds an existing remote object to a new resource address in Terraform state, the move you reach for after renaming a resource block or refactoring it into a child module, so Terraform treats the change as a rename rather than a delete-and-recreate. -dry-run reports every resource instance the given address matches without moving or forgetting any of them.
Run that, and Terraform prints "Would move" for each match instead of "Move," touching nothing. It's a narrower preview than plan in one specific sense: it reports address matches, not the downstream diff a subsequent plan would show once the binding actually changes.
Implementation Detail
-dry-run isn't a recent addition to state mv; the flag predates its own official documentation. It went unlisted on HashiCorp's published command reference for years until a documentation revamp finally added it to the options list – the kind of gap that's easy to mistake for "unsupported" when it was really just "undocumented."
For workflows that lean more on renaming resources during a refactor than on manually rewriting bindings, Terraform's moved block has become the more common modern path for the same rename scenario, recorded in your Terraform configuration rather than run by hand against state.
Reviewing a saved plan without applying it
Every saved plan file from the commands above can be reviewed independently of whoever eventually runs apply.
terraform show app.tfplan re-renders a saved plan in the same human-readable form plan originally printed it in, and terraform show -json app.tfplan does the same as structured JSON – the format most useful when a plan needs to go through automated policy checks rather than a pair of human eyes.
That distinction between Terraform commands impacts your code review workflow: a plan file can be handed to a reviewer, attached to a pull request, or archived as a CI artifact without granting that reviewer any ability to apply it, the same separation of who can see a change from who can commit it that most teams already expect from terraform state show when inspecting a live resource instead of a pending one.
Where routine dry runs break down at enterprise scale
Everything above describes a dry run as a single, isolated event, and for a lot of workflows, which is accurate. It stops being the whole picture once dry runs are routine work rather than an occasional check, run dozens of times a day by different engineers and CI pipelines against the same shared state.
Lock Granularity Principle
A terraform plan has no notion of who else might be planning, or applying, against that same state at that same moment. Two dry runs computed five seconds apart, for entirely unrelated changes, still queue behind whatever lock the first apply acquires: the lock covers the whole state file rather than the specific resources either change actually touches.
A dry run only reports configuration drift at the exact moment it runs; nothing about the command itself watches for drift introduced between one reviewed plan and the apply that eventually follows it, however long that gap turns out to be. Neither of these is a flaw in any single command covered above. They're both just outside what a per-invocation CLI preview was ever built to track.
A graph-aware way to run terraform dry runs at scale
Stategraph's own plan-and-apply workflow (stategraph tf plan and stategraph tf apply, running OpenTofu underneath by default and Terraform itself if you point TF_CMD at it) treats the dry run as a first-class state rather than an implicit side effect of forgetting a flag: run stategraph tf plan without --out, and you get a genuinely read-only preview by default, no file written at all. Applying without a saved plan runs a fresh one, shows the diff, and waits for approval before committing anything, the same discipline terraform apply asks of you manually.
Where it goes further is the part a flat state file structurally can't do. Because Stategraph tracks your infrastructure's dependency graph directly rather than reconstructing it from scratch on every invocation, it checks for conflicts at the level of the individual resources a change actually touches instead of the whole file, using resource-level locking so two dry runs (and the applies that follow them) for genuinely unrelated resources stop queuing behind one shared lock.
A change spanning more than one state gets the same atomicity guarantee through a single multi-state transaction rather than a series of separately reviewed, separately risky applies. Because a saved plan records the exact state revision it was computed against, apply refuses a stale one by default – the same protection terraform apply already gives you against a plan file overtaken by events, just enforced automatically rather than by convention.
To be honest about scope, none of this changes what any individual dry run reports. It's additional infrastructure for teams that have already outgrown running the plain CLI solo against one uncontended state, not a requirement for every reader here.
Conclusion
"Terraform dry run" covers a handful of commands that only look alike from a distance. plan is the general-purpose preview everything else borrows from, apply and destroy each depend on the plan that precedes them rather than owning a dry run of their own, state mv -dry-run reports address matches rather than a downstream diff.
Knowing which behavior you're actually getting from which command is most of what separates a dry run that catches a mistake from one that quietly assumes it did.
Try Stategraph free and see the same plan and apply commands you already run, working against a graph instead of a flat state file.
Terraform dry run FAQs
Is there an actual terraform dry-run command?
No. Terraform has no single command or global flag named dry-run. What people mean by the phrase is spread across plan (the default preview for a change), plan -destroy (the preview for a teardown), and state mv -dry-run (a preview for a state rewrite), each behaving differently rather than sharing one implementation.
When was -dry-run added to terraform state mv?
The flag predates its own official documentation. HashiCorp's published state mv command reference didn't list -dry-run among its options for years, until a documentation revamp in May 2021 finally added it to the page, described in that update as missing from the docs previously rather than newly built into the command.
Does terraform import support a dry run?
No, and this is the one genuine gap among the commands covered here. import writes to state the moment it runs, with no preview flag available. Pairing -generate-config-out with an immediate follow-up plan is the closest working substitute: a clean plan with no proposed changes confirms the import bound the correct object.
What's the difference between terraform plan and a dry run?
For most purposes, they're the same thing: plan is the specific command that performs the dry run for a Terraform apply command. The broader term "dry run" just extends the same idea to destroy, state mv, and (in import's case) the workaround that stands in for a preview that doesn't actually exist.