Skip to main content
In this lesson we cover the Terraform block — the top-level configuration that controls Terraform itself (not the cloud resources). Use the Terraform block to declare the required Terraform version, provider requirements and versions, and backend settings for state storage. Think of it as Terraform’s global settings: configuration that affects your entire workflow and team collaboration. Below we first walk through two real-world scenarios that show why the Terraform block matters, then we explain the Terraform block structure and the common version-constraint patterns you should use.

Real-world scenarios: why the Terraform block matters

  • Scenario 1: Developer A writes and tests infrastructure code using Terraform X.Y.Z, runs a successful terraform plan, and commits the changes. Developer B later pulls the same code but has a different Terraform version installed. Developer B now sees unexpected errors and spends time troubleshooting version mismatches instead of delivering features.
Example resource blocks that might be in such a repo:
  • Scenario 2: The CloudOps team commits working code that relies on an older provider version. Later, a different team pulls the code and adds a resource that requires a newer provider. Running terraform init may download the latest provider with breaking changes, causing the configuration to fail and work to stall.
Example resource blocks illustrating provider differences:
Both scenarios are common. The Terraform block prevents these issues by enforcing consistent Terraform and provider versions and by centralizing backend settings for state storage.

What the Terraform block controls

The Terraform block defines global configuration for a Terraform project. Its main responsibilities are:
  1. required_version — which Terraform version(s) may apply the configuration.
  2. required_providers — which providers the configuration needs, including the provider source and version constraints.
  3. backend — where Terraform will store state (local or a remote backend like S3, Azure Blob, Google Cloud Storage, Terraform Cloud, etc.).
These settings are essential in team environments to prevent inconsistent behavior from different Terraform or provider versions.

Correct structure and an example Terraform block

A typical Terraform block that includes required_version, required_providers, and a backend looks like this:
Notes about this example:
  • required_version = "~> 1.10.0" ensures Terraform 1.10.x is used (patch updates allowed within 1.10).
  • The aws provider is declared with a source and a ~> 5.87.0 constraint, allowing patch updates within 5.87.x.
  • The backend "s3" block is declared inside the terraform {} block and points to a remote S3 bucket for state — recommended for team collaboration.
The backend block should be declared inside the terraform {} block. The backend determines where Terraform reads/writes state; using a remote backend is recommended for team collaboration.

Understanding version constraints

Version constraints tell Terraform which versions are acceptable for the CLI and for providers. Below are the common patterns, with definitions and HCL examples. Below are the same patterns shown with provider examples to illustrate provider constraints.
  1. Exact version
  • Syntax: "1.9.8"
  • Meaning: Only the specified version is allowed. This gives absolute reproducibility.
  1. Minimum version (>=)
  • Syntax: ">= 1.9.8"
  • Meaning: Any version equal to or newer than the specified version is allowed. Useful when you require at least a minimum version but are comfortable with newer releases.
  1. Tilde greater than (~>) — pessimistic constraint
  • Syntax: "~> 1.11.0"
  • Meaning: Allows updates only to the last specified component. "~> 1.11.0" allows 1.11.1, 1.11.2, etc., but not 1.12.0. This accepts patch fixes automatically while avoiding minor version changes that may introduce breaking behavior.

When to use each approach

  • Exact versions:
    • Use when you need strict reproducibility and full control over every change (recommended for immutable CI pipelines).
  • >= (minimum):
    • Use when you require a baseline version that contains necessary features or bug fixes, and you accept newer versions automatically.
  • ~> (pessimistic):
    • Use when you want automatic patch updates but want to avoid automatic minor version upgrades that could break behavior.

Best practices summary

  • Always declare required_version and required_providers in your root module to avoid surprises across developers and CI systems.
  • Use a remote backend (S3, Azure Blob, GCS, Terraform Cloud, etc.) for state when working in a team to prevent conflicting local state files.
  • Choose version constraints deliberately:
    • Exact for strict reproducibility,
    • >= for minimum guarantees,
    • ~> for safe automatic patch updates.
  • Pin providers by source (e.g., hashicorp/aws) and version to avoid implicit provider source changes.
  • Run terraform init after changing the terraform block or required_providers to ensure the correct providers are installed.
Understanding and using the Terraform block and correct version constraints reduces friction in team workflows and prevents the issues described at the start of this lesson. Thanks for reading this lesson.

Watch Video