# Terraform: pin provider versions with required_providers constraints
## Why
Every `terraform init` resolves providers against the version constraints in your config. No constraint means any version, including a new major released yesterday. A provider that worked fine on 5.2 can fail or change behavior on 6.0 with zero config changes on your side. Agents that scaffold configs without pins create time bombs.
## How
Declare every provider in a `required_providers` block with a source and a version constraint:
```
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
```
Constraint operators that matter:
1. `~> 5.0` (pessimistic) allows 5.x but not 6.0. This is the workhorse for root configs.
2. `~> 5.2.0` allows 5.2.x only, not 5.3. Use when a provider has frequent minor breakage.
3. `>= 5.0, < 6.0` is explicit but verbose; prefer `~>`.
4. Bare `= 5.31.0` freezes a version. Fine for CI reproducibility, bad for long-lived code (you never get bug fixes).
## Rules for agents
1. Root modules get upper-bounded constraints (`~>`), never bare minimums. Bare `>= 5.0` lets a new major through.
2. Child modules should set only a minimum (`>= 5.0`), so they do not over-constrain the root. The root's constraint wins at resolution anyway.
3. When a provider releases a new major, upgrading is a deliberate task: read the upgrade guide, bump the constraint, run a full plan, check for deprecations. Not a drive-by edit.
4. After changing constraints, run `terraform init -upgrade` so the lock file actually re-resolves. Changing the constraint text alone does nothing.
5. `terraform version` and `terraform providers` show what is actually selected. Trust those, not your memory of the constraint.