TerraformXXIV · Upgrading Terraform, Providers, and ModulesProduction Terraform
Upgrading Providers Safely
What you'll learn
- Bump the constraint in `required_providers` deliberately
- Refresh `.terraform.lock.hcl` with `terraform init -upgrade` and review the diff
- Read the provider changelog and identify breaking changes before the plan
- Recognise the production cost of a major provider version bump
- Apply the lock for every platform the team uses
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
Providers are the plugins that talk to the APIs your estate depends on. The provider upgrade is the most common upgrade in production Terraform, and the most common source of unintended changes. A provider that introduces a new default for a popular attribute can rewrite hundreds of plans across a fleet the next morning.
The lesson teaches the upgrade as a four-step procedure, the changelog discipline that prevents surprise, and the cost of a breaking provider version that the team did not plan for.
Two layers, one upgrade
A provider upgrade touches two files:
versions.tf (the constraint in required_providers)
.terraform.lock.hcl (the resolved version and hashes)
Both must change together. A constraint bump without a lock
refresh produces an inconsistent state; a lock refresh without a
constraint bump produces a no-op that confuses the next engineer
who runs terraform init -upgrade.
Step 1: read the changelog
The provider’s GitHub releases page is the authoritative source. Three sections to look for:
1. BREAKING CHANGES. Documented under the major version bump. Renamed attributes, removed arguments, changed defaults. Every breaking change is the team’s work; the upgrade is incomplete until the configuration has been updated to match.
2. NEW FEATURES. New resources, new arguments, new data sources. Backwards compatible; nothing in the configuration changes unless the team opts in.
3. BUG FIXES AND DEPENDENCY UPDATES. The most common release. Bug fixes are usually safe; dependency updates can transitively change provider behaviour in subtle ways.
The cost of skipping the changelog is the surprise plan. The team sees a hundred unintended changes, applies blindly, and discovers three weeks later that an attribute default changed and a resource was recreated.
Step 2: bump the constraint
The constraint lives in versions.tf (or whatever file the team
uses for the terraform block):
terraform {
required_version = ">= 1.9.8, < 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.80"
}
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.0, < 5.0"
}
}
}
The constraint shape matters.
| Operator | Example | What it admits |
|---|---|---|
= 5.80.0 | exact pin | Only 5.80.0 |
~> 5.80 | pessimistic | 5.80 and any later 5.x; not 6.0 |
~> 5.80.0 | pessimistic | 5.80.x patches only |
>= 5.80, < 6.0 | explicit range | 5.80 and any later 5.x; not 6.0 |
>= 5.0, < 6.0 | major window | Any 5.x |
>= 5.0 | floor only | Any 5.x or later, including 6.x |
The default for production. ~> <major>.<minor>.<patch>
admits patches within the chosen minor and excludes both the
minor and the major bump. This is the right shape when the team
tracks the provider actively and wants patches without surprise.
Dropping the patch component — ~> <major>.<minor> — lets the
minor move as well, because ~> allows only the rightmost
component you name to increment.
When to use the major window. >= 5.0, < 6.0 is the right
shape when the team wants to delay a major bump indefinitely
and is happy to receive all patches and minors within the major.
When to use the floor. >= 5.0 admits 6.0. This is rare in
production because it lets a major bump land without warning.
The provider upgrade is the moment to revisit the constraint. The bump is deliberate; the constraint shape is the team’s contract with the registry.
Step 3: refresh the lock file
# READ-ONLY if the lock is up to date;
# CONFIGURATION if it changes.
terraform init -upgrade
init -upgrade does four things:
- Re-reads the constraint in
required_providers. - Resolves the latest version that satisfies the constraint.
- Downloads the new provider binary for the current platform.
- Writes the new resolved version and hash to
.terraform.lock.hcl.
The lock file change is the audit trail. Code review sees the constraint change and the lock change in the same pull request.
For multi-platform teams, lock every platform the team uses:
# CONFIGURATION: add hashes for every platform
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_amd64 \
-platform=darwin_arm64
A CI runner on Linux needs the linux_amd64 hash; a developer
on macOS needs darwin_arm64. Missing platforms cause noisy CI
diffs because the runner downloads the missing hash and writes
a new lock entry.
Step 4: read the plan
The plan is the diagnostic. Three patterns to look for:
# READ-ONLY
terraform plan -out=tfplan
terraform show -json tfplan | jq '.resource_changes[] |
{address: .address, actions: .change.actions}'
Pattern A: plan is empty. The upgrade is a no-op. Safe to apply. The lock file change is the only effect on disk.
Pattern B: plan shows new attributes. A new attribute the provider records in state. The underlying resource is unchanged. Safe to apply; review for sensitive values, but nothing was recreated.
Pattern C: plan shows replacements. Terraform proposes to destroy and recreate a resource. Stop. This is a breaking change in the schema or a default change that the upgrade introduces. Read the changelog again; identify the change; decide whether to proceed.
Pattern D: plan fails with “Unsupported attribute”. The new provider version removed or renamed an attribute the configuration uses. The fix is to update the configuration to use the new attribute name, or to delay the upgrade.
The plan review module covers the diagnostic in detail. The summary here: if the plan shows anything other than empty or new attributes, the upgrade is not a no-op, and the team decides whether the changes are intended.
The cost of a breaking provider version
A major provider version bump is allowed to break the schema. The cost is real.
A. Configuration rewrite. Every reference to a renamed attribute has to be updated. The team may have dozens of modules and hundreds of resources.
B. State migration. Some breaks are visible in the
configuration. Some are visible only in state. A provider may
change how an attribute is stored; the next plan shows every
resource as changing because the storage format changed. The
fix is a state upgrade via the provider’s migration story.
C. Unintended replacements. A default change can rewrite production. The team wakes up to a plan that proposes to recreate every database, every load balancer, every IAM role. Recovery is to revert the lock file, set the attribute explicitly, and try again.
D. Time. A major bump takes days, not hours. The team has to read the changelog, update the configuration, migrate state, and apply. The work is not optional; the alternative is to stay on the old major and accept the security risk.
E. Blast radius. A breaking provider is a fleet-wide event. Every working directory that uses the provider has to be touched. The team plans the change the way they would plan a Terraform Core major bump: dedicated runbook, staged rollout, post-upgrade review.
Production failure modes
Five failure modes recur.
1. Lock file not refreshed. Symptom: the constraint admits
the new version but the lock pins the old; init reports a
lock mismatch and the operator runs init -upgrade without
understanding why. Recovery: refresh the lock deliberately,
review the diff, commit both files.
2. Changelog not read. Symptom: a major bump lands; the plan shows 200 changes because the schema was rewritten; recovery is to revert the lock and pin the old major indefinitely. Recovery: read the changelog next time.
3. Lock file without platform hashes. Symptom: macOS
developer commits a lock with only darwin_arm64; Linux CI
cannot find a matching hash and downloads a new one, producing
a noisy diff. Recovery: lock every platform the team uses.
4. Provider yanked from registry. Symptom: the lock pins a
version HashiCorp has since yanked; init fails because the
registry no longer serves it. Recovery: bump the constraint
to the next version; re-lock.
5. State schema migration silently lost. Symptom: a major
provider bump changes how an attribute is stored; the next
plan shows every resource changing because the storage
format differs; the team accepts and applies, recreating
resources that did not need recreation. Recovery: revert the
lock, set attributes explicitly to force the old storage
shape, or follow the provider’s migration guide if one
exists.
Operational guidance
- One provider, one constraint, one lock file. The team pins the constraint, refreshes the lock, and commits both in one pull request.
- Read the changelog first. Especially for major bumps. The minor bumps are usually safe but the changelog is short; skim it anyway.
- Lock for every platform.
terraform providers lockwith every-platformthe team uses. - Apply through the plan. The saved plan is the audit
trail.
terraform apply tfplanrejects the apply if the state has drifted since the plan. - Major bumps are projects. Dedicated runbook, staged rollout, post-upgrade review. The bump is allowed to break; the team is responsible for the breakage.
Security and performance
- Hash integrity. The lock file records the SHA-256 hashes of every provider binary. Terraform re-verifies the hash on every run. A tampered binary produces a hash mismatch and the apply stops.
- Provider yanked. The registry can yank a release after
publication. The lock file pins the yanked version;
initfails; the team bumps and re-locks. - Performance. Provider performance changes between versions. A new release can speed up a slow resource or introduce a regression. The CI run is the early-warning system.
What comes next
The next lesson is on upgrading internal modules safely — the source-ref bump, the module lock entry, and the test discipline for changes the team owns.
Verification
# READ-ONLY: confirm the provider source and constraint
grep -A1 'hashicorp/aws' versions.tf
aws = {
source = "hashicorp/aws"
version = "~> 5.80"
}
# READ-ONLY: confirm the lock file resolved version
grep -A2 'hashicorp/aws' .terraform.lock.hcl
provider "registry.terraform.io/hashicorp/aws" {
version = "5.80.0"
hashes = [
# READ-ONLY: confirm the plan is a no-op or shows only safe changes
terraform plan -out=tfplan
terraform show -json tfplan | jq '[.resource_changes[] |
select(.change.actions | tostring != "[\"no-op\"]")] | length'
0
A count of zero means the plan is a no-op. Anything above zero is a change that needs review.
Knowledge check · 7 questions
Q1. Which two files must change together when a provider is upgraded?
Q2. Reading the provider changelog before a major version bump is optional if the team has good test coverage.
Q3. After a provider upgrade, terraform plan shows that three resources will be destroyed and recreated. What is the right action?
Q4. A team upgrades from hashicorp/aws 5.79 to 5.80. The plan is empty. What does that mean?
Q5. Which of the following are production risks of skipping the provider changelog before an upgrade? (Select all that apply.)
Q6. A developer upgrades hashicorp/aws from 5.80 to 5.99 without reading the changelog. The plan shows 200 changes because the provider introduced new default values for several attributes. What is the right action?
Q7. Which command refreshes the lock file after a provider constraint bump?
Passing score: 75%. Answers are checked in this browser.