Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 071 / 100

Provider lock files in Terraform, IaC & Cloud Automation

Reproducible provider versions.

Expona Admin6 min read

Introduction

We've all been there: you deploy infrastructure from your laptop, it works perfectly, then a teammate runs the same Terraform configuration and gets a different result because Terraform selected different provider versions. Provider lock files in Terraform, IaC & Cloud Automation solve exactly this problem by pinning the exact versions and cryptographic hashes of providers used in your infrastructure code. This article covers how to manage, commit, and maintain .terraform.lock.hcl files to ensure reproducible deployments across your team and CI/CD pipelines.

What is .terraform.lock.hcl and Why It Matters

The .terraform.lock.hcl file is Terraform's dependency lock mechanism, similar to package-lock.json in Node.js or Gemfile.lock in Ruby. When you run terraform init, Terraform downloads providers matching your requirements in the configuration, then records the exact version and a cryptographic hash of the downloaded binary in the lock file. This guarantees that every team member, every CI/CD run, and every deployment uses identical provider versions.

Without a lock file, version constraints like version = "~> 5.0" allow flexibility but introduce non-determinism. One day you might get version 5.3.1, and a week later a colleague gets 5.4.0. If a new provider version introduces a subtle breaking change or bug, your infrastructure behavior becomes unpredictable. The lock file eliminates this uncertainty by enforcing exact versions everywhere.

The Structure of .terraform.lock.hcl

Let's examine what a real lock file looks like:

hcl
# This file is maintained automatically by "terraform init".
# Manual edits may be made below if the lock entry, the arguments and values
# are changed, though the source of truth remains in the configuration.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.38.0"
  constraints = "~> 5.0"
  hashes = [
    "h1:abc123def456ghi789jkl012mno345pqr678stu901vwx234yz+789=",
    "zh:1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
    "zh:fedcba0987654321fedcba0987654321fedcba0987654321fedcba09876543",
  ]
}

provider "registry.terraform.io/hashicorp/random" {
  version     = "3.6.0"
  constraints = "~> 3.0"
  hashes = [
    "h1:xyz789abc123xyz789abc123xyz789abc123xyz789abc123xyz789abc12=",
    "zh:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fed",
  ]
}

Each provider block records three key pieces of information:

  • version: The exact version Terraform downloaded and validated.
  • constraints: The original constraint from your configuration, preserved for reference.
  • hashes: Multiple hash formats (platform-specific and generic) to verify the binary hasn't been tampered with and matches across all platforms.

The hashes serve as a security mechanism. When Terraform runs init with an existing lock file, it verifies that the downloaded provider binary matches the recorded hashes, preventing supply-chain attacks or accidental provider corruption.

Committing the Lock File to Git

The lock file must be committed to version control alongside your Terraform code. Treat it exactly like you would package-lock.json: never modify it manually, never add it to .gitignore.

Here's the typical workflow for a repository structure:

hcl
terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── .terraform.lock.hcl        # Commit this
├── terraform.tfvars           # Usually NOT committed
└── .gitignore

Your .gitignore should exclude the .terraform/ directory (which contains downloaded provider binaries) but always include .terraform.lock.hcl:

hcl
.terraform/
*.tfstate
*.tfstate.*
.DS_Store
terraform.tfvars
# DO NOT ignore .terraform.lock.hcl

When a colleague clones the repository and runs terraform init, Terraform reads the lock file and downloads the exact same provider versions you used. This consistency across developers eliminates the "works on my machine" problem for infrastructure code.

Cross-Platform Lock Files with terraform providers lock

Here's where provider lock files become particularly powerful in modern infrastructure teams: the terraform providers lock command ensures your lock file contains hashes for multiple operating systems and architectures, even if you're developing on macOS but deploying from Linux runners in your CI/CD pipeline.

Suppose you develop on an Apple Silicon Mac but your CI/CD runs on an x86_64 Linux container. Without cross-platform hashes, Terraform would re-download providers on every CI run, defeating the purpose of the lock file. With terraform providers lock, you can preemptively generate hashes for all required platforms:

