Skip to content
intermediate Phase 8 · Provider Configuration and Version Pinning

Provider Lock File Integrity

Committing .terraform.lock.hcl and verifying checksums across machines.

50m
0 problems
Topic Progress 0%

Reading `.terraform.lock.hcl`: Version, Constraints, and Two Kinds of Hash

terraform init prints a warning that most people scroll past:

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.

That file sits in the root module's working directory — not inside .terraform/, not per module — and it is the only place Terraform records which exact provider binary each machine should use. A real entry looks like this:

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

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"
  constraints = "~> 5.31.0"
  hashes = [
    "h1:EOJImaEaVThWasdqnJjfYc6/P8N/MRAq1J7avx5ZbV4=",
    "h1:1xvaS5D8B8t6J6XmXxX8spo97tAzjhacjedFX1B47Fk=",
    "zh:0015b491cf9151235e57e35ea6b89381098e61bd923f56dffc86026d58748880",
    "zh:4c5682ba1e0fc7e2e602d3f103af1638f868c31fe80cc1a884a97f6dad6e1c11"
  ]
}

Three fields, three different jobs:

Field Meaning Used for install decisions?
version the exact release Terraform selected from the search space your constraints define yes
constraints every constraint that was considered, recorded for humans no — documentation only
hashes checksums that a package must match to be accepted yes

Location and lifecycle are simpler than they look. There is exactly one lock file per configuration, in the directory where you run Terraform (the root module) — not inside .terraform/, not one per child module. You never hand-edit it: terraform init and terraform providers lock own it. Init creates it on the first successful provider resolution, updates it when requirements change or when a new platform contributes hashes, and — on Terraform 1.1 and later — removes entries whose provider lost its last reference in both configuration and state. Because the file is plain HCL, git diff .terraform.lock.hcl is readable in review: a moved version line means an upgrade happened, while hashes-only growth means coverage was extended.

That readability is why the file belongs in version control. On HCP Terraform and Terraform Enterprise the lock file drives installation too — a run installs exactly the recorded versions when the file exists — so committing it makes local runs, remote runs, and CI agree by construction rather than by hope.

The two hash prefixes are the detail interviews care about. zh: means "zip hash": a SHA256 of the official .zip archive as published by the origin registry, so it only exists for packages that came from a registry. h1: means "hash scheme 1": a SHA256 computed from the contents of the package rather than the archive bytes, which means it matches the official zip, an unpacked directory, or a recompressed zip holding identical files. Terraform is migrating toward h1:, so lock files legitimately grow new h1: lines over time even when version never changes.

Verification works on a trust-on-first-use model. The first init for a new provider downloads the package, records its checksums, and every later init on every machine must produce a byte-identical package or fail:

Error: Failed to install provider

Error while installing hashicorp/aws v5.31.0: the current package for
registry.terraform.io/hashicorp/aws 5.31.0 doesn't match any of the
checksums previously recorded in the dependency lock file.

That error is the feature. It means a tampered mirror, a corrupted CDN, or a re-published release cannot silently replace the binary your last plan was reviewed against. Commit the file, review its diffs in pull requests like any other source, and treat version changes as the dependency upgrades they are. Version Constraint Syntax defines the search space; the lock file is the decision, and it is what makes two laptops and a CI runner install identical plugins.

Pre-populating Every Platform with `terraform providers lock`

Your team runs macOS laptops (darwin_arm64) and a CI runner on Linux (linux_amd64). One developer initializes the configuration first, and the lock file records their platform's checksums. Six months later the CI runner performs a first init of that provider — from a filesystem mirror, where no origin registry is consulted — and Terraform can only vouch for the package in front of it. The lock entry now covers one platform, and the other machines cannot be verified against it.

The cure is a command that touches only the lock file:

terraform providers lock \
  -platform=darwin_arm64 \
  -platform=linux_amd64 \
  -platform=windows_amd64

Read what it does precisely: it consults each provider's origin registry, downloads and verifies the official packages for every listed platform, and writes both zh: and h1: checksums for all of them into .terraform.lock.hcl — without installing anything into .terraform/ and without changing the selected version. You are buying coverage, not upgrades.

Situation What to run
Normal work, providers from the public registry terraform init — signed checksums for all published platforms arrive for free
Team spans several OS/arch combinations terraform providers lock -platform=... once, then commit
Providers come from a filesystem or network mirror the same command before the config enters the air-gapped network
In-house provider with no origin registry add -fs-mirror=/srv/providers (or -net-mirror=URL) so the mirror's hashes are recorded

