A Practical Guide to Terraform Dependency Management

Published: (December 15, 2025 at 10:53 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

TL;DR

  • Treat Terraform dependency management as two different systems: providers are selected and pinned via .terraform.lock.hcl (repeatable by default), while modules are not pinned by a lock file and can drift over time unless you pin an exact version or a git ref.
  • Use bounded ranges for the Terraform CLI (required_version) and pessimistic constraints (~>) for providers in root modules.
  • In reusable sub‑modules, prefer broad minimums (plus optional upper bounds only when necessary), letting the root module do final resolution.
  • For modules, choose explicitly between exact pins for maximum reproducibility, or ~> ranges for easier upgrades (with disciplined terraform init -upgrade workflows).

Specify a version constraint, run terraform init, done—except that providers and modules follow different resolution and persistence rules. Providers are locked; modules are not. That asymmetry is why teams get surprised by “nothing changed” configurations producing different results across machines or CI runs.

In this article, a root module means the top‑level Terraform configuration you run (the directory you init/plan/apply). A reusable module means a library‑style module consumed by other configurations. We’ll build from the mechanics to a practical, testable policy for each.


The Real Problem: “Constraints” Do Not Mean “Pins”

A version constraint is a filter over acceptable versions (e.g., >= 5.0). The same operator can yield very different stability depending on whether Terraform writes down the chosen result.

Persistence differs:

ArtifactRecorded in lock file?Behaviour
ProvidersYes (.terraform.lock.hcl)Selections are locked and reused by default
ModulesNoRanges can float as new versions are published

Key insight: The same operator can yield very different stability depending on whether Terraform writes down the chosen result.


A Mental Model You Can Reason About

graph TD
    A[User defines constraints] --> B[Terraform resolves versions]
    B --> C{Provider?}
    C -->|Yes| D[Write to .terraform.lock.hcl]
    C -->|No| E[No lock file entry]
    D --> F[Future runs reuse locked version]
    E --> G[Future runs may pick newer module version]

This behavior is documented: the lock file covers providers, not modules.


Operators: What They Really Buy You

Terraform supports standard comparison operators plus the pessimistic constraint ~> (“allow changes only to the rightmost specified component”, i.e., a convenient bounded range).

How to Think About Each Operator

OperatorMeaning (operational)Primary risk
=Hard pinBlocks bug‑fix/security updates unless manually changed
>= (alone)“Anything newer is fine”Future breakage + drift; depends on lock behavior
~>Convenient bounded range (e.g., ~> 5.0 means >= 5.0.0, < 6.0.0)Easy to under/over‑constrain if you pick the wrong precision

Example Interpretations (Terraform Semantics)

Provider version constraint

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
  • ~> 5.0 translates to >= 5.0.0, < 6.0.0.
  • The lock file records the exact provider version that was selected, making subsequent runs repeatable unless terraform init -upgrade is invoked.

Module version constraint

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
}
  • The same ~> 5.0 range applies, but no lock file entry is created for the module.
  • Future runs may pick a newer 5.x release unless the version is explicitly upgraded with -upgrade or a tighter constraint is added.
Back to Blog

Related posts

Read more »