Terraform Provider Version Constraints and Lock File

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/local provider 2.5.0 and 2.9.0
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user
Scope Provider version constraints in required_providers, .terraform.lock.hcl, terraform init vs init -upgrade, terraform providers lock, checksums, Git workflow, and common lock errors. Does not cover provider aliases, authentication, full terraform init reference, module version pinning, or remote backends.
Related guides Terraform lab environment on Ubuntu
Install Terraform on Ubuntu
Terraform HCL syntax
Terraform Associate certification course

A single terraform block often lists both required_version and required_providers with a version argument:

hcl
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

Which line controls the Terraform CLI, and which line controls the provider plugin? Teams confuse these three layers constantly. This guide separates Terraform CLI version, provider version constraints, and .terraform.lock.hcl, then walks through init, upgrade, multi-platform locking, and recovery on Ubuntu.

NOTE
Use the Terraform lab environment on Ubuntu and install Terraform if terraform version is not available yet. Examples use the hashicorp/local provider so you do not need cloud credentials.

Terraform CLI version vs provider version

Three different settings answer three different questions:

Setting or file Controls
required_version Terraform CLI binary compatibility
required_providers version Allowed provider versions (constraint range)
.terraform.lock.hcl Selected provider version and package checksums

required_version never pins a provider. required_providers.version never pins the Terraform binary. The lock file never replaces either constraint — it records what terraform init already chose within those rules.

Keep that table in mind when you read init output, review pull requests that touch .terraform.lock.hcl, or debug version errors.


Lab directory for this guide

Create a dedicated working directory under your lab tree:

bash
mkdir -p ~/terraform-labs/terraform-provider-version-lock-file

Move into it — the commands below assume this path:

bash
cd ~/terraform-labs/terraform-provider-version-lock-file

Add a minimal resource so terraform plan has something to evaluate later:

hcl
resource "local_file" "demo" {
  content  = "lock lab"
  filename = "${path.module}/demo.txt"
}

Save that block in main.tf. Put version metadata in versions.tf:

hcl
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

The source address tells Terraform which registry namespace owns the plugin. The version string is a constraint, not an exact pin unless you use =.


Configure a provider version constraint

The version argument inside required_providers accepts one or more constraint strings. Terraform evaluates them when it resolves provider packages during terraform init.

Run init to download the provider and create the lock file:

bash
terraform init

Sample output:

output
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

Terraform selected hashicorp/local v2.9.0 — the newest release that satisfies ~> 2.5 at the time of this lab run. Your registry may offer a different latest 2.x version later; the constraint syntax stays the same.


Terraform version constraint operators

Provider constraints use the same operator vocabulary Terraform documents for module versions. Each operator defines which releases are acceptable:

Operator Example Acceptable versions (illustration)
= = 2.5.0 Exactly 2.5.0
!= != 2.4.0 Any release except 2.4.0
> > 2.4.0 2.4.1, 2.5.0, 3.0.0, …
>= >= 2.4.0 2.4.0, 2.9.0, 3.0.0, …
< < 2.6.0 2.5.9, 2.0.0, … (not 2.6.0)
<= <= 2.5.0 2.5.0 and older
~> ~> 2.5 2.5.0 through 2.x (not 3.0.0)

The pessimistic constraint operator ~> is the one teams reach for most often, and the one that causes the most mistakes if you guess the range.

With ~>, Terraform allows the right-most specified component to increase while keeping the preceding components within the same compatibility range:

  • ~> 2.5 means >= 2.5.0 and < 3.0.0 — any 2.x at or above 2.5.0.
  • ~> 2.5.1 means >= 2.5.1 and < 2.6.0 — only 2.5.x from 2.5.1 upward.
  • ~> 1.2.3 means >= 1.2.3 and < 1.3.0.

Combine constraints with commas when you need a narrower band, for example >= 2.4.0, < 2.6.0.


What is .terraform.lock.hcl?

After the first successful init, Terraform writes .terraform.lock.hcl in the module root. List the directory to confirm it appeared:

bash
ls -la .terraform.lock.hcl

Sample output:

output
-rw-r--r-- 1 user user 1234 Aug 11 18:45 .terraform.lock.hcl

Inspect the provider stanza:

bash
sed -n '1,20p' .terraform.lock.hcl

Sample output:

output
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/local" {
  version     = "2.9.0"
  constraints = "~> 2.5"
  hashes = [
    "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=",
    "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0",
    ...
  ]
}

The lock file records:

  • Selected versionversion = "2.9.0" is the build init installed.
  • Configured constraintsconstraints = "~> 2.5" mirrors your required_providers block at lock time.
  • Hashes — checksums for provider packages Terraform verified (see the checksum section below).
  • Maintenance — Terraform updates this file during init and terraform providers lock; treat manual hash edits as a last resort.

Version constraint vs locked version

A broad constraint such as version = ">= 2.0" allows many releases. The lock file still records one selected build — for example 2.9.0 — until you change constraints or run an upgrade init.

Run init again without flags to see how Terraform treats an existing lock:

bash
terraform init

Sample output:

output
Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/local from the dependency lock file
- Using previously-installed hashicorp/local v2.9.0

Terraform has been successfully initialized!

Ordinary terraform init reuses the locked version even when the registry publishes a newer release that still satisfies ~> 2.5. That stability is intentional — teammates and CI should not silently pick up a new provider on every init.

To allow Terraform to reconsider versions inside the constraint, pass -upgrade:

bash
terraform init -upgrade

When the locked version is already the newest match, output stays on the same release:

output
- Finding hashicorp/local versions matching "~> 2.5"...
- Using previously-installed hashicorp/local v2.9.0

The upgrade path matters most after you widen a constraint or when a newer provider was published since the lock was written. The end-to-end lab below shows a lock moving from 2.5.0 to 2.9.0.


Should .terraform.lock.hcl be committed to Git?

For shared root configurations, commit the lock file. It gives everyone the same provider selection and checksum set, makes dependency bumps reviewable in pull requests, and lets CI verify package integrity.

Check whether Git sees the new file:

bash
git status --short .terraform.lock.hcl

Sample output on a fresh repo:

output
?? .terraform.lock.hcl

Stage it like any other tracked artifact:

bash
git add .terraform.lock.hcl

Do not commit .terraform/ — only the lock file and your .tf sources belong in version control. The dependency lock file belongs to the overall configuration and is stored in the root module working directory. Reusable child modules therefore generally do not publish their own lock file.


What does the lock file actually lock?

.terraform.lock.hcl tracks provider plugins resolved from provider registries (or mirrors). It does not lock:

  • Remote module version selections from module blocks
  • Terraform CLI version (required_version is enforced separately at init)
  • Backend or workspace configuration

Module versions come from the version argument on module blocks and the module registry. Provider locking and module pinning are related ideas but different mechanisms — module version depth belongs in the modules lesson track.


Provider checksums in .terraform.lock.hcl

Each provider entry includes a hashes list with two prefix styles:

  • zh: — checksum based on the provider distribution ZIP archive.
  • h1: — checksum based on the contents of the provider package.

Terraform compares downloaded packages against these values. If a mirror serves a tampered or wrong artifact, init fails checksum verification instead of running an unexpected binary.

Do not hand-edit hash lines to “fix” init errors. Regenerate checksums with terraform init, terraform init -upgrade, or terraform providers lock after you intentionally change provider versions or platforms.


Pre-populate checksums with terraform providers lock

Teams that develop on Linux laptops and deploy from CI on the same architecture still benefit from an explicit lock refresh when you add platforms. The terraform providers lock command fetches provider packages and writes checksums without running a full init workflow.

From your lab directory with an existing lock file, add checksums for two Linux platforms:

bash
terraform providers lock -platform=linux_amd64 -platform=linux_arm64

Sample output:

output
- Fetching hashicorp/local 2.9.0 for linux_amd64...
- Retrieved hashicorp/local 2.9.0 for linux_amd64 (signed by HashiCorp)
- Fetching hashicorp/local 2.9.0 for linux_arm64...
- Retrieved hashicorp/local 2.9.0 for linux_arm64 (signed by HashiCorp)
- Obtained hashicorp/local checksums for linux_amd64; All checksums for this platform were already tracked in the lock file
- Obtained hashicorp/local checksums for linux_arm64; Additional checksums for this platform are now tracked in the lock file

Success! Terraform has updated the lock file.

Review the changes in .terraform.lock.hcl and then commit to your
version control system to retain the new checksums.