hcl
terraform providers lock \
  -platform=darwin_arm64 \
  -platform=linux_amd64 \
  -platform=windows_amd64

This command updates .terraform.lock.hcl with hashes for macOS (ARM64), Linux (x86_64), and Windows (x86_64). After running this, when a Linux CI runner executes terraform init, it finds a matching hash in the lock file and uses the cached provider instead of re-downloading. The same applies for any other team member on a different OS.

A practical example from a real deployment workflow:

  1. Developer on macOS runs terraform providers lock -platform=darwin_arm64 -platform=linux_amd64.
  2. Lock file now contains hashes for both platforms.
  3. Developer commits .terraform.lock.hcl.
  4. CI runner (Linux) runs terraform init and finds hashes for linux_amd64.
  5. Provider is validated and used without re-downloading.

This is particularly important in regulated environments or when bandwidth is constrained. The cross-platform hashes also ensure that everyone—regardless of their development OS—is using the exact same provider bytecode, eliminating OS-specific bugs or behavioral differences.

Updating Providers with terraform init -upgrade

Over time, you'll want to update to newer provider versions for bug fixes, security patches, or new features. The terraform init -upgrade command safely updates the lock file while respecting your version constraints.

Let's say your configuration specifies:

hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

And your current lock file pins version 5.38.0. Running terraform init -upgrade will check for newer versions matching the constraint ~> 5.0. If version 5.42.0 is available, it will download it, validate the hash, and update the lock file:

hcl
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.42.0"
  constraints = "~> 5.0"
  hashes = [
    "h1:newHashValue123...",
    # ... platform-specific hashes
  ]
}

The key benefit: -upgrade respects your constraints. If you specify version = "= 5.38.0" (exact version), -upgrade won't download anything. If you specify version = ">= 5.0, < 6.0", -upgrade will fetch the latest within that range. This gives you control over how aggressively you update.

In a team environment, when you upgrade providers:

  1. Run terraform init -upgrade locally.
  2. Test the configuration with terraform plan and terraform validate.
  3. Commit the updated .terraform.lock.hcl to a feature branch.
  4. Run full integration tests in CI.
  5. Merge after approval.

This workflow ensures provider updates are deliberate, tested, and reviewed before they affect production infrastructure.

Common Mistakes

Not committing .terraform.lock.hcl: If you gitignore the lock file, every terraform init becomes non-deterministic. Developers and CI runners will silently download different provider versions, leading to hard-to-debug inconsistencies. Always commit it.

Manually editing the lock file: The lock file is machine-generated. Manual edits are fragile and can introduce corrupted hashes. If you need to change provider versions, use terraform init -upgrade or modify your configuration constraints and re-initialize.

Forgetting cross-platform hashes: Developing on macOS but deploying to Linux without running terraform providers lock for both platforms means your CI/CD will re-download and re-validate providers on every run, slowing deployments and defeating the lock file's purpose.

Ignoring hash mismatches: If you see a hash mismatch error during terraform init, don't try to work around it. Investigate why the downloaded provider doesn't match the recorded hash. It could indicate a corrupted download, a man-in-the-middle attack, or a legitimate provider release that needs lock file regeneration.

Not using terraform providers lock in CI: Some teams run terraform init -upgrade in CI to "always get the latest providers." This defeats the entire purpose of the lock file. Use CI to validate that the pinned versions work, not to upgrade them.

Key Takeaways

  • The .terraform.lock.hcl file pins exact provider versions and cryptographic hashes, ensuring reproducible deployments across your team and CI/CD pipelines.
  • Always commit the lock file to version control; treat it like a core artifact of your infrastructure code.
  • Use terraform providers lock -platform=... to generate hashes for all operating systems your team uses, preventing unnecessary re-downloads in CI/CD.
  • Update providers deliberately with terraform init -upgrade, test thoroughly, and commit the updated lock file after review.
  • Never manually edit the lock file; let Terraform manage it automatically.
  • Hash mismatches are a security signal—investigate rather than ignore them.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles