What does `required_version` in Terraform's `terraform` block constrain, and how is it enforced differently from a provider version constraint?
answer
- it is about the binary, not a plugin
- checked before anything else runs
- no lock file exists for this one
- floor plus a major-version ceiling
- state remembers who wrote it last
basics
~20 srequired_version constrains the Terraform CLI itself, not a provider. Terraform checks it before doing any work and errors out if the running binary is outside the range. Unlike providers, no lock file pins the CLI — you only get a range check.
solid answer
~40 s`required_version` sits directly in the `terraform` block and takes the same constraint syntax as provider versions — `>= 1.5.0`, `~> 1.9`, and so on — but it applies to **Terraform Core**, the CLI binary. Terraform validates it very early, so an out-of-range binary fails with an unsupported-version error before it installs providers or touches a backend. The important asymmetry: providers get both a constraint *and* a lock file recording the exact build with checksums, while the CLI gets only the constraint. Nothing records which CLI version actually ran, so `required_version` can guarantee a floor but not uniformity — teams pair it with a version manager and a pinned CLI version in CI. It also cannot reference variables; it must be a literal string, because it is read before expressions are evaluated.
code
hcl · 12 linesterraform {
# Constrains the CLI binary: a floor plus a major-version ceiling.
required_version = ">= 1.5.0, < 2.0.0"
# Constrains a plugin: this one is also pinned exactly by the lock file.
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}go deeper
Know that required_version is about the Terraform CLI itself, sits in the terraform block, and makes Terraform refuse to run when your binary is outside the range.
Explain that it is checked before providers or the backend are touched, applies to every module in the configuration, and unlike providers has no lock file recording the exact build.
Show how you get real uniformity in practice: a floor and major-version ceiling in configuration, an exact CLI pin in the pipeline, and awareness that state records the writing version and blocks older binaries.
Own the upgrade cadence — how CLI bumps are rolled across many states and repositories given that state makes them effectively one-way, and where the exact version is authoritatively declared.
## Where it goes and what it means ```hcl terraform { required_version = ">= 1.5.0, < 2.0.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } ``` `required_version` is a direct argument of the `terraform` block, a sibling of the `required_providers` block rather than an entry inside it. Its value is an ordinary version constraint string using the same operators (`=`, `!=`, `>`, `>=`, `<`, `<=`, `~>`), and it describes which **Terraform CLI** releases may operate on this configuration. ## Enforcement: early and total Terraform evaluates this constraint before it does real work — before provider installation and before backend initialization. If the running binary falls outside the range, the command stops with an unsupported Terraform Core version error naming the constraint and the running version. There is no override flag and no soft-fail mode; that bluntness is the feature, because the alternative is an old CLI producing a plan against a configuration using syntax it does not understand. Every module can declare its own `required_version`, including child modules, and all of them must be satisfied by the single binary you are running. A shared module with a tight ceiling therefore blocks upgrades for every consumer, which is the same antisocial pattern as an exact provider pin in a shared module. Because the constraint is read before expressions are evaluated, it cannot reference variables, locals or any function — it must be a literal string. ## The asymmetry with providers | | Providers | Terraform CLI | | --- | --- | --- | | Declared as | `required_providers` entry with `source` + `version` | `required_version` | | Range check | yes | yes | | Exact build recorded | yes — `.terraform.lock.hcl` | no | | Checksum verified | yes | no | | Auto-installed | yes, by `init` | no — you bring your own binary | That last row is the practical point. Terraform will download and verify a provider for you; it will not install a different copy of itself. So `required_version` is a *guard*, not a *pin*: it stops the clearly wrong binary, but three engineers inside the same range are still running three different builds. Teams that want real uniformity add a version manager driven by a `.terraform-version` file in the repo, and pin the exact CLI version in the CI job that runs plan and apply — the CI pin is the one that matters, since that is where production changes are actually made. ## The one-way ratchet through state There is a second, softer enforcement mechanism people forget. Terraform records in the state snapshot which version last wrote it, and an older CLI refuses to operate on state written by a newer one. So the moment one engineer applies with a newer minor release, everyone else on that state must upgrade too. Practically, CLI upgrades are a team-wide event per state, not an individual choice — which is exactly why `required_version` usually expresses a floor (`>= 1.5.0`) that gets raised deliberately, plus a major-version ceiling. ## Choosing a constraint `>= 1.5.0, < 2.0.0` is the common shape: a floor at the release whose features you actually use — `import` blocks, `check` blocks, the test framework, whatever the configuration depends on — and a ceiling at the next major version, because Terraform's 1.x line promises configuration compatibility within the major but makes no promise across it. Pinning the CLI to an exact version in configuration is usually too strict, since the constraint is shared by every consumer of the module and cannot be overridden per environment; make the exact choice in the CI job instead.
- If required_version already sets a floor, why do teams also pin the CLI version in CI?Because the constraint is a range and nothing records which build ran. Two runners inside `>= 1.5.0` can differ, and minor releases can change plan output formatting, upgrade state, or add behaviour that then blocks older binaries. Pinning the exact version in the pipeline makes plan and apply reproducible and makes upgrading an explicit, reviewable change to the pipeline definition.
- What happens if a colleague applies with a newer Terraform CLI than yours?The state snapshot records the version that wrote it, and your older CLI refuses to operate on it — it will not risk reading a format it may not fully understand. In practice CLI upgrades are per-state, team-wide events: once someone applies with the newer version, everyone sharing that state upgrades. It is a good reason to bump versions deliberately rather than ad hoc.
- Can required_version be set from a variable so each environment picks its own?No. The `terraform` block is read before variables and expressions are evaluated, so `required_version` must be a literal constraint string. Environment-specific CLI choices belong outside the configuration — in the CI job definition or a version-manager file — not in the block itself.
saying these in an interview costs you the question
- Thinking required_version pins a provider version
- Expecting Terraform to install the required CLI itself
- Believing the lock file records the CLI version
- Setting an exact CLI pin inside a shared module
- Assuming an older CLI can still read newer state