Skip to main content
RunBook Academy

TerraformVI · Providers and the Provider EcosystemProduction Terraform

Provider Versioning and the Dependency Lock

Intermediate⏱ ~14 minbash

What you'll learn

  • Write provider version constraints using `=`, `>=`, `<`, and `~>`
  • Explain the role of `.terraform.lock.hcl` and commit it to Git
  • Upgrade providers with `terraform init -upgrade` and review the plan
  • Lock the file for every platform the team uses
  • Recognise the production cost of an unpinned provider

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.

A pinned provider version is reproducible. An unpinned version can change without notice. The control is two-fold: the version constraint in required_providers and the resolved version in .terraform.lock.hcl. Both belong in Git. The lesson teaches the constraint operators, the lock file semantics, and the upgrade procedure.

The constraint in required_providers

Every configuration that uses a provider declares the version it accepts:

terraform {
  required_version = ">= 1.9.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.80"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 4.0, < 5.0"
    }
    github = {
      source  = "integrations/github"
      version = "= 6.5.0"
    }
  }
}

Five operators are useful in production:

  • = 5.80.0 — exact. No flexibility. Use when a specific version is the only one the team has validated.
  • >= 5.80.0 — at least this version. Usually paired with an upper bound.
  • < 6.0.0 — less than. Always pair >= with < when you want to avoid a major bump.
  • ~> 5.80 — pessimistic. Only the rightmost component named in the constraint may increment. ~> 5.80 names the minor, so the minor may move: it accepts 5.80 and every later 5.x release, but not 6.0. Name the patch as well to tighten it — ~> 5.80.0 accepts 5.80.0 and later 5.80.x patches, but not 5.81.0.
  • >= 5.0, < 6.0 — explicit range. Useful when the team wants to lock to a major version without using ~>.

The default for production is ~> <major>.<minor>.<patch> for providers you track closely, because it admits patches and nothing else. Use ~> <major>.<minor> (equivalently >= <major>.<minor>, < <major+1>.0) when the team is content to take new minors within the same major automatically. Avoid the unpinned case:

# REJECTED: no version constraint.
required_providers {
  aws = {
    source = "hashicorp/aws"
    # version = ???
  }
}

The dependency lock file

After terraform init, the resolved versions live in .terraform.lock.hcl:

# This file is maintained automatically by "terraform init".
# Manual edits may be lost - proceed with caution!

provider "registry.terraform.io/hashicorp/aws" {
  version = "5.80.0"
  hashes = [
    "h1:abcd1234...",
    "h1:efgh5678...",
    "h1:ijkl9012...",
  ]
}

provider "registry.terraform.io/hashicorp/azurerm" {
  version = "4.20.0"
  hashes = [
    "h1:mnop3456...",
  ]
}

The lock file records two things for each provider:

  • The exact version resolved against the current constraint.
  • The hashes of the binary on every platform Terraform has downloaded.

Two properties follow from this:

  1. Reproducibility. A colleague who clones the repository and runs init against the same lock file gets the same provider binaries. No surprise upgrades.
  2. Integrity. Terraform re-verifies the binary against the hash on every run. A tampered binary produces a hash mismatch and the apply stops.

The lock file should be committed to Git. The .terraform/ working directory should not be:

.terraform/                  ← gitignored
.terraform.lock.hcl          ← committed
terraform.tfstate            ← gitignored (or remote-backed)
terraform.tfstate.backup      ← gitignored
*.tfplan                     ← gitignored

Locking for multiple platforms

A team with mixed workstations and a Linux CI runner needs hashes for every platform it uses. Run once on each:

# CONFIGURATION: lock for the three common platforms.
terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_amd64 \
  -platform=darwin_arm64

The lock file gains three hashes per provider. CI on Linux finds a matching hash and proceeds without re-downloading.

Upgrading providers

An upgrade is a managed change. The procedure:

# 1. Edit the constraint in versions.tf
vim versions.tf

# 2. Refresh the lock file
terraform init -upgrade

# 3. Run plan and inspect for unexpected changes
terraform plan -out=tfplan

# 4. Apply
terraform apply tfplan

init -upgrade is the only step that touches the lock file. It downloads the new binary, updates the lock entry, and leaves the configuration alone. The lock file change is the audit trail — it is what code review sees.

Reading the upgrade plan

A provider upgrade can introduce silent changes. Three patterns to watch for in the plan:

1. Default changes. The new provider version changed a default for an attribute you did not set explicitly. The plan shows a change. If the change is the desired behaviour, accept it. If not, set the attribute explicitly.

2. New computed attributes. A resource has a new attribute that Terraform now records in state. The plan shows the attribute being added; the underlying resource is unchanged. Safe to accept.

3. Removed or renamed attributes. The new provider version removed or renamed an attribute you set. The plan fails with an error: “Unsupported attribute”. The fix is to update the configuration to use the new attribute name, or to delay the upgrade.

The provider’s release notes are the authoritative source. Reading them is not optional.

Production failure modes

Five failure modes recur:

1. Unpinned provider. The configuration has no required_providers block at all. The provider resolves to the latest version on every init. A new release can change the schema between two plans. Fix: pin the constraint, commit the lock file.

2. Lock file not committed. A developer runs init, produces a lock file, but never commits it. CI on a fresh clone resolves to a different version. Fix: commit the lock file as part of the configuration change.

3. Stale lock file with platform mismatch. A macOS developer commits a lock file with only darwin_arm64 hashes. The Linux CI runner cannot verify and downloads a new hash, producing a noisy diff. Fix: lock for every platform the team uses.

4. Forcing an upgrade without reading the changelog. A team sets version = ">= 5.0" and init -upgrade jumps to 5.99. The plan shows 200 changes because the provider has added and renamed attributes. Fix: read the release notes before upgrading, and give the constraint a tighter ceiling — ~> 5.80.0 admits 5.80.x patches and nothing else.

5. Provider yanked from the registry. HashiCorp can yank a release after publication. A lock file that pins the yanked version fails init because the registry no longer serves it. Fix: bump the constraint and re-lock.

Operational guidance

For a production Terraform estate:

  • Pin every provider. No exceptions. The constraint is the first line of defence; the lock file is the second.
  • Commit the lock file. Every commit that changes the constraint should change the lock file in the same commit. Reviewers can audit both.
  • Lock for every platform the team uses. A single missing platform causes noisy CI diffs.
  • Upgrade deliberately. An upgrade is a pull request. The PR contains the constraint change, the lock file change, and the plan output.
  • Read the release notes before merging. Especially for major version bumps. Provider authors document breaking changes; the team should not be surprised by them.

What comes next

The next lesson is on provider aliases — how to configure multiple instances of the same provider for multi-region and multi-account work.

Verification

Knowledge check · 6 questions

  1. Q1. What does the version constraint `~> 5.80` mean?

  2. Q2. What is the role of `.terraform.lock.hcl`?

  3. Q3. The `.terraform.lock.hcl` file should be committed to Git.

  4. Q4. What does `terraform init -upgrade` do?

  5. Q5. Which of the following are production risks of an unpinned provider? (Select all that apply.)

  6. Q6. A team upgrades the AWS provider from 5.80 to 5.99 and the plan now proposes 50 unintended changes. The release notes mention new default values for several attributes. What is the right action?

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