The Dependency Lock File
The .terraform.lock.hcl file records the exact provider versions and their checksums that init selected, so every subsequent run — yours, a teammate's, CI's — uses identical providers rather than whatever happens to be newest within the constraint. It is Terraform's package-lock.json, written by init and committed to the repo.
Committing it is non-negotiable for reproducibility. The two things that turn the lock file from invisible into a CI incident are init -upgrade and platform-specific hashes — understanding both is what keeps the lock helping instead of surprising you.
What the Lock File Records
For each provider the lock stores the exact version selected, the constraint it was selected under, and a set of checksums — one per platform. The h1: entries are the hashes Terraform verifies a downloaded provider against, and the list covers each operating system and architecture, with the platform implied by which package was hashed rather than written into the hash string. That per-platform detail is the source of the most common lock surprise, covered below.
provider "registry.terraform.io/hashicorp/aws" { version = "6.31.0" constraints = "~> 6.0" hashes = [ "h1:FJwsuowaG5CIdZ0WQyFZH9r6kIJeRKts9+GcRsTz1+Y=", "h1:c/ntSXrDYM1mUir2KufijYebPcwKqS9CRGd3duDSGfY=", "zh:04f0a50bb2ba92f3bea6f0a9e549ace5a4c13ef0cbb6975494cac0ef7d4acb43", ] }
Why It's Committed
The constraint sets a range; the lock pins the exact pick within it. Without the lock committed, every machine and every CI run is free to resolve a different version inside ~> 6.0 — one developer on 6.31, another on 6.40, CI on whatever was latest that morning — and their plans diverge for reasons no one can see. Committing the lock collapses that to one version everyone shares, which is the entire point of reproducibility.
Updating the Lock
terraform init -upgrade is the deliberate way to move within the constraint and rewrite the lock. It picks the newest versions the constraints allow and records them, so the change to .terraform.lock.hcl shows up in your diff as a reviewable bump. A plain init never moves a locked version — it respects the existing pin — which is why an upgrade has to be explicit and on purpose.
# a plain init keeps the locked versions terraform init # -upgrade moves to the newest allowed and rewrites the lock terraform init -upgrade # review the lock diff before committing git diff .terraform.lock.hcl
Platform Hashes
The classic failure: you run init on macOS, the lock gets only the darwin_arm64 hash, you commit it, and Linux CI fails with a checksum error because the lock has no linux_amd64 entry for the provider it just downloaded. The fix is to record hashes for every platform your team and CI use with terraform providers lock, listing each -platform. Run it once when adding a provider and the lock carries every hash anyone will need.
# add hashes for mac, linux CI, and windows in one shot
terraform providers lock \
-platform=darwin_arm64 \
-platform=linux_amd64 \
-platform=windows_amd64
Lock File and Constraints Together
The constraint and the lock are two layers, not duplicates. The ~> constraint in required_providers declares the allowed range; the lock pins exactly which version inside that range is in use right now. Change the constraint and you change what's allowed; run init -upgrade and you change what's picked. Reading both in a PR — the constraint widening and the lock moving — tells you exactly what changed and why, which is why the lock diff belongs in review like any other dependency change.
- Gitignoring
.terraform.lock.hcl, so each machine and CI resolve different provider versions within the constraint and plans diverge for invisible reasons. - Generating the lock on macOS only, then hitting a checksum error in Linux CI because the lock has no
linux_amd64hash for the provider. - Running
init -upgradeunintentionally and committing a provider bump nobody reviewed, sneaking a version change in under an unrelated PR. - Treating the lock diff as noise and reverting it, undoing a deliberate upgrade and dragging the team back to the old version.
- Assuming a plain
initupdates the lock — it respects the existing pin, so versions never move without-upgrade.
- Commit
.terraform.lock.hcland review changes to it like any dependency change, so version moves are always visible. - Record hashes for every platform your team and CI use with
terraform providers lock -platform=...to avoid checksum failures across operating systems. - Update the lock deliberately via
init -upgradeas a reviewed change, never as an accidental side effect of an unrelated init. - Read the lock diff in PRs to see exactly which provider versions moved and confirm the bump was intended.
- Keep the constraint and the lock in mind as two layers — the range you allow and the exact pick within it — when reasoning about what an upgrade changed.
Knowledge Check
What does .terraform.lock.hcl pin?
- The exact provider versions and their per-platform checksums that init selected
- The Terraform core binary version the whole project must run under
- The AWS access key and region the provider used during the last apply
- The resource IDs Terraform created along with all their attributes, much like a state file does
Why does Linux CI hit a checksum error when the lock was generated on macOS?
- The lock holds only the darwin hash and lacks the linux_amd64 hash for the provider
- Lock files are simply not portable between different operating systems and must be regenerated locally
- CI must delete and regenerate the lock from scratch on every pipeline run
- macOS writes the lock in a binary format that Linux Terraform cannot parse
How do you record provider hashes for every platform your team and CI use?
terraform providers lock -platform=...with one flag per OS and architecture- Run
initonce on each developer's platform and merge the resulting lock files by hand - Add a list of platforms to a new argument in the
required_providersblock - Delete the lock so CI regenerates the right per-platform hashes on its next run
What is the difference between a version constraint and the lock file?
- The constraint sets the allowed range; the lock pins the exact version chosen within it
- The lock sets the allowed range while the constraint pins the one exact version chosen
- They are the same single thing expressed redundantly across two committed files
- The constraint pins the exact version and the lock stores the provider's credentials
You got correct