Chapter 7: Providers in Depth
Topic 45

Provider Version Constraints

VersioningDependencies

Providers evolve fast. The AWS provider ships a release most weeks, tracking new AWS services and arguments almost as they land, and every few quarters it cuts a major version that carries breaking changes — renamed arguments, removed attributes, altered defaults. Constraining the provider version in required_providers is what keeps an init from silently pulling a breaking 7.0 into a config written for 6.x.

This is the same version-constraint syntax modules use, applied to the most volatile dependency in your project. The AWS provider changes far more often than Terraform core does, so the constraint here earns its keep more than almost anywhere else in the config.

Declaring Provider Constraints

The constraint lives in the required_providers block inside the terraform block. Each provider names its source — the registry address, hashicorp/aws — and a version constraint. The source is not optional cosmetics: without it Terraform guesses the namespace, which breaks for any non-HashiCorp provider and produces confusing init errors.

versions.tf — constraining the AWS provider and core
terraform {
  required_version = "~> 1.13"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

Why AWS Provider Versions Matter

The AWS provider's release cadence is its own argument for pinning. A new release most weeks means an unconstrained init can pull a different version on Tuesday than it did on Monday. New AWS resources arrive tied to specific provider versions, so the feature you need may require a bump — and a major version drops support for arguments your config still uses. Both directions, too old and too new, are real risks, which is why the constraint sets a range rather than chasing latest.

The Pessimistic Constraint for Providers

The ~> operator — the pessimistic constraint — is the right tool here. ~> 6.0 allows any 6.x release, so you take patches and new minors automatically, but it refuses 7.0 and the breaking changes a major version brings. You get bug fixes and new resources within 6.x without an unreviewed jump across the major-version boundary that would land renamed or removed arguments on you mid-sprint.

what ~> 6.0 allows and blocks
# version = "~> 6.0"
# 6.31.0  -> allowed   (a 6.x minor)
# 6.99.1  -> allowed   (still within 6.x)
# 7.0.0   -> blocked   (breaking major, needs a reviewed bump)

Upgrading the Provider

A major version bump is a deliberate, reviewed change, not a side effect of running init. Read the provider's upgrade guide first — it lists the renamed and removed arguments — then widen the constraint to the new major, run init -upgrade to move the lock, and read the resulting plan carefully for replacements before applying. Treating the bump as a normal PR, with the plan attached, is what keeps a 6-to-7 move from breaking production by surprise.

moving to the next major version, deliberately
# 1. read the v7 upgrade guide on the registry
# 2. widen the constraint:  version = "~> 7.0"
# 3. rewrite the lock within the new range
terraform init -upgrade
# 4. review the plan for renamed args and replacements
terraform plan

required_version vs Provider Version

Two separate constraints guard two separate things. required_version pins Terraform core — the binary that runs the plan — and the provider's version pins the AWS plugin. Constraining only the provider leaves core version skew in place, where one teammate runs 1.7 and another 1.9 and a state file gets written by a version others cannot read. Pin both, in the same terraform block, so the whole team and CI run identical core and identical provider.

Common Mistakes
  • Leaving the AWS provider unconstrained, so a routine init jumps to a new major version mid-sprint and breaks the config on renamed arguments.
  • Pinning to an exact old provider version forever, losing access to new AWS resources and the bug fixes that ship in later 6.x releases.
  • Bumping a major provider version without reading its upgrade guide, then hitting renamed or removed arguments only after the plan errors.
  • Constraining the provider but omitting required_version for Terraform core, so version skew across the team persists and writes an unreadable state.
  • Using >= 6.0 instead of ~> 6.0, which permits 7.0 and reintroduces exactly the unreviewed major jump the constraint was meant to block.
Best Practices
  • Constrain the AWS provider with ~> and commit the lock file, so everyone resolves the same version within an allowed range.
  • Read the provider's upgrade guide before any major bump and do the move as a reviewed change with the plan attached.
  • Keep both required_version for core and the provider version constraint in place, in the same terraform block.
  • Upgrade regularly in small steps rather than letting the provider fall years behind and turning the eventual jump into a migration project.
  • Always declare an explicit source for every provider, so Terraform never guesses the namespace and fails on non-HashiCorp providers.
Comparable tools Pulumi pins provider package versions in its language manifest CloudFormation has no user-managed provider versions — AWS controls behavior npm and Bundler version constraints follow the same discipline

Knowledge Check

What does the constraint ~> 6.0 on the AWS provider allow and block?

  • It allows any 6.x release but blocks 7.0 and its breaking changes
  • It pins exactly 6.0.0 and rejects every later patch or minor release
  • It allows any version 6.0 or higher, including the 7.x major line
  • It allows only new major versions and blocks every minor release

Why is the AWS provider version especially worth constraining?

  • It ships a release most weeks and cuts breaking major versions periodically
  • It is the only provider Terraform ever downloads during the init step
  • Its release versions are chosen by AWS itself and cannot be pinned at all
  • It almost never changes, so the constraint is purely documentation

Why is pinning to an exact old provider version forever also a risk?

  • You lose access to new AWS resources and bug fixes shipped in later releases
  • Terraform simply refuses to run against any exactly pinned provider version
  • An exact pin forces a breaking major upgrade on the very next init regardless
  • The lock file is unable to record an exactly pinned provider version

What is the difference between required_version and a provider version constraint?

  • required_version pins Terraform core; the provider version pins the AWS plugin
  • They are two aliases for the same single setting and only one is ever needed
  • required_version pins the provider plugin while version pins the lock file
  • Both of them pin Terraform core, so the provider version is left unconstrained

You got correct