TerraformIII · Installing and Versioning TerraformProduction Terraform
required_version: The Floor
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
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.
| Operator | Example | Meaning |
|---|---|---|
= | = 1.9.8 | Exact version. Useful only when you control every host. |
!= | != 1.7.0 | Exclude one version. Rarely useful alone; common in combination. |
> | > 1.9.0 | Strictly greater than. |
>= | >= 1.9.0 | Greater than or equal. The standard floor. |
< | < 2.0.0 | Strictly less than. The standard ceiling. |
<= | <= 1.9.99 | Less than or equal. Less common. |
~> | ~> 1.9.0 | Pessimistic: 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.0admits 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_versionto 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.0admits patches within that minor without admitting the next minor. Written as~> 1.9it admits the next minor too. Be explicit. - Treat major-version constraints as deliberate events. A bump
from
< 2.0.0to< 3.0.0is 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
Q1. What does required_version = '~> 1.9.0' mean?
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?
Q3. required_version = '= 1.9.8' is a safe production default for a team of 30 engineers.
Q4. Which constraint shape is the production default for most teams?
Q5. Which of these are correct practices for required_version in production? (Select all that apply.)
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?
Q7. Where does the required_version block live?
Passing score: 75%. Answers are checked in this browser.