.terraform.lock.hcl records which provider versions were selected and their checksums. Terraform refuses to proceed when what the configuration needs and what the lock file records disagree.
Two variants, with different fixes.
Variant 1: A provider is required but not locked
- provider registry.terraform.io/hashicorp/random: required by this
configuration but no version is selected
Something now uses a provider the lock file has never seen. Usually because you added a resource, or a module you upgraded started using one.
terraform providers
Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/aws] ~> 5.0
├── provider[registry.terraform.io/hashicorp/random]
└── module.vpc
└── provider[registry.terraform.io/hashicorp/aws] >= 4.0
Note it includes providers required by modules. A module bumped from 3.x to 4.x may add one without any change to your own code, which is why this appears after a module upgrade you thought was unrelated.
The fix is a plain init:
terraform init
This adds the missing provider at the newest version your constraints allow and leaves the others alone. Then commit the updated lock file.
Why not -upgrade
terraform init -upgrade also works, and it does more than you asked: it re-resolves every provider to the newest allowed version. With ~> 5.0 that can move AWS from 5.31 to 5.70 as a side effect of adding random.
That is fine when you meant to upgrade. It is not fine as a reflex fix in CI, where it silently defeats the entire purpose of a lock file: two runs a week apart get different provider versions, and a provider bug arrives in a pipeline nobody changed.
If a pipeline needs -upgrade to work, the lock file is not committed, or it is out of date and should be updated deliberately in a reviewed change.
Variant 2: No hash for this platform
Provider registry.terraform.io/hashicorp/aws v5.70.0 does not have a
package available for your current platform, linux_arm64.
The lock file stores checksums per platform:
provider "registry.terraform.io/hashicorp/aws" {
version = "5.70.0"
constraints = "~> 5.0"
hashes = [
"h1:xxxxx...",
"zh:aaaa...",
]
}
Generate it on an Apple Silicon Mac and it holds darwin_arm64 hashes. A linux_amd64 CI runner then finds no hash it can verify and fails, even though the version is right.
Record every platform you build on:
terraform providers lock \
-platform=linux_amd64 \
-platform=linux_arm64 \
-platform=darwin_arm64 \
-platform=darwin_amd64 \
-platform=windows_amd64
Commit the result. Run it again whenever you change provider versions or add a platform, such as moving CI to Graviton runners.
In CI, use -lockfile=readonly
This is the setting that makes the lock file mean something:
terraform init -lockfile=readonly
The job fails if the lock file would need to change, rather than quietly changing it. A dependency drift then surfaces as a failed build with a clear cause, instead of an apply using a provider version nobody reviewed.
# GitLab CI
plan:
script:
- terraform init -lockfile=readonly
- terraform plan -out=tfplan
Pair it with a pinned Terraform version. The lock file format is forward-compatible but not backward-compatible, so a newer CLI writing it locally can produce a file an older CLI in CI cannot read.
Provider version constraints
The lock file records what was chosen; required_providers constrains what may be chosen.
terraform {
required_version = "~> 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.70"
}
}
}
~> 5.70 allows 5.70 and above within 5.x. ~> 5.70.0 allows only 5.70.x. Neither pins exactly; the lock file does that, which is why both are needed.
A module with a conflicting constraint produces a different error naming both:
Error: Incompatible provider requirements
Resolve that by widening one constraint or upgrading the module, not by deleting the lock file.
When to just delete it
Rarely, and never in CI. If the lock file is genuinely corrupt:
rm .terraform.lock.hcl
rm -rf .terraform
terraform init
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64
This re-resolves everything to the newest allowed versions, so review the diff before committing. Treat it as a deliberate upgrade and test it, not as a repair.
A checklist
- Read which variant: a missing provider, or a missing platform hash.
terraform providersto see what the configuration actually requires.- Missing provider → plain
terraform init, then commit the lock file. - Avoid
-upgradeunless you mean to move versions. - Platform error →
terraform providers lock -platform=...for every platform you use. - Commit
.terraform.lock.hcl. It is not a cache. - CI →
terraform init -lockfile=readonly. - Pin the Terraform version so local and CI write the same format.
Frequently Asked Questions
Should I commit .terraform.lock.hcl to version control?
Yes. It is a lock file in the same sense as package-lock.json or Cargo.lock: it records the exact provider versions and checksums that were verified to work, so every machine and every pipeline run resolves identically. Without it, each terraform init picks the newest version your constraints allow, and a provider released overnight can change a plan in a repository nobody touched. The .terraform/ directory alongside it is a cache and should stay ignored; the lock file should not.
What is the difference between terraform init and terraform init -upgrade?
Plain init keeps every provider already recorded in the lock file at its locked version and only resolves ones that are missing. -upgrade re-resolves all of them to the newest version each constraint permits. So adding a new provider needs a plain init, while -upgrade is a deliberate act of moving versions that should produce a reviewable diff in the lock file. Using -upgrade routinely in CI defeats the lock file entirely, since two runs a week apart can then use different providers.
Why does the lock file work locally but fail in CI?
Because the checksums are recorded per platform. A lock file generated on an Apple Silicon Mac contains darwin_arm64 hashes, and a linux_amd64 runner finds no hash it can verify, so verification fails even though the version is correct. Run terraform providers lock with a -platform flag for every operating system and architecture you build on, then commit the result. You need to repeat it whenever provider versions change or you add a platform, such as switching CI to ARM runners.
What does -lockfile=readonly do?
It makes terraform init fail rather than modify the lock file. That turns any unrecorded dependency change into an explicit build failure instead of a silent update, which is what you want in a pipeline: the lock file should only change through a reviewed commit. Without it, CI happily writes a new lock file inside the job, uses whatever it resolved, and discards the change when the runner is torn down, so the version that was applied is not the version anyone approved.
Is it safe to delete the lock file to fix an error?
It resolves most lock file errors and it is a blunt instrument, because it re-resolves every provider to the newest permitted version. That is an upgrade, not a repair, and it should be treated as one: review the resulting diff, run a plan, and test before merging. Never do it inside CI, where it removes the guarantee that the pipeline is using reviewed versions. Prefer the targeted fixes first, a plain init for a missing provider and terraform providers lock -platform=... for a missing hash.