That last row matters: by default the command assumes every provider has a real registry behind it. For a provider distributed only as files under terraform.example.com/examplecorp/ourcloud, -fs-mirror redirects the lookup, and the resulting entry covers exactly the platforms you named — checksums come from the mirror, not from a publisher signature.

Practical notes that save an afternoon:

  • Run it from the root module directory; it analyzes the same configuration terraform init does, including child modules' requirements.
  • You can restrict its work by listing addresses: terraform providers lock hashicorp/aws leaves every other entry untouched.
  • It needs network reachability to the registries (or the mirror), so run it on a connected machine and ship the file — the air-gapped runners then verify against hashes they never had to fetch.
  • Do not confuse it with terraform providers mirror, which copies provider packages into a directory; this one copies checksums into the lock file. The pairing of the two is the entire air-gap workflow in Provider Mirrors for Air-gapped Networks.

The command's output deserves one reading, because it is where trust is established. For each provider it reports which packages it obtained for each requested platform and whether those packages were covered by a cryptographic signature — you will see a line like (signed by a HashiCorp partner, key ID DC9FC6B1FCE47986). The command verifies integrity, not provenance: it will happily record checksums for a signer you have never heard of, so the human step is to confirm the key ID belongs to a publisher you trust before committing the file. Skipping that check turns a signing mechanism into a ritual.

In pipelines, run it where it can be reviewed: a scheduled or dependency-bump job that regenerates hashes and opens a pull request, never a job that pushes the lock file straight to the branch being applied. Fetching official packages for four platforms on every build is slow, and writes that nobody reviews defeat the point of recording them.

A useful review habit: when a lock-file diff appears, ask which of three events happened — a new provider was added, the selected version changed (version line moves), or new platforms were locked (only hashes grows). Each has a different blast radius, and only the second one can change infrastructure.

Deleting the Lock File Is Not a Fix — It Is an Unreviewed Upgrade

The fastest way to clear a confusing provider error is to delete .terraform.lock.hcl and re-run init. It works. It is also the single most dangerous habit in Terraform, because the lock file is the only thing preventing version re-resolution: with it gone, init treats every provider as brand new and silently selects the newest version your constraints allow.

Compare what each action actually does:

Action Selected version Checksums Review artifact
terraform init unchanged — the recorded selection wins verified none
terraform init -upgrade newest that satisfies every constraint rewritten for the new version lock diff in your PR
delete lock file + terraform init newest that satisfies every constraint, silently rewritten from scratch nothing to diff

The third row is the problem: teammates pull a branch whose lock file was regenerated, their git diff shows thousands of hash lines with no version context they chose, and a provider major bump rides in under an unrelated change. Delete a lock file and you also discard every platform hash that was not regenerated on that machine — exactly the coverage the previous chapter exists to provide.

If the damage is already done, recovery is a git operation rather than a Terraform one:

git diff .terraform.lock.hcl           # did a version move, or did only hashes grow?
git checkout -- .terraform.lock.hcl    # restore the reviewed selection
terraform init                         # reinstall exactly that version

One related trap lives in .gitignore: ignoring .terraform/ is correct, but a blanket pattern such as .terraform* also drops .terraform.lock.hcl, and the next fresh clone initializes from constraints alone — no review, no diff, no error. Repositories should track that file explicitly.

Other failure modes worth knowing:

  • init -upgrade in CI. Run it deliberately, in a dependency-bump job that opens a pull request, never in the pipeline that applies production. Upgrades should arrive as reviewed diffs, not as whatever the registry served during a deploy.
  • Removing a provider, then re-adding it later. Once the last reference disappears from both configuration and state, init drops the entry; re-adding the requirement makes Terraform treat it as a new dependency, which means a fresh selection that need not match the version you used before. Note the version you are on before you prune.
  • Stale entries on old Terraform. Up to v1.0, init never removed unneeded entries, so a workspace upgraded across that boundary may report missing or corrupted provider plugins until you run a modern terraform init to tidy the file.
  • The plugin cache temptation. A global plugin_cache_dir is a speed-up, not a source of truth; Terraform only reuses a cached package when its checksum already appears in the lock file. The escape hatch plugin_cache_may_break_dependency_lock_file = true disables that guarantee and produces entries valid only on the machine that wrote them — for exceptional situations, and never for a shared repository.

Everything in this chapter presumes providers are configured per deployment target, which is where a second (and third) configuration of the same provider enters the picture: Provider Aliases for Multi-Region Use.

Interview shorthand: if asked "what does .terraform.lock.hcl guarantee?", answer that it pins exact versions and cryptographic hashes per platform so every machine installs byte-identical binaries — then mention terraform providers lock -platform=... as the way you extend coverage, and terraform init -upgrade as the only sanctioned way to move the pin.

