skip to content

What does Terraform's `.terraform.lock.hcl` record, and should it be committed to version control?

level: middleimportance: must knowfreq 70%

answer

  1. records a decision, not a wish
  2. one entry per provider
  3. version, constraints, and hashes
  4. checksum verification at install time
  5. gitignore the directory, not the file

basics

~20 s

The dependency lock file records, per provider, the exact version Terraform selected, the constraints it was selected under, and checksums of the provider packages. It belongs in Git: it is what makes every laptop and CI runner install byte-identical plugins.

solid answer

~40 s

`.terraform.lock.hcl` is the **dependency lock file**. Terraform writes it in the root module directory during `init`, and for each provider it records the selected version, the constraint strings that produced that selection, and a list of hashes — `h1:` entries for provider packages Terraform has actually installed and `zh:` entries from the registry's signed checksum document. **Commit it.** It is the difference between "we're all on some 5.x" and "we're all on 5.31.0, verified by checksum". On later runs `init` reuses the locked version instead of re-resolving, so nobody silently drifts onto a new provider; moving forward is a deliberate `terraform init -upgrade` plus a committed lock-file change that shows up in code review. Note it covers **providers only** — it is not a lock file for the Terraform CLI itself.

code

hcl · 10 lines
hcl
# .terraform.lock.hcl (generated by terraform init - commit this file)
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"
  constraints = "~> 5.0"
  hashes = [
    "h1:EXAMPLEhashForOnePlatformPackage=",
    "zh:EXAMPLEsignedRegistryChecksum1",
    "zh:EXAMPLEsignedRegistryChecksum2",
  ]
}

go deeper

for a junior

Know it is generated by terraform init, that it records which provider versions were chosen, and that it is committed to Git while the .terraform/ directory is not.

for a middle

Explain the three parts of an entry — version, constraints, hashes — and describe how init reuses the locked version until -upgrade or a constraint change forces re-resolution.

for a senior

Demonstrate operating it: -lockfile=readonly in CI, treating checksum failures as a platform or mirror question rather than deleting the file, and resolving merge conflicts by regenerating rather than hand-editing.

for a principal

Own it as a supply-chain control — where provider binaries come from, whether an internal mirror or private registry sits in the path, and how provider upgrades are reviewed and rolled out across many repositories.

## Why the file exists Version constraints are ranges. `~> 5.0` says any 5.x is acceptable, which means two engineers running `terraform init` a week apart would get different plugin builds, and a provider regression would appear on one machine and not another. The dependency lock file removes that variance: it records the *decision* Terraform made, so every subsequent run repeats it. It was introduced in Terraform 0.14 and lives in the root module directory as `.terraform.lock.hcl` — deliberately next to your code, not inside the gitignored `.terraform/` cache directory. ## What is inside One block per provider: ```hcl provider "registry.terraform.io/hashicorp/aws" { version = "5.31.0" constraints = "~> 5.0" hashes = [ "h1:...", "zh:...", "zh:...", ] } ``` - **`version`** — the exact release selected. - **`constraints`** — a record of what the configuration asked for, kept for human readability and for detecting when the config and the lock disagree. - **`hashes`** — the integrity data. `zh:` entries come from the registry's signed checksum document for that release and cover the release's packages generally; `h1:` entries are computed from a provider package Terraform actually installed, and each one corresponds to a specific platform build (`linux_amd64`, `darwin_arm64`, and so on). What is **not** inside: no credentials, no infrastructure data, no Terraform CLI version, and no module versions — the lock file tracks provider dependencies only. ## Commit it The lock file is source code, and every mainstream Terraform layout treats it that way. Committing it buys three things: 1. **Reproducibility.** CI installs the same provider build the author tested against, not whatever shipped this morning. 2. **Integrity.** On install, Terraform verifies the downloaded package against the recorded hashes and refuses to proceed on a mismatch — a real supply-chain control, not just tidiness. 3. **Reviewability.** A provider upgrade appears in a pull request as a lock-file diff. Without the file, upgrades are invisible events that happen whenever someone happens to re-init. Adding `.terraform.lock.hcl` to `.gitignore` is a defect. The thing that *is* gitignored is the `.terraform/` directory, which holds the downloaded plugin binaries and backend cache — a different path with a confusingly similar name, and the source of the mix-up. ## How it behaves during init With a lock file present, `terraform init` installs the recorded version and verifies its checksum, even when a newer release satisfies the constraint. Three cases change that: - The recorded version **no longer satisfies** the constraint (someone raised the floor). `init` re-resolves and updates the lock file. - You pass **`-upgrade`**. `init` ignores the recorded selection, picks the newest allowed release, and rewrites the entry. - You pass **`-lockfile=readonly`**. `init` refuses to modify the file and fails instead — useful in CI to guarantee the pipeline runs exactly what was reviewed. The hash set can also *grow* without a version change: when a new platform installs the same provider version, Terraform appends that platform's `h1:` hash. Those one-line diffs are normal, not corruption. ## Failure modes worth recognising A checksum mismatch at `init` means the package Terraform fetched does not match anything recorded. Usually that is a platform gap (the lock was created on one OS/architecture and CI runs another) or a mirror serving a repackaged binary — not tampering, but worth understanding before you reach for the delete key. **Deleting the lock file to make CI pass is the wrong reflex**: it throws away the integrity check and the pinned version at the same moment you are least sure what is going on. The right fixes are to record the missing platforms deliberately with `terraform providers lock -platform=...`, or to re-run `init -upgrade` locally and commit the result. Merge conflicts happen when two branches bump different providers. Resolving by hand-editing hashes is unreliable; take one side, then re-run `terraform init -upgrade` locally and commit the regenerated file.

  • How do you stop a CI pipeline from ever modifying the lock file?
    Run `terraform init -lockfile=readonly`. Terraform then refuses to write the file and fails the step if the recorded selections would have to change — for example because someone widened a constraint without regenerating the lock locally. It turns a silent, invisible upgrade into a build failure that points at a missing commit.
  • Does the lock file pin module versions too?
    No. It tracks provider dependencies only. Module version selection is resolved separately and is not checksummed in this file, which is a common misconception when people map Terraform onto package managers like npm or Bundler whose lock files cover everything.
  • Two branches each bumped a different provider and the lock file now conflicts. How do you resolve it?
    Do not hand-merge hash lists — the entries are position-independent but easy to corrupt, and a wrong hash fails init in a confusing way. Take either side wholesale to get a syntactically valid file, then run `terraform init -upgrade` locally so Terraform re-resolves every provider against the merged constraints, and commit the regenerated file.
  • What is the difference between the `zh:` and `h1:` hashes in an entry?
    `zh:` hashes come from the registry's signed checksum document published with the release. `h1:` hashes are computed by Terraform from a provider package it actually installed, so each corresponds to a specific platform build. That is why the hash list can grow when a colleague on a different OS runs init against the same provider version.

saying these in an interview costs you the question

  • Adding .terraform.lock.hcl to .gitignore
  • Confusing the lock file with the .terraform directory
  • Believing the lock file holds state or credentials
  • Deleting the lock file to make CI init pass
  • Thinking it pins the Terraform CLI version too

context