Quick answer: Pin every provider in the required_providers block with a pessimistic constraint such as version = "~> 5.0", commit the .terraform.lock.hcl file so every machine uses the identical build, and upgrade deliberately with terraform init -upgrade after reading the changelog and testing in a non-production environment. Unpinned providers are the most common cause of “nothing changed but the plan wants to replace everything”.
This guide explains how Terraform resolves provider versions, how to read and write version constraints, what the lock file does and why you must commit it, and a safe step-by-step upgrade process β including how to handle major version jumps like AWS provider 4.x to 5.x. You will also learn how to automate upgrade pull requests and the mistakes that bite teams in production.
Why provider versions matter so much
A provider is the code that translates your HCL into API calls. When the provider changes, behaviour changes β even if your .tf files are byte-for-byte identical. New provider versions bring:
- New resources and arguments as the cloud adds services.
- Bug and security fixes, sometimes affecting how existing resources are read.
- Changed defaults and deprecations, which can turn a no-op plan into a replacement.
- Breaking changes in major versions, such as renamed arguments or resources split into several new ones.
If two engineers run the same code with different provider versions, they can get different plans. Version management is how you prevent that.
Where to declare versions (and where not to)
Older tutorials β including the original version of this article β show version inside the provider block. That syntax was deprecated in Terraform 0.13 and removed in 1.x. The correct location is required_providers:
terraform {
required_version = ">= 1.6.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "aws" {
region = "ap-south-1"
# no version argument here
}
The required_version line pins the Terraform CLI itself, which is just as important β a teammate on Terraform 1.3 cannot use features your code relies on from 1.9. For a refresher on how provider blocks fit into the rest of a configuration, see Terraform Core Concepts and Syntax: A Beginner’s Guide.
Understanding version constraint operators
Providers follow semantic versioning: MAJOR.MINOR.PATCH. Breaking changes only arrive in a new major version, so most constraints are designed to allow minor and patch updates while blocking the next major.
| Constraint | Allows | Blocks | Typical use |
|---|---|---|---|
= 5.62.0 or 5.62.0 |
Exactly 5.62.0 | Everything else | Reproducing a bug; rarely in modules |
~> 5.62 |
5.62.0 up to 5.x (any later minor) | 6.0.0 | Root modules β recommended default |
~> 5.62.0 |
5.62.0 up to 5.62.x (patches only) | 5.63.0 | Very conservative production setups |
>= 5.0, < 6.0 |
Any 5.x | 6.0.0 | Equivalent to ~> 5.0, more explicit |
>= 4.50 |
Anything from 4.50 onwards | Nothing above | Shared modules that must work with many root configs |
| (none) | Latest release at init time | Nothing | Never in real projects |
A useful rule: root modules pin tightly, reusable modules pin loosely. A module that demands ~> 5.62.0 will conflict with any root module wanting 5.63; a module that says >= 5.0 lets the root decide.
The lock file: your reproducibility guarantee
Constraints express what is acceptable. The .terraform.lock.hcl file, written by terraform init, records what was actually selected along with checksums:
# .terraform.lock.hcl (generated β do not edit by hand)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.67.0"
constraints = "~> 5.60"
hashes = [
"h1:3b1b2f5d...",
"zh:0a9d6c7e...",
]
}
Commit this file. With it, init on your laptop, your colleague’s laptop and the CI runner all install 5.67.0, even if 5.70.0 was released yesterday. Without it, every machine picks the newest version matching the constraint and your team drifts apart. If your team works across operating systems, record hashes for every platform so verification works on each:
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64 \
-platform=windows_amd64
A safe upgrade process, step by step
- Check what you have.
terraform versionlists Terraform and every installed provider version;terraform providersshows the constraints each module imposes. - Read the changelog. Every HashiCorp provider publishes a CHANGELOG on GitHub and an upgrade guide for major versions. Search for the resources you use.
- Widen the constraint. Change
~> 4.0to~> 5.0in a branch. - Upgrade and relock. Run
terraform init -upgrade. This fetches the newest version within the new constraint and rewrites the lock file. - Plan against a non-production environment. A clean upgrade produces “No changes”. Replacements or unexpected updates mean a changed default or renamed argument β fix the code before going further.
- Fix deprecations. Warnings in plan output tell you what will break in the next major release; clearing them now makes the next upgrade cheap.
- Review and merge. The pull request should include the constraint change, the lock file diff and the plan output.
- Roll out through environments. Apply to dev, then staging, then production, with a plan check at each stage.
For major jumps, HashiCorp’s upgrade guides are detailed and worth reading end to end. The AWS 4.x to 5.x move, for example, removed several deprecated S3 arguments and changed default tag behaviour, and many teams needed small code edits before the plan came back clean.
Automating upgrade pull requests
Manually checking for provider releases does not scale. Dependabot (GitHub) and Renovate both understand Terraform: they watch the registry, open a pull request that bumps the constraint and lock file, and your pipeline’s plan job shows the impact. A minimal Dependabot configuration:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "terraform"
directory: "/infra/envs/prod"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Reviewing a weekly bot PR with a plan attached is far less painful than discovering you are three major versions behind when a security fix lands.
Seven versioning mistakes to avoid
- No constraint at all. Every
initgets the latest release; plans differ between machines and over time. - Not committing
.terraform.lock.hcl. Constraints alone do not guarantee reproducibility. - Using the old
versionargument in theproviderblock. Terraform 1.x rejects it; move it torequired_providers. - Exact pins in shared modules. They cause constraint conflicts for everyone who uses the module.
- Running
init -upgradedirectly in production. Upgrade in a branch, plan in dev, then promote. - Ignoring deprecation warnings. They are tomorrow’s hard errors.
- Jumping several major versions at once. Upgrade one major at a time; each upgrade guide assumes you start from the previous major.
Frequently asked questions
Does terraform init automatically upgrade providers?
No. Plain init respects the lock file and installs the recorded version. Only init -upgrade looks for newer versions within your constraints and rewrites the lock file.
What is the difference between ~> 5.0 and ~> 5.0.0?
~> 5.0 allows any 5.x (5.1, 5.60, 5.99). ~> 5.0.0 allows only 5.0.x patch releases. The operator permits the rightmost specified component to increase.
Can I downgrade a provider?
Yes. Tighten the constraint to the older version, run terraform init -upgrade and check the plan. Downgrading is risky if the newer provider already wrote state attributes the older one does not understand, so test carefully.
Key takeaways
- Declare versions in
required_providers, never in theproviderblock. - Use
~>to allow safe minor and patch updates while blocking breaking majors. - Commit the lock file and record hashes for every platform your team uses.
- Upgrade in a branch with
init -upgrade, read the changelog, plan in dev and promote through environments. - Let Dependabot or Renovate propose upgrades so you stay current without surprises.
Want to run Terraform upgrades confidently in a real production environment? Our DevOps course covers Terraform best practices, CI/CD and cloud operations with live projects, mentor support and placement assistance. For practical demos, subscribe to our YouTube channel.



