Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 8 min read

Taming the Terraform Lock File and Version Constraints for Real

The .terraform.lock.hcl file and version constraints quietly decide whether your applies are reproducible. Most teams treat them as noise. Here's how to use them right.

  • #terraform
  • #lock-file
  • #versioning
  • #providers
  • #reproducibility
  • #ci

There’s a file in your Terraform repo that most engineers treat as autogenerated noise to be merged past as fast as possible: .terraform.lock.hcl. And there are those version = "~> 5.0" lines in your provider blocks that people copy from the last module without thinking. Together, these two things decide whether the plan you reviewed is the plan that actually applies — and whether a teammate or CI runner gets the same providers you do.

Get them wrong and you’ll eventually hit the worst kind of bug: works on my machine, breaks in CI, with a plan diff nobody can explain because someone silently picked up a new provider version. Here’s how to make version handling boring and reliable.

Two different things people conflate

There are two separate version controls, and confusing them is the root of most pain:

  • Version constraints live in your config (required_providers and required_version). They express what range is acceptable. They’re a policy, not a pin.
  • The lock file (.terraform.lock.hcl) lives in your repo and records exactly which versions and hashes were selected the last time you ran init or providers lock. It’s the pin.

Constraints say “5.x is fine.” The lock file says “but right now we are using exactly 5.62.0, with these checksums.” You need both, and you need to understand which one to touch when.

Writing constraints that don’t surprise you

Set both a Terraform version floor and provider constraints explicitly:

terraform {
  required_version = ">= 1.9.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.60"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

The operators that matter:

  • ~> 5.60 — pessimistic constraint. Allows 5.60, 5.61, … up to but not including 6.0. This is the right default: get patch and minor updates, never a breaking major.
  • >= 1.9.0 — minimum, no ceiling. Good for required_version so you don’t lock out newer CLI versions, risky for providers where a major bump can break you.
  • = 5.62.0 — exact pin in config. Almost never do this; that’s the lock file’s job. Pinning in config means every consumer of a shared module is frozen, which defeats the point.

The rule of thumb: constrain to a range in config, pin to an exact version in the lock file. A module that hard-pins providers in its required_providers is a module that fights its consumers.

The lock file is the thing that makes applies reproducible

When you run terraform init, Terraform selects the newest version allowed by your constraints, records it in .terraform.lock.hcl, and from then on uses exactly that until you deliberately upgrade. The file includes cryptographic hashes:

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.62.0"
  constraints = "~> 5.60"
  hashes = [
    "h1:abc123...",
    "zh:def456...",
  ]
}

Two non-negotiable rules:

  1. Commit the lock file. Always. It is the single thing guaranteeing that you, your teammate, and the CI runner all use byte-identical providers. A repo without a committed lock file has no reproducibility, full stop.
  2. In CI, use terraform init -lockfile=readonly. This makes CI fail if the lock file would need to change, rather than silently regenerating it and picking up a different version than you reviewed. That failure is a feature — it forces version bumps to be deliberate commits, not invisible drift.

The multi-platform hash gotcha

Here’s the one that bites teams with mixed laptops and Linux CI. By default, init only records hashes for your platform. Your Mac records darwin_arm64 hashes; CI runs on linux_amd64, finds no matching hash, and fails with a checksum error.

The fix is to populate the lock file for every platform you run on:

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

Run that, commit the result, and your lock file now satisfies every machine in your fleet. Put it in a Makefile target so nobody has to remember the incantation.

Upgrading on purpose, not by accident

When you actually want newer providers, the upgrade is a discrete, reviewable event:

terraform init -upgrade

This re-evaluates constraints, picks the newest allowed versions, and rewrites the lock file. The diff in .terraform.lock.hcl is your changelog: it shows exactly which providers moved and to what version. Review that diff. A jump from 5.62 to 5.80 is twenty releases of potential behavior change — read the provider changelog, run a plan against a non-prod workspace, and make sure no resources are flagged for unexpected replacement.

A workflow that stays boring

Putting it together:

  • Constrain providers with ~> in config — accept minors, reject majors.
  • Commit .terraform.lock.hcl and treat its diffs as meaningful review material.
  • Populate multi-platform hashes via terraform providers lock.
  • Enforce -lockfile=readonly in CI so version changes can’t sneak in.
  • Upgrade deliberately with init -upgrade, in its own PR, with a plan run to back it.

The payoff is that “it worked yesterday” stays true. The plan you reviewed at 4pm is the plan that applies at 4:05, because nothing about the provider versions could have shifted underneath you.

A lock-file diff or a constraint loosening from ~> to >= is easy to wave through in review because it looks like noise — which is exactly why it’s worth a careful second look, since that’s how an unintended major upgrade or a reproducibility hole slips in. Running version-bump PRs through a code review workflow catches the constraint that got quietly widened. For more on keeping Terraform predictable, see our other Terraform guides.

Lock file and constraint behavior can differ between Terraform and OpenTofu and across versions. Verify against your current tooling before standardizing a workflow.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.