Skip to main content
RunBook Academy

TerraformIII · Installing and Versioning TerraformProduction Terraform

required_version: The Floor

Foundation⏱ ~10 minbash

What you'll learn

  • Write the required_version block with the correct syntax and operators
  • Distinguish between a pinned version, a narrow range, and the pessimistic ~> operator
  • Recognise the warning and error that ship when a version mismatch occurs
  • Choose the right constraint shape for production (pin, range, or floor)
  • Document the upgrade window that the chosen constraint implies

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

required_version is the contract between the configuration and the operator that runs it. It says: “This configuration will only behave correctly if you run it with Terraform version X or later.” Get the constraint wrong and you either block legitimate operators or you let an outdated binary run a configuration it does not understand.

The syntax

required_version lives inside the top-level terraform { ... } block. It accepts a comma-separated list of version constraints.

terraform {
  required_version = ">= 1.9.0, < 2.0.0"
}

That single line says: “Any Terraform from 1.9.0 up to but not including 2.0.0 will run this configuration.” Operators above 2.0 fail the check; operators below 1.9 fail the check.

The constraint string is parsed by Terraform’s version constraint parser. The grammar is identical to the one used by required_providers, module versions, and any other version constraint in the configuration.

The operators

Six operators, in the order you are most likely to use them.

OperatorExampleMeaning
== 1.9.8Exact version. Useful only when you control every host.
!=!= 1.7.0Exclude one version. Rarely useful alone; common in combination.
>> 1.9.0Strictly greater than.
>=>= 1.9.0Greater than or equal. The standard floor.
<< 2.0.0Strictly less than. The standard ceiling.
<=<= 1.9.99Less than or equal. Less common.
~>~> 1.9.0Pessimistic: greater than or equal to 1.9.0, less than 1.10.0. Locks the minor version.

The right constraint shape

Three common shapes, each with a different operational story.

Pinned: = 1.9.8. The configuration runs on exactly one version. Every operator and every CI runner must be on 1.9.8. There is no upgrade story; a new version requires a coordinated change to the constraint and to every host. Useful for an immutable image that already bakes in 1.9.8.

Narrow range: >= 1.9.0, < 1.10.0. The configuration runs on the 1.9 minor line. Patches are admitted. Minor bumps are not. Upgrade story: bump the upper bound, test, deploy. This is the production default for most teams.

Floor only: >= 1.9.0. The configuration runs on anything from 1.9.0 onward, including 2.0.0 if it ever arrives. The upgrade story is implicit: every new version is admitted by default. Risky in production because a major-version bump can change resource graph behaviour.

What happens on a mismatch

When an operator runs terraform init, terraform plan, or terraform validate against a configuration whose required_version excludes the binary, Terraform checks the constraint. The behaviour differs by command.

terraform init emits a warning by default and continues:

│ Warning: Version constraints inside the configuration block
│
│   > The following version constraints are set based on the
│   > configuration. If you are using a different version, you
│   > may experience different behaviour:
│   > required_version = ">= 1.9.0, < 2.0.0"
│
│   (and 1 more warning)

It does not exit non-zero on init. The init succeeds; the incompatibility is left for plan to catch.

terraform plan emits the same warning plus a hard error on the first plan that would change a resource:

│ Error: Unsupported Terraform Core version
│
│   This configuration does not support Terraform version 1.6.6.
│   To proceed, either change the version of the Terraform binary
│   or update the configuration's required_version.

terraform validate prints the warning and exits non-zero.

In every case the message identifies the offending constraint and the offending version, which is enough to diagnose.

