Terraform 1.17.0 landed on 21 October 2026 with -minimal-refresh, the flag that closes issue #35290 (terraform plan -light, 207 reactions) after two years. The short version: it refreshes only the resource instances whose plan already shows a change, so a drift job running terraform plan -minimal-refresh -detailed-exitcode returns 0 on infrastructure that has moved underneath you. Use it pre-merge, keep the nightly drift run on a full refresh, and spend five minutes on the other eleven items below before you bump the version in CI. The last Terraform release that cost this site an afternoon was the 1.16.3 download served as binary/octet-stream, and that one at least failed loudly.

The tips

  1. Do not put -minimal-refresh in the job that detects drift. Terraform plans against stored state first and refreshes only the instances whose plan shows a change, so an externally modified resource with no config delta is never read back and -detailed-exitcode returns 0 instead of 2. The v1.17 plan reference states the cause outright: "this option causes Terraform to ignore external changes to resources that have no planned changes." A test on 1.17.0-rc2 with two local_file resources reproduced it: a hand-edited out/flags.json printed No changes. Your infrastructure matches the configuration. and exited 0, while a plain plan refreshed both and exited 2. A pipeline that reports success because it never looked has precedent in CI, see the Actions runner that exits 0 with .runner deleted.
# fast pre-merge plan
terraform plan -minimal-refresh -detailed-exitcode   # 0 = no changes it bothered to look for

# nightly drift job: leave it alone
terraform plan -refresh-only -detailed-exitcode      # 2 = something moved under you
plan -minimal-refreshchange vs stored state?skip refresh, report no-oprefresh this instance onlyre-plan the instancedrift elsewhere stays invisible"no""yes"
  1. Audit your apply invocations, not only plan. The apply command reference notes that without a saved plan file, apply "supports all planning modes and planning options available for terraform plan". Any wrapper that templates a single TF_ARGS string into both commands hands -minimal-refresh to apply, where a stale read stops being a reporting problem and starts writing. Grep your Atlantis config, Terragrunt hooks and Makefiles for the variable, not for the flag.
  2. Expect the speed win to vanish on provider upgrade day. The plan docs carve out one exception: "Terraform refreshes a resource regardless of the plan result if the resource schema version changes." A provider major bump that moves schema versions re-reads every instance, so a plan-duration SLO or a tight pipeline timeout built on the fast path will page someone at the worst moment.
  3. Resolve the flag conflicts before rollout. On 1.17.0-rc2, -minimal-refresh combined with -refresh=false or -refresh-only errors out, and terraform refresh rejects the flag entirely. Wrappers already carrying -refresh=false fail at argument parsing, which is the outcome you want: the failure in tip 1 is the quiet one.
  4. A variable used in required_providers must be declared const = true. PR #39153 made variables and locals valid inside provider requirements, and the variable block reference sets the constraint: const "allows the variable to be used when Terraform loads configuration, such as during a Terraform init operation," and such a variable "can only contain a known, constant value. It cannot depend on dynamic results from a plan operation." The same rule covers module source and version.
variable "provider_host" {
  type  = string
  const = true
}

terraform {
  required_providers {
    aws = {
      source  = "${var.provider_host}/hashicorp/aws"
      version = "~> 6.0"
    }
  }
}
  1. Move the registry host into a variable and leave the version literal. const variables cannot be ephemeral, per the #39362 implementation, and dependency bots parse HCL rather than evaluate it. lucasew made that argument on the predecessor PR #38405: static versions are "useful for stuff like Renovate to work well." A per-environment version expression also turns one .terraform.lock.hcl into a cross-environment argument, since -lockfile=readonly conflicts with -upgrade. Variable-ise the host for an airgapped mirror and keep version as a string Renovate can bump.
  2. Grep for log( and pow( before you upgrade. Both now raise an error rather than returning infinity: PR #39304 added fmt.Errorf("result is not a number"), #38912 stopped the outright panic on NaN, and a test case expecting -Inf was deleted. Config computing a logarithm over a count that can reach zero used to emit a plausible-looking plan and now fails the run.
terraform console <<<'log(0, 10)'   # 1.16: -Inf   |   1.17: error, "result is not a number"
grep -rnE '\b(log|pow)\(' --include='*.tf' .
  1. Terraform 1.17 is built with Go 1.26.10 and your providers are not. The 1.17.0 release notes tie that build to CVE-2026-111943: the toolchain no longer trusts the compromised eMudhra intermediates. Each provider is a separate binary compiled by its maintainer with its own Go version, so terraform init can start failing against an internal registry while a provider keeps reaching the same internal API, or the reverse after the next provider release. The block is keyed on SPKI hashes compiled into crypto/x509, so adding the CA to the system trust store changes nothing, which is why the error names no CA at all.
TF_LOG=trace terraform init 2>&1 | grep -i 'x509\|certificate'
go version -m .terraform/providers/*/*/*/*/terraform-provider-* | grep -E '^\s+mod|go1\.'

On Brotli patch day last week I probed the handshake instead of checking the inventory, on an end-of-life Node 20 host whose release number implied nothing useful and whose decoder returned errno=-9 for the unpatched path. That write-up is the php2shell and Node Brotli post, and the habit transfers directly: run the command, do not trust the version string.

  1. Budget for new warnings on applies that use ephemeral resources. PR #38989 fixed a dropped-diagnostics bug where the renewal path called r.renewDiags.Append(diags) without assigning the result, discarding every warning and error raised while renewing an ephemeral resource instance. From 1.17 those surface when the instance closes, and a renewal error can mark an instance expired. Fresh noise on a long apply holding short-lived credentials means renewal was already failing and nobody was being told.
  2. Delete the duplicated -var-file from your terraform show step. Since 1.15 added -var to every command, show expected variables to be supplied again when reading a saved plan. #39362 makes it read the values stored in the plan "regardless of any current variable values or defaults," so policy and cost-estimation stages that re-mount a tfvars file purely to render JSON can drop it.
terraform plan -out=tfplan
terraform show -json tfplan > plan.json   # 1.17: no -var-file needed here
  1. Terraform Policy is GA, and -policies now insists on a directory. PR #38970 removed the experimental-build and -allow-experimental-features requirement for the -policies flag on plan, apply and query. Pointing it at a single .tfpolicy.hcl file used to pass validation and fail later inside the policy engine; #39238 makes it fail fast with an "Invalid policy path" error naming the path and exit 1. Keep the local loop honest with tfpolicy validate --policies=./policies and tfpolicy test --policies=./policies.
  2. Stop regexing init output, and bump terraform-json. terraform version -json now emits a format_version field, tested with the value "1.0" in the terraform-json commit that added FormatVersion, and strict Go decoders that reject unknown fields break on it. PR #38918 separately enriches init and state-migrate messages with provider versions and reshapes the JSON reusing_previous_version_info payload, so a CI step scraping init text for a version string is now scraping a different sentence.

Wrap-up

Decide per job whether that run is allowed to be blind. -minimal-refresh belongs in the pre-merge plan where speed is the product, and the pipeline answering "has anything changed in production?" keeps paying for the full refresh, otherwise the 1.17 upgrade converts your drift detector into a machine that agrees with you. The rest of the list is grep work you can finish in one sitting: log(, pow(, -var-file on show, any .tfpolicy.hcl path passed to -policies, and the Go version baked into each provider binary.