Quiz

1. What is the `constraints` field inside `.terraform.lock.hcl` used for?

Question 1 options

2. A teammate initializes the same repository from a filesystem mirror on macOS; your CI runner on linux_amd64 then fails with a checksum that does not match the lock file. What is the correct fix?

Question 2 options

3. What distinguishes `zh:` hashes from `h1:` hashes in the lock file?

Question 3 options

4. Someone deletes `.terraform.lock.hcl` to fix an init error and reruns init. What happens next?

Question 4 options

Flashcards

Question

What does `.terraform.lock.hcl` record for each provider?

Answer

The exact selected `version`, the `constraints` that were considered (documentation only), and `hashes` — accepted package checksums for every platform that has been locked — so every machine installs a byte-identical binary or fails loudly.

Question

Difference between `zh:` and `h1:` checksums?

Answer

`zh:` ('zip hash') is a SHA256 of the official zip as indexed by the provider registry; `h1:` ('hash scheme 1') is a SHA256 of the package contents, valid for the official zip, an unpacked directory, or a recompressed zip with the same files. Terraform migrates toward `h1:` opportunistically.

Question

Which command adds checksums for more platforms without changing the selected version?

Answer

`terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 ...` (flags repeat). It consults origin registries, verifies official packages for each platform, and writes hashes only. For in-house providers without a registry, add `-fs-mirror=PATH` or `-net-mirror=URL`.

Question

When does `terraform init` change a recorded provider selection?

Answer

Only when there is no entry yet, when the recorded version no longer satisfies the configuration's constraints, or when you pass `-upgrade`. Plain init always re-selects the recorded version, even if newer releases exist.

Question

Why must the lock file be committed to version control?

Answer

So laptops, CI runners, and remote execution all install identical binaries and any tampered or corrupted package fails with a checksum error instead of silently upgrading. Its diff is also the review artifact for every provider change.

Revision Notes

Key Takeaways

  • 1. The lock file pins exact versions and per-platform cryptographic hashes, which is what makes installs reproducible across laptops, CI, and remote runs
  • 2. `zh:` hashes the official zip from the registry while `h1:` hashes package contents, so Terraform can verify unpacked or repacked packages too
  • 3. `terraform providers lock -platform=...` adds coverage for additional platforms without touching the selected version — essential when init runs through a mirror
  • 4. Deleting the lock file is an unreviewed upgrade: constraints alone re-resolve everything and every platform hash must be regenerated
  • 5. Verification is trust-on-first-use — the first install records checksums and later mismatches fail loudly instead of replacing a reviewed binary

Interview Tips

  • • "What does `.terraform.lock.hcl` guarantee?" — exact versions plus cryptographic hashes per platform, so every machine installs byte-identical binaries; then name `terraform providers lock -platform=...` as the coverage tool
  • • "Your CI fails with 'doesn't match any of the checksums' — diagnose it" — separate tampering from coverage: compare `version` and `hashes` in the diff, check whether init ran through a mirror, and see which platform wrote the entry
  • • "How do you upgrade a provider safely?" — widen the constraint, `terraform init -upgrade`, review the lock diff, ship it as its own PR; never delete the file to escape an error
  • • "What are zh and h1 hashes?" — zip hash versus content hash; adding that `h1:` survives repacking and that `zh:` comes from the registry's signed checksums shows real depth

Cheat Sheet

Provider Lock File Integrity

A real entry:

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"       # what to install — exact
  constraints = "~> 5.31.0"    # what was considered — documentation
  hashes = [
    "h1:EOJImaEaVThWasdqnJjfYc6/P8N/MRAq1J7avx5ZbV4=",   # content hash
    "zh:0015b491cf9151235e57e35ea6b89381098e61bd923f56dffc86026d58748880" # zip hash
  ]
}

Hash schemes:

Prefix Mnemonic Covers
zh: zip hash SHA256 of the official .zip from the registry protocol
h1: hash scheme 1 SHA256 of package contents — zip, unpacked dir, or recompressed zip

Actions:

Action Version Checksums Reviewable?
terraform init unchanged (recorded selection wins) verified no change
terraform init -upgrade newest satisfying all constraints rewritten yes — lock diff in the PR
delete file + init re-resolved silently regenerated on this machine only no
terraform providers lock -platform=... unchanged hashes added for those platforms yes

Workflow: commit .terraform.lock.hcl next to the configuration; run providers lock -platform=darwin_arm64 -platform=linux_amd64 -platform=windows_amd64 on a connected machine before an air-gapped rollout; treat version moves as dependency upgrades and hashes-only growth as new platform coverage.

Continue Learning