skip to content

In a Terraform version constraint, what does the pessimistic operator `~>` allow, and how does `~> 5.2` differ from `~> 5.2.0`?

level: middleimportance: should knowfreq 62%

answer

  1. count the components you wrote
  2. rightmost one is allowed to move
  3. two dots is tighter than one
  4. fewer digits, wider door
  5. patch-only versus minor-and-patch

basics

~20 s

~> allows increments of the rightmost component you wrote, and nothing above it. ~> 5.2 means 5.2.0 up to but excluding 6.0.0, so any 5.x minor. ~> 5.2.0 means 5.2.0 up to but excluding 5.3.0, so patches only.

solid answer

~40 s

The pessimistic constraint operator `~>` permits the **rightmost specified component** to increase and holds everything to its left fixed. So `~> 5.2` allows minor bumps within major 5 — `>= 5.2.0, < 6.0.0` — while `~> 5.2.0` allows only patch bumps — `>= 5.2.0, < 5.3.0`. The number of components you write is the whole meaning of the constraint, which is what trips people up: the two look almost identical and behave very differently. Version constraints in general are a comma-separated list of conditions that must all hold, so `">= 5.2.0, < 5.40.0"` is a perfectly good alternative when you want a range `~>` cannot express. Whatever the constraint, the exact release chosen is recorded in `.terraform.lock.hcl` — the constraint bounds the choice, the lock file is what actually reproduces it.

code

hcl · 21 lines
hcl
terraform {
  required_providers {
    # minor and patch may move: >= 5.2.0, < 6.0.0
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.2"
    }

    # patch only: >= 5.2.0, < 5.3.0
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6.0"
    }

    # explicit range when ~> cannot express it
    null = {
      source  = "hashicorp/null"
      version = ">= 3.2.0, < 3.9.0"
    }
  }
}

go deeper

for a junior

Memorise the two shapes: ~> 5.2 allows any 5.x from 5.2 up, ~> 5.2.0 allows only 5.2.x. Say which one you would put in a root module and why.

for a middle

Explain the mechanical rule — the rightmost written component may increase — and translate any constraint into its equivalent >=/< pair on demand.

for a senior

Show judgment on tightness: permissive floors in shared modules, a sensible ceiling at the root, no redundant exact pins because the committed lock file is what actually reproduces a run.

for a principal

Own the upgrade policy across the estate — how major provider bumps are scheduled and tested, when a != exclusion is acceptable, and how constraints and lock-file bumps are automated without surprising teams.

## Constraints are ranges, not pins A Terraform version constraint is a string of one or more comma-separated conditions, all of which must be satisfied. The operators are `=` (or bare version, meaning exact equality), `!=`, `>`, `>=`, `<`, `<=` and `~>`. Providers use semantic versioning: `MAJOR.MINOR.PATCH`, where major bumps may break, minor adds features, patch fixes bugs. ```hcl version = "~> 5.2" # >= 5.2.0, < 6.0.0 version = "~> 5.2.0" # >= 5.2.0, < 5.3.0 version = ">= 5.2.0, < 5.40.0" # explicit range version = "5.31.0" # exact ``` ## How `~>` actually reads The rule is mechanical: **the last component you wrote may increase; every component to its left is fixed.** Count the dots. - `~> 5.2` — two components. The minor (`2`) may increase, the major (`5`) is fixed. Allowed: 5.2.0, 5.9.4, 5.80.1. Rejected: 6.0.0, 5.1.9. - `~> 5.2.0` — three components. The patch may increase, major and minor are fixed. Allowed: 5.2.0, 5.2.17. Rejected: 5.3.0, 6.0.0. That is why it is called *pessimistic*: it assumes anything beyond the component you allowed to move might break you. It is the standard way to say "take bug fixes automatically, never take a breaking change silently". A useful sanity check when reading someone else's code: `~> 5.2` is **more** permissive than `~> 5.2.0`, even though it has fewer digits. Fewer components means a wider door. ## Choosing the tightness For the root module of a real deployment, `~> 5.0` or `~> 5.2` is the usual choice: you accept new features and fixes within a major version, and a major upgrade becomes a deliberate, reviewed change. Providers with large resource surfaces — the AWS provider being the obvious case — do ship regressions in minor releases, so some teams tighten to `~> 5.2.0` for the most critical root modules. For **reusable child modules**, be permissive: `>= 5.0` or `~> 5.0`. Terraform intersects all constraints in a configuration and must find a single version that satisfies every module, so an exact pin buried in a shared module makes the whole configuration unupgradable and produces a confusing `init` failure naming modules the operator did not write. Exact pinning at the root (`version = "5.31.0"`) is redundant in modern Terraform and mildly harmful: the lock file already pins the exact build with checksums, and an exact constraint means every patch release needs a code change and a review, so teams stop taking fixes at all. ## The division of labour with the lock file The constraint answers "what would we accept?" The lock file answers "what did we actually select, and what is its checksum?" Under `~> 5.2`, `terraform init` picks the newest 5.x release available and writes it into `.terraform.lock.hcl`; later runs reuse that recorded version even though newer 5.x releases have since appeared. Moving forward within the constraint requires `terraform init -upgrade`, which re-resolves and rewrites the lock file. So a wide constraint does **not** mean you silently drift onto new provider versions — it means you *may* move when someone deliberately upgrades. ## Practical gotchas Pre-release versions such as `5.32.0-beta1` are never matched by an ordinary constraint; they must be requested exactly. Constraints cannot use variables or any expression — they must be literal strings, because Terraform evaluates them before the rest of the configuration is loaded. And `~> 5` with a single component is legal but nearly meaningless: it allows the major to move, so it permits 6.0.0 and beyond. If you catch that in review, treat it as a bug.

  • Is a wide constraint like `~> 5.0` dangerous because CI might silently pick up a new provider version?
    No, as long as the lock file is committed. `terraform init` reuses the version already recorded in `.terraform.lock.hcl` even when newer releases satisfy the constraint. Movement only happens when someone runs `terraform init -upgrade` and commits the rewritten lock file. The constraint sets the ceiling for that deliberate upgrade; the lock file governs every ordinary run.
  • Why should a shared child module avoid pinning a provider to an exact version?
    Because Terraform must select one provider version satisfying every module in the configuration. An exact pin inside a shared module intersects to a single point, so any root module that needs a newer provider — for a new resource type or a fix — cannot upgrade without a new release of the shared module. Child modules should state a floor, such as `>= 5.0`.
  • How would you express "any 5.x, but skip the known-bad 5.44.0"?
    Combine conditions in one constraint string: `"~> 5.0, != 5.44.0"`. All comma-separated conditions must hold, so this allows the whole 5 major line while excluding one release. It is a reasonable short-term measure; the durable fix is to move past the bad release and drop the exclusion once the floor is above it.

saying these in an interview costs you the question

  • Reading `~> 5.2` as "5.2 only"
  • Thinking more digits means a wider range
  • Pinning exact versions in shared child modules
  • Assuming a wide constraint auto-upgrades every run
  • Expecting `~>` to match pre-release versions

context