Domain 2 of 8 · Chapter 1 of 4

Install and version Terraform providers

Declaring provider requirements

A provider block configures a plugin, but Terraform must first know which plugin to fetch, and that is what a required_providers block declares, nested inside the top-level terraform block:

terraform {
  required_version = "~> 1.12"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.1"
    }
  }
}

Each entry maps a local name (the key, here aws and random) to an object holding a source address and an optional version constraint[1]. Placing required_providers at the top level of a file, or inside a provider block, is invalid: it belongs only in the terraform block. Naming that source, constraining the version, and letting terraform init install and lock the result are the three moves this page walks through.

The source address is not a URL

A source value is a registry address, not a download link. It has the form [HOSTNAME/]NAMESPACE/TYPE, three slash-delimited parts[1]. hashicorp/aws names the aws type in the hashicorp namespace; the hostname is omitted, so Terraform resolves it against the default public registry registry.terraform.io. Writing the hostname explicitly (registry.terraform.io/hashicorp/aws) is legal but redundant for public providers.

The local name is a handle, not the type

The key you choose is the provider's local name, and it is what the rest of the module uses. It prefixes that provider's resource types (aws_instance comes from the provider whose local name is aws) and it is the label a provider "aws" block configures. By convention the local name matches the source TYPE, and using the provider's preferred local name[1] keeps a configuration clear, but the two are technically independent and can differ.

required_version is a separate lever

Alongside required_providers, the terraform block accepts required_version[2], which constrains the Terraform CLI (core) itself rather than any provider, using the same constraint syntax covered in the next section. If the running CLI does not satisfy it, Terraform prints an error and exits without taking any action. Keep the two straight: required_version gates the binary, and each version under required_providers gates one plugin.

terraform { } blockrequired_versionconstrains Terraform CLIrequired_providers { }aws (local name)source = hashicorp/awsversion = ~> 5.0random (local name)source = hashicorp/randomversion = >= 3.1
The terraform block: required_version gates the CLI, and each required_providers entry maps a local name to a source and a version.

Version constraints and the ~> operator

A version constraint decides which releases Terraform may select, and it is always a quoted string with the operator and the number both inside the quotes. version = ">= 3.1" is valid; version >= 3.1 and version = >= 3.1 are syntax errors[3].

The operators

Terraform supports seven constraint operators[3]: = (or no operator) for an exact version, != to exclude one, the comparisons >, >=, <, <=, and the pessimistic ~>. Separate several conditions with commas and Terraform combines them with logical AND, so version = ">= 1.2.0, < 2.0.0" means at least 1.2.0 and below 2.0.0, not either bound. Use the decision tree above to pick an operator; this section explains what each one means.

The ~> pessimistic operator

~> allows only the right-most component you wrote to increment, which makes it the workhorse for taking patch updates without breaking changes. The exact behavior is worth memorizing, because the exam probes it directly:

Constraint Allows Rejects
~> 1.0.4 1.0.5, 1.0.10 (any 1.0.x at or above 1.0.4) 1.1.0
~> 1.1 1.2, 1.10 (any 1.x at or above 1.1) 2.0

Read it as: the last number you pin is the one allowed to grow. ~> 1.0.4 fixes the minor at 0 and lets the patch float, while ~> 1.1 fixes the major at 1 and lets the minor float.

Pre-release versions are opt-in

A pre-release such as 1.2.0-beta (a version carrying a dash suffix) is never selected by a range operator[3]. >, >=, <, <=, and ~> all skip it. The only way to select one is to request that exact version with =, for example version = "= 1.2.0-beta". A plain >= 1.2.0 therefore passes over 1.2.0-beta and waits for the stable 1.2.0.

Installation sources and the lock file

Declaring a provider does not fetch it; terraform init[4] does. During init Terraform scans the configuration for every provider it references, selects a version that satisfies the constraints, downloads the matching plugin binary into the working directory, and records what it chose. You must run init before plan or apply can use any provider.

Where providers come from