Production guidance

  • Default to a narrow range. >= 1.9.0, < 1.10.0 admits patches and excludes major and minor surprises. Bump the upper bound when the team has tested the new minor line.
  • Document the upgrade window. “We support Terraform 1.9.x; upgrades to 1.10.x require a deliberate change and a regression plan.” That sentence belongs in README.md.
  • Match required_version to the team’s pinned CLI. If the CI runner is on 1.9.8 and the laptop fleet is on 1.9.8, the constraint should admit 1.9.8. If the team has just standardised on 1.9.0 as the floor, pin the lower bound to 1.9.0, not 1.8.0.
  • Write pessimistic constraints with all three components. When a module uses a feature from a specific minor, ~> 1.9.0 admits patches within that minor without admitting the next minor. Written as ~> 1.9 it admits the next minor too. Be explicit.
  • Treat major-version constraints as deliberate events. A bump from < 2.0.0 to < 3.0.0 is the kind of change that warrants a configuration review, a state migration plan if necessary, and a rollback runbook. It is not an unattended upgrade.

Production failure modes

1. Floor-only constraint in production. Symptom: a new Terraform version ships with a behavioural change; the team’s configuration picks it up on the next CI run; the apply fails or, worse, produces an unexpected diff. Recovery: tighten the constraint to a narrow range; pin the team’s binary to the narrow-range lower bound.

2. Constraint that excludes the team’s pinned binary. Symptom: every CI run fails with “Unsupported Terraform Core version”. Recovery: either bump the team’s binary to a version that matches the constraint, or relax the constraint to admit the binary. Do not paper over with -ignore-version flags that do not exist; correct the source of the mismatch.

3. Hand-edited constraint without a tested plan. Symptom: a constraint bump merged without a terraform plan against staging; the next apply discovers a behaviour change from the new minor. Recovery: revert the constraint, run a full staging plan, then re-bump deliberately.

4. Mixing constraint operators inconsistently. Symptom: the configuration uses = 1.9.0 in one module and ~> 1.9.0 in another; plans diverge depending on which module runs first. Recovery: standardise on one shape per team. Most teams pick >= 1.9.0, < 1.10.0.

5. Upper bound forgotten after a major release. Symptom: a team adopts >= 1.9.0 in early 2025; Terraform 2.0 ships in 2026 with a graph rewrite; the team’s applies now use the new graph and produce unexpected diffs. Recovery: the upper bound is a discipline; add < 2.0.0 to the floor before 2.0 ships and revisit at the next minor.

6. Required_version drift between root and modules. Symptom: the root module declares >= 1.9.0; a child module declares >= 1.10.0. The child is silently ignored because the root constraint governs the run. Recovery: align constraints; the child constraint is documentation only, not enforcement.

Security and performance

  • Version constraints are not security boundaries. An operator can always edit the constraint out of the configuration, or run a build that does not read it. The constraint is for honest operators.
  • Performance. The check is a single version string comparison. It does not change apply time or plan time meaningfully.

What comes next

The next lesson covers the dependency lock file. Together, required_version (which constrains the CLI) and .terraform.lock.hcl (which constrains the providers) form the reproducibility contract for a working directory.

Verification

Run the following on your own configuration to confirm the constraint is in effect.

# READ-ONLY
terraform version
Terraform v1.9.8
# READ-ONLY: confirm the constraint matches the binary
grep required_version *.tf
versions.tf:  required_version = ">= 1.9.0, < 2.0.0"

If terraform version returns something outside the constraint, the configuration is unsafe to apply until either the binary or the constraint is changed.

# READ-ONLY: confirm the constraint is satisfied
terraform validate

A clean exit means the operator’s binary is in range.

Knowledge check · 7 questions

  1. Q1. What does required_version = '~> 1.9.0' mean?

  2. Q2. An operator runs Terraform 1.6.6 against a configuration that declares required_version = '>= 1.9.0, < 2.0.0'. What does terraform plan report?

  3. Q3. required_version = '= 1.9.8' is a safe production default for a team of 30 engineers.

  4. Q4. Which constraint shape is the production default for most teams?

  5. Q5. Which of these are correct practices for required_version in production? (Select all that apply.)

  6. Q6. An engineer upgrades the production CI runner from Terraform 1.9.8 to 1.10.0 without telling the team. The configuration declares required_version = '>= 1.9.0, < 1.10.0'. What happens on the next plan?

  7. Q7. Where does the required_version block live?

Passing score: 75%. Answers are checked in this browser.