Commit the updated lock file so both platforms can verify downloaded provider packages against checksums already recorded in the lock file.


Upgrade a Terraform provider safely

Treat provider upgrades as a deliberate change, not a side effect of routine init.

1. Inspect constraints in versions.tf (or your required_providers file).

2. Inspect the lock file — note the current version = "..." line.

3. Widen or adjust the constraint if the target release is outside the current range.

4. Run upgrade init:

bash
terraform init -upgrade

5. Review the lock diff — confirm the version line and any new h1: entries match what you expected.

6. Validate and plan before apply:

bash
terraform validate

A successful validate ends with:

output
Success! The configuration is valid.

Then preview infrastructure changes introduced by the new provider build:

bash
terraform plan

Provider upgrades can change default behaviors or schemas even when your .tf files are unchanged. Read the provider changelog, review the plan output, and run apply only after the diff looks correct.


Downgrade or pin an exact provider version

Exact pins use = with a full semantic version:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "= 2.5.0"
    }
  }
}

Run init to lock that build:

bash
terraform init

Sample output:

output
- Installing hashicorp/local v2.5.0...
- Installed hashicorp/local v2.5.0 (signed by HashiCorp)

The lock file now records version = "2.5.0".

Exact pins help when you are reproducing a production incident or holding a known-good build during a provider bug. They also reduce flexibility — every bump requires editing the constraint and running terraform init -upgrade.

If you widen the constraint later — for example from = 2.5.0 to ~> 2.5 — ordinary init keeps 2.5.0 until upgrade init runs:

bash
terraform init

Sample output:

output
- Reusing previous version of hashicorp/local from the dependency lock file

Pull the newer acceptable release with:

bash
terraform init -upgrade

Sample output:

output
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

The lock file version line moves from 2.5.0 to 2.9.0. That before/after pair is the core lesson: constraints define what is allowed; the lock file records what is selected.

For reusable modules, prefer a minimum-compatible provider constraint such as >= 2.4.0. Let the root module apply tighter ~> bounds and own the final provider selection.


required_version constrains the Terraform CLI

required_version belongs in the same terraform block but answers a different question — whether the installed Terraform binary is new enough (or within a supported band).

Example:

hcl
terraform {
  required_version = ">= 1.12.0"
}

If you set an impossible floor, init fails before provider resolution starts:

hcl
terraform {
  required_version = ">= 99.0.0"
}

Run init to see the core version guardrail:

bash
terraform init

Sample output:

output
Error: Unsupported Terraform Core version

  on versions.tf line 2, in terraform:
   2:   required_version = ">= 99.0.0"

This configuration does not support Terraform version 1.15.8. To proceed,
either choose another supported Terraform version or update this version
constraint.

Restore a realistic constraint (>= 1.12.0 on this lab) before continuing. required_version is essential for team consistency, but it is not a provider version mechanism — keep it in the comparison table at the top of this guide, not mixed into provider upgrade runbooks.


End-to-end verification lab

This sequence ties constraint, lock, ordinary init, and upgrade init together. Use a fresh directory so the output is easy to follow:

bash
mkdir -p ~/terraform-labs/terraform-provider-version-lock-file-upgrade

Switch into that directory for the rest of the lab:

bash
cd ~/terraform-labs/terraform-provider-version-lock-file-upgrade

Add the same local_file resource in main.tf as earlier. Start with an exact pin in versions.tf:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "= 2.5.0"
    }
  }
}

Step 1 — first init selects and locks 2.5.0:

bash
terraform init

Step 2 — widen the constraint to ~> 2.5 but do not upgrade yet. Edit versions.tf, then run ordinary init:

bash
terraform init

Terraform reuses 2.5.0 from the lock file even though ~> 2.5 allows newer 2.x releases.

Step 3 — upgrade init picks the newest acceptable 2.x:

bash
terraform init -upgrade

The lock file version changes to 2.9.0 (or whatever latest 2.x your registry offers). Commit that diff, then run terraform validate and terraform plan before any apply in a real environment.

text
broad constraint  →  terraform init  →  lock records selection
change constraint (wider)  →  ordinary init keeps lock
terraform init -upgrade  →  lock updates to newer acceptable release

Common version and lock file errors