Providers ship as pre-built plugin binaries; Terraform never compiles provider source code at run time. By default init downloads from the origin registry (registry.terraform.io), but a provider_installation block in the CLI configuration[5] can redirect that:

  • direct: the default, fetching each provider from its origin registry over the network.
  • filesystem_mirror: read providers from a local directory, which is how air-gapped installs work.
  • network_mirror: read providers from an HTTPS server, regardless of their origin registry.

Independently, a shared plugin cache set through plugin_cache_dir or the TF_PLUGIN_CACHE_DIR environment variable[5] lets init reuse an already-downloaded binary instead of fetching it again. The -plugin-dir=PATH flag forces init to read plugins only from one directory, as if it were the sole filesystem_mirror.

The dependency lock file

Each init creates or updates .terraform.lock.hcl[6] in the working directory. It records the exact provider versions selected together with their checksums (hashes), so a teammate or CI system running init installs the same plugins. Commit this file to version control, exactly as you would the configuration itself.

Two limits matter on the exam. First, the lock file tracks providers only: it does not remember module versions, so Terraform always re-selects the newest module version allowed by that module's own constraint. Second, a plain terraform init reuses the versions already recorded in the lock file even when newer allowed releases exist. To move forward you run terraform init -upgrade[4], which re-evaluates the constraints, selects newer permitted versions, and rewrites the lock file.

terraform initRead required_providersand version constraintsSelect a versionthat meets the constraintsInstall plugin binaryregistry, mirror, or cacheWrite .terraform.lock.hclversions and checksums
terraform init reads the requirements, selects a version, installs the plugin from a registry, mirror, or cache, then writes the lock file.

How this appears on the exam

This objective is tested less by definitions than by spotting the plausible-but-wrong option. The recurring traps, each a restatement of a rule established above:

  • source looks like a URL. A stem may offer source = "https://github.com/hashicorp/terraform-provider-aws". It is wrong: source is a registry address [HOSTNAME/]NAMESPACE/TYPE, and omitting the hostname implies registry.terraform.io, not that a URL is expected.
  • required_version versus provider version. A stem blurs the two. required_version in the terraform block constrains the Terraform CLI; the version inside a required_providers entry constrains that one plugin. "Which Terraform binary can run this?" is the former; "which AWS provider release?" is the latter.
  • ~> arithmetic. Expect "does ~> 1.1 allow 2.0?" (no) or "does ~> 1.0.4 allow 1.1.0?" (no). Re-derive from the rule: only the right-most written component may increment.
  • When providers download. The answer is terraform init, never plan or apply. A plain re-run of init does not upgrade; only terraform init -upgrade re-selects newer versions.
  • What the lock file locks. .terraform.lock.hcl pins providers and their checksums, not modules. "Commit it to version control" is correct; "it also pins module versions" is the distractor.
  • AND, not OR. Comma-separated conditions such as ">= 1.2.0, < 2.0.0" must all hold at once; reading them as alternatives is the wrong answer.

Provider version constraint operators

AspectExact `= x.y.z`Pessimistic `~> x.y`Range `>= x, < y`
MeaningOnly that one versionRight-most component may incrementAny version inside the bounds
Example allows`= 5.2.0` selects only 5.2.0`~> 1.1` allows 1.2 and 1.10`>= 1.2.0, < 2.0.0` allows 1.9.0
Example rejectsEvery other version`~> 1.1` rejects 2.0; `~> 1.0.4` rejects 1.1.0Rejects 2.0.0 and 1.1.0
Pre-release `-beta`Matched only by exact `=`Never matchedNever matched

Decision tree

Need one exact build?= (exact version)YesNoOnly patch/minor bumps?~> (pessimistic)YesNoBound an open range?>= , < (range)YesNo!= (exclude a build)

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

required_providers lives inside the terraform block

The required_providers block must be declared inside the top-level terraform block, and each entry maps a module-local name to an object containing a source argument and an optional version argument. It is not valid at the top level of a file or inside a provider block.

