Skip to main content
RunBook Academy

TerraformIII · Installing and Versioning TerraformProduction Terraform

The Dependency Lock File

Intermediate⏱ ~12 minbash

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

Not yet marked complete on this device.

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. version is the exact plugin version selected on the last terraform init. constraints is the range declared in required_providers at the time of the lock.
  • Hashes. Two hash schemes per provider: a zh: “zip hash”, the legacy registry-protocol SHA-256 of the official .zip package, and an h1: 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.

FileLives inPurposeFormat
required_providers blockInside terraform { ... } in your .tf filesDeclares the range the configuration is willing to acceptHCL constraints with operators
.terraform.lock.hclAt the working directory rootRecords the exact provider version and hashes last resolvedHCL, 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_version and 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.tfstate under source and resolved against the module registry or your private module source. For a stricter module lock, use a separate tool such as terraform-mvdb or commit a terraform.tfvars with 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: or zh: 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.hcl as plain HCL. Use a reviewer checklist that calls out provider changes explicitly.
  • Re-verify after every upgrade. terraform plan against 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

  1. Q1. What does .terraform.lock.hcl record?

  2. Q2. You widen a required_providers version constraint. What command updates the lock file?

  3. Q3. Hand-editing .terraform.lock.hcl is acceptable if terraform validate passes afterwards.

  4. Q4. How does the lock file differ from the required_providers block?

  5. Q5. Which of these are correct discipline for the lock file? (Select all that apply.)

  6. 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?

  7. Q7. What does the h1: hash in a lock file entry prove?

Passing score: 75%. Answers are checked in this browser.