TerraformIII · Installing and Versioning TerraformProduction Terraform
The Dependency Lock File
What you'll learn
- Describe the contents and purpose of .terraform.lock.hcl
- Distinguish the lock file from the required_providers constraint block
- Apply the right discipline: commit the lock file, upgrade deliberately, never edit it by hand
- Recognise the symptoms of a stale or missing lock file in production
- Diagnose and recover from a lock file that no longer matches the configuration
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
The .terraform.lock.hcl file is the reproducibility contract for a
Terraform working directory. It pins the exact provider plugin version
that every operator, CI runner, and apply host will resolve to. Without
it, a configuration that works on your laptop can resolve a different
provider patch on the apply host and produce a different plan.
What the file contains
terraform init writes one .terraform.lock.hcl per working directory.
The file is HCL, JSON-compatible, and intended to be machine-readable
and human-readable. A real example, trimmed:
# This file is maintained automatically by "terraform init".
# Manual edits may be lost - changes should be made using the relevant
# command in the Terraform CLI.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.65.0"
constraints = "~> 5.0"
hashes = [
"h1:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
"zh:1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.2.3"
constraints = "~> 3.0"
hashes = [
"h1:0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4",
"zh:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
]
}
Three things in every block:
- Source.
registry.terraform.io/hashicorp/aws— the fully qualified provider address. This is the network location Terraform resolved from, not the friendly short name. - Version and constraints.
versionis the exact plugin version selected on the lastterraform init.constraintsis the range declared inrequired_providersat the time of the lock. - Hashes. Two hash schemes per provider: a
zh:“zip hash”, the legacy registry-protocol SHA-256 of the official.zippackage, and anh1:hash, the newer and preferred SHA-256 computed over the contents of the unpacked package. Terraform rejects a provider package that matches none of the hashes in the lock entry.
Lock file vs required_providers
Two files, two purposes. Engineers regularly confuse them.
| File | Lives in | Purpose | Format |
|---|---|---|---|
required_providers block | Inside terraform { ... } in your .tf files | Declares the range the configuration is willing to accept | HCL constraints with operators |
.terraform.lock.hcl | At the working directory root | Records the exact provider version and hashes last resolved | HCL, JSON-compatible |
The required_providers block is a constraint: “I will accept
anything in this range”. The lock file is a record: “this is what
we last resolved to and what we should resolve to again”. The lock
file exists because the constraint alone is not enough for
reproducibility. A constraint of ~> 5.0 admits hundreds of possible
patch versions; the lock file picks one.
terraform {
required_version = ">= 1.9.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
That block is the intent. The corresponding lock file entry is the
record. You edit the block to widen or narrow the range. You
update the lock file by running terraform init -upgrade, not by
opening the file in your editor.
The right discipline
Three rules, non-negotiable.
1. Commit the lock file. It belongs in the same commit as the
configuration change that caused it. Most repositories add it to a
default-allow .gitignore is wrong; add .terraform/ to .gitignore
but leave the lock file tracked.
# .gitignore - correct
.terraform/
.terraform.tfstate.lock.info
*.tfstate
*.tfstate.backup
crash.log
crash.*.log
# .terraform.lock.hcl is intentionally NOT ignored
2. Upgrade deliberately. When a provider author publishes a new
patch you want, run terraform init -upgrade, review the diff, run
terraform plan, then commit. The diff is the audit trail:
# READ-ONLY until you commit
terraform init -upgrade
# Inspect the change
git diff .terraform.lock.hcl
# Confirm the plan is sane
terraform plan
The zh: zip hash and the h1: content hash both change when the
provider release changes. The zh: line can also move on its own if
the archive is repackaged, because it covers the .zip rather than
the files inside it. Read the diff.
3. Never edit by hand. The file is owned by the CLI. Hand edits
will be overwritten on the next init if the constraint still
matches, or rejected outright if they produce an inconsistent file.
If you need a different provider, change the constraint block and run
init -upgrade.
What the lock file does not do
- It does not lock the Terraform CLI version. That is the job of
required_versionand the team standardisation policy. - It does not lock the state file. State is in the backend (S3, Consul, Terraform Cloud, and so on).
- It does not lock modules. Module versions are recorded in
terraform.tfstateundersourceand resolved against the module registry or your private module source. For a stricter module lock, use a separate tool such asterraform-mvdbor commit aterraform.tfvarswith pinned module sources. - It does not lock provider configuration. The arguments you pass to a provider block are still yours to manage.
Production failure modes
These are the failure modes that show up in real incident channels.
1. The lock file is in .gitignore. Symptom: every operator
re-resolves, every init may pick a new patch version, and two
engineers in the same day can produce different plans for the same
change. Recovery: remove the line from .gitignore, restore the
canonical lock from git log -- .terraform.lock.hcl, recommit.
2. The lock file was upgraded in a feature branch and merged
without a plan. Symptom: a clean terraform plan on main
suddenly shows provider-driven churn. Recovery: revert the lock file,
inspect the diff that caused the upgrade, decide whether the upgrade
is wanted, re-apply deliberately.
3. The lock file hashes do not match. Symptom: terraform init
fails with Locked provider ... does not match the expected checksum.
This usually means the provider registry was repackaged, the lock
file was hand-edited, or a private mirror served a different
artifact. Recovery: trust the registry. Update the hashes by running
terraform init -upgrade against the upstream registry, not the
mirror, and confirm the diff is consistent.
4. The constraint in required_providers was widened without a
matching upgrade. Symptom: a new engineer pulls the latest main and
finds the lock file does not cover the wider range; init re-resolves
without -upgrade and produces a lock that may diverge from the rest
of the team. Recovery: standardise the upgrade procedure. Anyone who
changes a constraint also commits the corresponding lock entry.
5. The lock file is shared across working directories. Each
working directory writes its own lock. If you cp a lock from one
directory into another, the providers referenced may not match the
configuration. Symptom: terraform plan fails with
“Provider configuration not present”. Recovery: delete the lock and
re-init against the current configuration.
6. Two engineers upgrade providers independently on different
branches. Symptom: a merge conflict on .terraform.lock.hcl. Both
edits are valid; the merge resolution is to combine the entries and
re-run terraform init (without -upgrade) to confirm the merged
lock is still consistent with both branches’ constraints. If the
merged constraints are incompatible, the constraint blocks need to
be reconciled first.
Security and integrity
The lock file is the integrity boundary for the provider
downloads. The zh: line is a SHA-256 of the .zip package the
origin registry indexed; the h1: line is a SHA-256 over the contents
of the unpacked package. Terraform compares the package it installs
against the recorded hashes before it loads the plugin, so a network
attacker who substitutes a different provider download is caught on
the next init.
Production practice:
- Pin the registry. A private mirror should expose the same hash contract; if it does not, do not use it for production applies.
- Audit hash changes. Every change to the
h1:orzh:lines must correspond to a known provider release. Treat unexplained hash changes as an integrity incident until proven otherwise. - Do not disable the hash check. There is no production knob to do so; if you find yourself wanting to, you have a mirror problem, not a lock-file problem.
Performance
The lock file has no runtime cost. The provider download is amortised
over the lifetime of the working directory because the .terraform/
directory caches the plugin by version. The first init after an
upgrade pays a download cost. Subsequent inits are near-instant.
If your init is slow, the cause is almost always network or
mirror reachability, not the lock file. The lock file is a constant
cost.
Production guidance
- Treat the lock file as a build artifact with semantics. It belongs in version control, in the same commit as the configuration change, with a reviewable diff.
- Define an upgrade procedure. Who can run
init -upgrade? On which branches? With what review? Without a procedure, the lock drifts and the next incident is a provider surprise. - Make the diff visible. Most code review tools render
.terraform.lock.hclas plain HCL. Use a reviewer checklist that calls out provider changes explicitly. - Re-verify after every upgrade.
terraform planagainst a staging backend is the cheap way to confirm a new provider patch does not produce unexpected churn.
What comes next
The next lesson in this part covers the terraform serve command
introduced in Terraform 1.9 and the related question of where state
lives when the team is too small for Terraform Cloud and too
disciplined for a hand-rolled backend.
Verification
Run the following to confirm a working lock file in your own configuration. Adapt the provider to your stack.
# READ-ONLY
ls -l .terraform.lock.hcl
# Confirm the file is tracked
git ls-files --error-unmatch .terraform.lock.hcl
# Confirm the hashes resolve
terraform providers
The last command prints the providers that the lock file references and the version that will be loaded. A clean output looks like:
Providers required by configuration:
:
provider["registry.terraform.io/hashicorp/aws"]
Providers required by state:
:
provider["registry.terraform.io/hashicorp/aws"]
Providers required by modules:
:
provider["registry.terraform.io/hashicorp/aws"]
If terraform providers reports a version that does not match the
lock file, something is inconsistent and the configuration is not
safe to apply until the inconsistency is resolved.
Knowledge check · 7 questions
Q1. What does .terraform.lock.hcl record?
Q2. You widen a required_providers version constraint. What command updates the lock file?
Q3. Hand-editing .terraform.lock.hcl is acceptable if terraform validate passes afterwards.
Q4. How does the lock file differ from the required_providers block?
Q5. Which of these are correct discipline for the lock file? (Select all that apply.)
Q6. A new engineer joins the team. She clones the repo, runs terraform init, and the resulting plan differs from the team lead's plan. What is the most likely cause?
Q7. What does the h1: hash in a lock file entry prove?
Passing score: 75%. Answers are checked in this browser.