Trap Placing required_providers at the top level of the file or inside a provider block instead of nested in the terraform block.

8 questions test this
Provider source address format

A provider source address has the form [HOSTNAME/]NAMESPACE/TYPE (for example hashicorp/aws); when the hostname is omitted Terraform defaults to the public registry registry.terraform.io.

Trap Thinking source is a download URL, or that the hostname registry.terraform.io must always be written explicitly.

10 questions test this
The local name is the required_providers key

The key of each required_providers entry is the provider's local name, which is used to reference that provider elsewhere in the module (in provider blocks and as the prefix of its resource types). By convention it matches the source TYPE but it can differ.

Trap Assuming the local name must exactly equal the TYPE in the source address.

terraform required_version setting

Inside the terraform block, required_version constrains which Terraform CLI/core version may run the configuration using the standard constraint operators (=, !=, >, >=, <, <=, ~>). It applies only to the Terraform binary and is distinct from the per-provider version constraints in required_providers; if the running CLI version does not satisfy the constraint, Terraform prints an error and exits immediately without taking any action.

Trap required_version constrains the Terraform CLI, not providers — provider versions are pinned separately in required_providers under the terraform block.

6 questions test this
Version constraint operators

Provider version constraints support = (or no operator, exact), != (exclude), >, >=, <, <=, and ~>. Multiple constraints separated by commas are combined with logical AND, for example version = ">= 1.2.0, < 2.0.0".

Trap Treating comma-separated constraints as OR rather than AND.

5 questions test this
The ~> pessimistic operator

The ~> operator allows only the right-most specified version component to increment: ~> 1.0.4 permits 1.0.5 and later 1.0.x but not 1.1.0, while ~> 1.1 permits 1.2 and later 1.x but not 2.0.

Trap Believing ~> 1.1 allows 2.0, or that ~> 1.0.4 allows 1.1.0.

4 questions test this
Version constraints are quoted strings

The version requirement is a string assigned to version inside the provider object, with the operator and number both inside the quotes, e.g. version = ">= 3.1". Forms such as version >= 3.1 or version = >= 3.1 are invalid syntax.

Trap Writing the constraint without the = assignment or without quotes, e.g. version >= 3.1.

5 questions test this
Pre-release versions need an exact match

Pre-release versions such as 1.2.0-beta are only selected when requested with an exact = constraint; they never satisfy range operators like >=, <, or ~>.

Trap Expecting >= 1.2.0 to select a 1.2.0-beta pre-release.

terraform init installs providers

terraform init is the command that downloads and installs the provider plugins required by the configuration into the working directory; it must be run before terraform plan or terraform apply can use those providers.

Trap Thinking providers are downloaded during terraform plan/apply rather than during terraform init.

5 questions test this
Where Terraform installs providers from

Terraform can install providers directly from an origin registry (default registry.terraform.io), from a local filesystem mirror (a plugins directory), from an HTTPS network mirror, or reuse a shared plugin cache (plugin_cache_dir / TF_PLUGIN_CACHE_DIR). Terraform never compiles provider source code at runtime.

Trap Believing Terraform compiles provider source code at run time — providers are distributed as pre-built plugin binaries.

6 questions test this
The .terraform.lock.hcl dependency lock file

Running terraform init creates or updates .terraform.lock.hcl in the working directory, recording the exact provider versions selected plus their checksums (hashes); this file should be committed to version control and does not lock module versions.

Trap Assuming the lock file also pins module versions — it only locks providers.

6 questions test this
terraform init -upgrade

Without -upgrade, terraform init re-selects the versions already recorded in .terraform.lock.hcl even if newer releases exist; running terraform init -upgrade reconsiders the constraints, selects newer allowed versions, and updates the lock file.

Trap Expecting a plain terraform init to upgrade providers to the newest matching version.

References

  1. Provider Requirements
  2. The terraform block: settings and required_version
  3. Version Constraints
  4. Command: terraform init
  5. CLI Configuration: provider_installation and plugin_cache_dir
  6. Dependency Lock File