A teammate widened the AWS provider constraint in `required_providers` from `~> 5.30` to `~> 5.80`, but CI's `terraform plan` still runs provider 5.31.0. Why, and how do you move it forward safely?
answer
- the constraint widened, the decision did not
- init reuses a still-valid recorded version
- one flag re-resolves the selection
- the diff belongs in the pull request
- never let the pipeline upgrade itself
basics
~20 sBecause .terraform.lock.hcl still records 5.31.0, and 5.31.0 still satisfies the widened constraint, so terraform init reuses it. Run terraform init -upgrade locally, review the lock-file diff and the resulting plan, then commit the updated lock file.
solid answer
~50 sWidening a constraint does not select a new version — it only widens what *may* be selected. `terraform init` reuses the version recorded in `.terraform.lock.hcl` whenever that version still satisfies the constraints, and 5.31.0 satisfies `~> 5.80`... no: it does not, and that distinction is the whole answer. If the locked version still satisfies the new constraint, init keeps it and you need `terraform init -upgrade` to re-resolve; if the new constraint's floor is *above* the locked version, init is forced to re-resolve on its own and rewrites the lock file. So the diagnosis is to read the constraint and the lock entry together. The safe upgrade is deliberate: run `terraform init -upgrade` locally on a branch, commit the rewritten lock file, run `plan` in a non-production workspace first to see whether the new provider proposes unexpected diffs, then promote. Never make CI upgrade itself.
code
bash · 13 lines# Diagnose: what is actually locked?
grep -A2 'hashicorp/aws' .terraform.lock.hcl
# Move forward deliberately, on a branch
terraform init -upgrade
git diff .terraform.lock.hcl
terraform plan -out=tfplan
# Commit the rewritten lock file with the constraint change
git add .terraform.lock.hcl main.tf
# And make CI refuse to do this silently on its own
terraform init -lockfile=readonlygo deeper
Know that changing the constraint is not the same as changing the version, and that terraform init -upgrade is the command that re-selects providers and updates the lock file.
Explain the rule init follows — reuse the recorded version whenever it still satisfies every constraint, otherwise re-resolve — and predict which of those two paths a given constraint edit triggers.
Show the production process: upgrade on a branch, commit the lock-file diff, exercise the plan in a non-production environment first, and expect provider upgrades alone to move a plan.
Own the upgrade cadence across the estate — small frequent bumps over big jumps, -lockfile=readonly as the pipeline guard, and how a bad provider release is detected and rolled back.
## The mechanic behind the symptom Two things decide which provider build runs: the constraint in `required_providers`, and the entry in `.terraform.lock.hcl`. The constraint is a range; the lock file is a decision already made. On `terraform init` Terraform asks a single question — *does the recorded version still satisfy every constraint in the configuration?* If yes, it installs exactly that version and does not consult the registry for anything newer. If no, it must re-resolve, picks the newest release satisfying the constraints, and rewrites the entry. That gives you a clean diagnosis. A widened constraint whose floor stays below the locked version — `~> 5.30` to `~> 5.90` when 5.31.0 is locked, or `~> 5.0` to `>= 5.0` — leaves the lock valid, so nothing moves and CI keeps running the old plugin. A constraint whose floor rises above the locked version forces re-resolution on the next init, and the lock file changes underneath you. If your CI checks out the repo fresh each run, this is deterministic and repeatable, which is why "CI is still on the old provider" is a configuration observation, not a flake. ## The wrong explanations to rule out - *"CI cached `.terraform/`."* Possible on a persistent runner, but even a warm cache is governed by the lock entry — the fix is the same and the cache is not the cause. - *"The constraint change needs an apply."* No. Provider selection happens entirely at init. - *"The lock file pins the constraint."* The `constraints` field in the lock is a record for humans; the configuration is authoritative for what is allowed. ## Doing the upgrade properly ```bash # on a branch, in the root module terraform init -upgrade git diff .terraform.lock.hcl # version bump + new hashes terraform plan ``` `-upgrade` tells init to ignore the recorded selections and choose the newest release allowed by the constraints, rewriting the lock file including its hashes. Commit that file: the pull request now shows both the constraint change and the version actually selected, and a reviewer can see 5.31.0 to 5.92.0 rather than an unbounded "newer 5.x". Then judge the blast radius. A provider upgrade can change a plan even with no configuration change: new attributes appear with computed defaults, deprecated attributes start being written differently, or a fixed bug means an attribute that was previously ignored is now managed. Run `plan` against a non-production state first and read the diff before promoting the same lock file to production. Where a major provider version is involved, expect breaking changes and read the upgrade guide — a major bump is a project, not a lock-file line. ## Making the pipeline honest The complementary control is `terraform init -lockfile=readonly` in CI. Terraform then refuses to modify the lock file and fails instead, so a constraint change that would silently force a re-resolution becomes a build failure telling you to run `-upgrade` locally and commit the result. That converts the exact class of surprise in this question — "which provider is production actually running?" — into something visible in review. The anti-pattern is putting `-upgrade` in the CI init step permanently. It makes every pipeline run re-resolve against the registry, so the provider version becomes a function of *when* the pipeline ran; two runs of the same commit can use different plugins, and a bad provider release reaches production with no code change to point at. Upgrades belong on a branch, in a diff, with a plan attached.
- Is `terraform init -upgrade` safe to run against a production state?The command itself only installs plugins — it changes no infrastructure. The risk is downstream: the newer provider can produce a different plan for unchanged configuration, so applying straight after an upgrade mixes two changes. Run it on a branch, read the plan in a non-production environment, then promote the same committed lock file so production runs the build you already exercised.
- Why not just put -upgrade in the CI init step so the team always gets fixes?Because it makes the provider version depend on the wall-clock time of the run rather than on the commit. Two pipeline runs of identical code can use different plugins, a bad release reaches production with no diff to blame, and the lock file's integrity guarantee becomes decorative. Upgrades should be a reviewed commit; `-lockfile=readonly` in CI enforces that.
- After -upgrade, plan shows changes to resources nobody edited. What is going on?Almost always the new provider's schema or defaults changed — an attribute that is now computed and populated, a deprecated field written differently, or a fixed bug that means real drift is finally being reported. Read the provider changelog for the range you jumped, confirm the diff is benign against a non-production state, and use it as an argument for smaller, more frequent provider bumps.
saying these in an interview costs you the question
- Assuming a widened constraint auto-selects a newer version
- Blaming a CI plugin cache instead of the lock entry
- Putting -upgrade permanently in the CI init step
- Deleting the lock file to force a fresh resolution
- Applying immediately after an upgrade without reading the plan