Symptom Likely cause Fix
Locked provider does not match configured constraint Lock file records 2.5.0 but constraint is now = 2.4.0 Align constraint with the locked version, or run terraform init -upgrade after fixing the constraint to an allowed range
Provider package checksum mismatch Mirror or cache served a different artifact than the lock file expects Restore a correct mirror, or run terraform providers lock / terraform init -upgrade to refresh hashes; do not edit hashes by hand
Unsupported Terraform Core version required_version excludes your CLI Install a supported Terraform release or relax required_version
No available provider version satisfies constraints Constraint such as = 99.0.0 has no registry release Pick a version that exists on the registry, or widen the constraint
Platform package unavailable Provider does not publish a build for the target OS or architecture Choose a supported provider version or platform
Checksum missing for another platform Lock file does not yet contain sufficient hashes for your platform Run terraform providers lock -platform=... on a networked machine, commit the lock, or fix mirror/offline paths

Locked provider does not match configured constraint

Start from a lock that records 2.5.0, then tighten the constraint to = 2.4.0:

bash
terraform init

Sample output:

output
Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider
hashicorp/local: locked provider registry.terraform.io/hashicorp/local
2.5.0 does not match configured version constraint 2.4.0; must use
terraform init -upgrade to allow selection of new versions

Either restore a compatible constraint or run terraform init -upgrade after setting a constraint that includes the version you want.

Provider package checksum mismatch

When a filesystem mirror serves the wrong file, Terraform refuses to install it. Init with a populated lock file and a bad mirror artifact produces:

bash
terraform init

Sample output:

output
Error: Failed to install provider

Error while installing hashicorp/local v2.9.0: the local package for
registry.terraform.io/hashicorp/local 2.9.0 doesn't match any of the
checksums previously recorded in the dependency lock file (this might be
because the available checksums are for packages targeting different
platforms); for more information:
https://developer.hashicorp.com/terraform/language/files/dependency-lock#checksum-verification

Fix the mirror artifact or regenerate locks from a trusted network path.

No available provider version satisfies constraints

Set version = "= 99.0.0" in required_providers, then init:

bash
terraform init

Sample output:

output
Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider
hashicorp/local: no available releases match the given constraints 99.0.0

Choose a published version or widen the constraint.


Recover from an incorrect lock file

Do not delete .terraform.lock.hcl as a first troubleshooting step. Start with inspection:

  1. Read required_providers constraints in your .tf files.
  2. Read the version and constraints lines in .terraform.lock.hcl.
  3. Decide whether the constraint or the lock is wrong.

Then pick the smallest fix:

  • Constraint typo — fix the version string, run terraform init -upgrade if you need a new selection.
  • Stale lock after intentional bump — run terraform init -upgrade, review the diff, commit.
  • Missing platform checksums — run terraform providers lock -platform=... for each CI or laptop platform, commit.
  • Irreconcilably corrupt lock file — back up .terraform.lock.hcl, remove the lock file, and run terraform init to create a new one. Review the resulting provider selections before committing it. Removing .terraform/ alone does not regenerate the lock file; Terraform continues honoring an existing .terraform.lock.hcl.

Regenerating the entire lock file is appropriate when the file is clearly broken and you cannot reconcile constraints. Prefer targeted init -upgrade or providers lock when possible so unrelated provider entries stay stable.


References


Summary

You separated three layers that beginners often merge: required_version guards the Terraform CLI, required_providers.version defines acceptable provider releases, and .terraform.lock.hcl records the selected version and acceptable package checksums after init. Constraint operators — especially ~> — describe ranges; the lock file records one selected version until you change constraints or run terraform init -upgrade.

On Ubuntu you watched hashicorp/local move from a pessimistic ~> 2.5 constraint to a concrete 2.9.0 lock entry, saw ordinary init reuse that lock when the registry had newer compatible releases, and used upgrade init to move from an exact 2.5.0 pin to a newer 2.x build. You also pre-populated multi-platform checksums with terraform providers lock and staged the lock file for Git so teammates select the same provider version and verify packages against the same recorded hashes.

The mistake to avoid is treating required_version as a provider pin, or assuming init always downloads the newest provider. Commit .terraform.lock.hcl for shared roots, review lock diffs like application code, and run validate and plan after every provider upgrade before you apply in production. Next in this course track: Terraform state fundamentals and the core init/plan/apply workflow commands.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)