AWS Builder Center
Terraform 1.17 -minimal-refresh said "No changes" while a file had drifted

Terraform 1.17 -minimal-refresh said "No changes" while a file had drifted

Terraform 1.17.0-rc2 adds -minimal-refresh, which skips the refresh for any resource your change does not touch. I edited a managed file by hand and ran plan both ways: the new flag printed "No changes" and exited 0, the full plan found the drift and exited 2. How the flag decides, what it never refreshes, and how to split PR plans from drift checks before you adopt it.

Engineer @ DevOps area

A plan that reported no changes on a drifted file

Terraform 1.17.0-rc1 shipped on October 7, 2026 with a planning option called -minimal-refresh. rc2 followed on October 14. GA is not out yet, so everything here was run on v1.17.0-rc2, darwin_arm64.
The flag closes issue #35290, "terraform plan -light", opened on June 4, 2024 and carrying 207 reactions. The complaint was the refresh: on a large state, plan reads every managed object back from the provider before it looks at your change. The workarounds in the issue were all bad trades. Raising -parallelism hits API rate limits. -refresh=false plans against stale state for everything. Splitting the state means maintaining dependencies across roots.
-minimal-refresh only refreshes what your change touches. I wanted to see what that means for drift, so I built the smallest setup that shows it.

The setup

Two local_file resources. No cloud account needed, and the provider's read behaviour is easy to reason about.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}

resource "local_file" "app_config" {
filename = "${path.module}/out/app.conf"
content = "replicas = 2\n"
}

resource "local_file" "feature_flags" {
filename = "${path.module}/out/flags.json"
content = jsonencode({ new_checkout = false })
}
1
2
3
4
5
6
7
$ terraform version
Terraform v1.17.0-rc2
on darwin_arm64

$ terraform apply -auto-approve
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Then the drift. Someone flips a feature flag by hand, outside Terraform:
1
echo '{"new_checkout":true}' > out/flags.json

Phase 1: no config change

With the new flag:
1
2
3
4
5
6
7
$ terraform plan -minimal-refresh -detailed-exitcode; echo $?

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
0
Without it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ terraform plan -detailed-exitcode; echo $?
local_file.feature_flags: Refreshing state... [id=0932faec15d7d4ea4fc3aa3808d19b09bfedac33]
local_file.app_config: Refreshing state... [id=9c9784843f694b0d666d6e2413b3edc46e87e47b]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# local_file.feature_flags will be created
...
Plan: 1 to add, 0 to change, 0 to destroy.
2
Same state, same drift, opposite answers. The first run printed zero Refreshing state... lines and the second printed two. If a scheduled job keys off -detailed-exitcode, the first run is green.

Root cause

PR #38953, which implements the flag, describes the mechanism. For each resource instance, Terraform first plans against the stored state. If that plan is a no-op, the refresh is skipped. If it produces a change, Terraform discards it, refreshes that instance, and plans again.
feature_flags had no config change, so its first plan was a no-op and it was never read from disk. The drift was invisible by design.
The CLI help text in rc2 says so:
1
2
3
4
5
6
-minimal-refresh Skips checking for external changes to objects managed by
this configuration when an initial plan indicates that the
object has not changed since the most recent Terraform apply.
This can potentially make planning faster, but at the expense
of possibly planning against a stale record of the remote
system state.

"But the output says it compared real infrastructure"

It does. The closing message is the long-standing no-changes text, and under this flag it describes a comparison that did not happen for the untouched resources. Read "No changes" from a minimal-refresh plan as "no changes in what you modified".

Phase 2: a real PR change next to the drift

Next I changed replicas = 2 to replicas = 3 in app_config and left the hand-edited flags file in place.
1
2
3
4
5
6
7
8
9
10
11
$ terraform plan -minimal-refresh
local_file.app_config: Refreshing state... [id=9c9784843f694b0d666d6e2413b3edc46e87e47b]
# local_file.app_config must be replaced
Plan: 1 to add, 0 to change, 1 to destroy.

$ terraform plan
local_file.feature_flags: Refreshing state... [id=0932faec15d7d4ea4fc3aa3808d19b09bfedac33]
local_file.app_config: Refreshing state... [id=9c9784843f694b0d666d6e2413b3edc46e87e47b]
# local_file.app_config must be replaced
# local_file.feature_flags will be created
Plan: 2 to add, 0 to change, 1 to destroy.
The changed resource got refreshed and planned correctly. The drifted neighbour was not mentioned. Output trimmed to the lines that differ.

Root cause

From the PR notes, each instance decides in isolation, so refreshing one resource does not refresh the resources that depend on it. Two more cases are fixed by design: a provider schema upgrade always triggers a refresh, and destroy plans plus deposed or orphaned instances never refresh under this flag.
ResourceRefreshed with -minimal-refresh?
Changed by the configyes, after an initial no-refresh plan
Unchanged but driftedno
Depends on a changed resourceno
Schema upgradedalways
Destroy plan, deposed, orphanednever
The Terraform maintainer answered the drift question directly in the PR thread: "if you never update your state (for example, if you always ran with this flag), then you'll never detect any drift until you change your configuration."

Fix: split the PR plan from the drift check

The flag is doing what it says. The problem is a pipeline with one plan job that serves as both the review gate and the drift detector. Until 1.17 every plan was a full refresh, so that worked by accident.
Pull request plan, for reviewers:
1
terraform plan -minimal-refresh -out=tfplan
Scheduled plan, for drift:
1
2
3
4
5
6
7
terraform plan -detailed-exitcode -lock=false
rc=$?
if [ "$rc" -eq 2 ]; then
echo "drift detected"
exit 1
fi
exit "$rc"
Terraform refuses the combinations that would blur the two. Both errors are from rc2:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ terraform plan -minimal-refresh -refresh=false

Error: Incompatible refresh options

The -minimal-refresh and -refresh=false options are mutually-exclusive,
because -minimal-refresh only affects whether Terraform refreshes.

$ terraform plan -minimal-refresh -refresh-only

Error: Incompatible plan mode options

The -minimal-refresh option can only be used in normal or destroy planning
modes, but the current mode is RefreshOnlyMode.
terraform refresh rejects the flag too.
Warning
Do not put -minimal-refresh in TF_CLI_ARGS_plan or any shared environment that the drift job also reads. On HCP Terraform and Terraform Enterprise, the PR thread says support for the flag alongside background drift detection is still in progress.

Honest trade-offs

  • No speed number. My test has two local files and plans in about a tenth of a second either way. HashiCorp has not published a speed-up figure either. The benefit scales with how many resources a refresh reads and how slow the provider API is, so measure it on your own state before you quote anything.
  • The setup is synthetic. local_file is convenient because its read removes the resource from state when the content no longer matches, so the drift shows up as a create. A cloud resource with a modified attribute would show as an update instead. The refresh logic that decides whether drift is seen at all is the same.
  • rc, not GA. Behaviour can still change before 1.17.0. I would wait for GA before rolling it into shared pipeline templates.
  • What I missed at first. I assumed a refreshed resource would pull its dependents into the refresh. It doesn't, and the PR says so in its first bullet.

Summary

-minimal-refresh makes the PR plan answer a narrower question: what will this change do. That is a good question for a PR plan, and a bad one for a drift check.
Before you adopt it, make sure drift detection lives in its own scheduled full-refresh job. Then the flag is a free win on review latency. Without that job, it quietly turns your only drift check off, and the output will tell you everything matches.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article