3.1 Install and Version Terraform Providers
Key Takeaways
- Declare each provider's source and version in terraform { required_providers { } }; the version argument on a provider block is deprecated
- A source such as hashicorp/aws means registry.terraform.io/hashicorp/aws; omitting source still implies the hashicorp namespace
- terraform init downloads plugins into .terraform/providers and records the exact version plus package checksums in .terraform.lock.hcl
- Commit .terraform.lock.hcl so Terraform CLI and HCP Terraform install the same provider packages on every run
- terraform init -upgrade ignores existing lock-file selections and chooses the newest versions that still match your constraints
3.1 Install and Version Terraform Providers
Quick Answer: Declare every provider's
sourceandversioninsideterraform { required_providers { ... } }. Runterraform initto download plugins into.terraform/providersand record exact versions plus checksums in.terraform.lock.hcl. Commit the lock file. Useterraform init -upgradeonly when you intend to move to newer versions that still match those constraints.
Objective 2a on Terraform Associate (004) asks you to install and version Terraform providers. The product version on the exam is Terraform 1.12. Providers remain separately released plugins: Terraform core does not ship the AWS, Azure, Kubernetes, or Datadog APIs. You tell Terraform which plugin to fetch, which registry namespace owns it, and which versions are acceptable. Then terraform init performs the download.
Official reference: Provider Requirements and Dependency Lock File.
Why this objective appears on 004
A configuration that works on one laptop but silently upgrades hashicorp/aws on a teammate's machine or on HCP Terraform is a production incident. HashiCorp tests whether you can write a modern required_providers block, read a source address, choose a version constraint, explain what the lock file pins, and know when -upgrade is the right flag. If you still put version inside provider "aws" { }, you are answering from a deprecated model.
The required_providers block
Provider requirements belong in a required_providers block nested inside the top-level terraform block:
terraform {
required_version = ">= 1.12.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
Each argument name (aws, random) is the provider's local name in this module. The object has two fields you must be able to name on the exam:
| Field | Role | Example |
|---|---|---|
source | Global source address Terraform uses to find the plugin | hashicorp/aws |
version | Constraint describing acceptable plugin versions | ~> 5.0 |
Outside required_providers, every provider block, provider meta-argument, and implied resource-type prefix uses the local name, not the full registry path. Use the provider's preferred local name (the prefix on its resource types) whenever you can so aws_instance automatically selects the aws provider.
required_version constrains the Terraform CLI. It does not constrain provider plugins. Provider versions belong only in required_providers.
Source addresses
A source address is [<HOSTNAME>/]<NAMESPACE>/<TYPE>:
- Hostname defaults to
registry.terraform.iowhen omitted. - Namespace is the publishing organization (
hashicorp, a partner such asdatadog, or a community account). - Type is the short name, usually the preferred local name (
aws,random,kubernetes).
hashicorp/aws is the shortened form of registry.terraform.io/hashicorp/aws. Official HashiCorp providers live in the hashicorp namespace on the public Terraform Registry. Partner and community providers live in other namespaces. If you omit source, Terraform 1.12 still implies registry.terraform.io/hashicorp/<LOCAL NAME> — a backward-compatibility shortcut from the 0.13 transition. That shortcut is why a third-party provider declared only as datadog = { version = "~> 3.0" } looks for hashicorp/datadog and fails. Always write an explicit source in any module that targets Terraform 1.12.
HashiCorp versus other namespaces
| Kind | Typical source | What terraform init does |
|---|---|---|
| Official HashiCorp | hashicorp/aws, hashicorp/random, hashicorp/archive | Downloads from the public registry hashicorp namespace |
| Partner / third-party | datadog/datadog, cloudflare/cloudflare | Downloads from that organization's namespace — you must set source |
| Private / in-house | app.terraform.io/my-org/internal or a custom hostname | Uses HCP Terraform's private registry or another registry host |
Installing "from HashiCorp" means the namespace is hashicorp on registry.terraform.io. It does not mean those APIs live inside the terraform binary.
Version constraints
Constraints are string literals. You can combine comparison conditions with commas (">= 5.0, < 6.0").
| Operator | Meaning | Example result |
|---|---|---|
= or no operator | Exact version only | = 5.70.0 installs only 5.70.0 |
>= / > / <= / < | Comparison | >= 5.0 allows 5.0 and newer |
~> | Only the right-most component may increment | ~> 5.0 allows 5.1 and 5.70, not 6.0; ~> 5.70.0 allows 5.70.x only |
!= | Exclude one version | Combined with other conditions |
~> 1.0.4 allows 1.0.5 and 1.0.10 but not 1.1.0. ~> 1.1 allows 1.2 and 1.10 but not 2.0. Pre-release versions such as 1.2.0-beta match only an exact = constraint; they do not satisfy >= or ~>.
HashiCorp's published guidance that still applies on Terraform 1.12:
- Root modules (the directory where you run
terraform apply) should set a~>constraint so a routineinit -upgradecannot jump a major line. - Reusable modules should set a minimum with
>=and let the root module cap the maximum. Tight~>constraints inside every child module force consumers to bump many modules at once.
What terraform init installs
terraform init is the command that installs providers. It reads required_providers from the root and every child module, intersects those constraints, consults .terraform.lock.hcl if the file exists, downloads the selected plugin packages, writes binaries under .terraform/providers/, and creates or updates the lock file.
A typical plugin path after a successful init looks like:
.terraform/providers/registry.terraform.io/hashicorp/aws/5.70.0/darwin_arm64/terraform-provider-aws_v5.70.0_x5
The layout is <hostname>/<namespace>/<type>/<version>/<os>_<arch>/. .terraform/ is a local cache. You do not commit it. HCP Terraform (the current name for the product formerly called Terraform Cloud) and Terraform Enterprise install providers on every run using the lock file when one is present; they do not reuse your laptop's .terraform directory.
If neither a lock file nor a version constraint exists, Terraform selects the newest published version. That is convenient in a scratch directory and dangerous in production.
The dependency lock file
.terraform.lock.hcl belongs to the configuration as a whole, in the root-module working directory. It uses HCL syntax, but it is not a Terraform language .tf file — the .hcl suffix marks that difference. As of Terraform 1.12, and still in current HashiCorp documentation, the lock file tracks provider selections only. It does not remember remote module versions; module pins still require an exact version argument on the module block.
Each provider entry records:
- version — the exact version Terraform selected
- constraints — the constraint string it considered (documentation for humans; later installs reuse the recorded version)
- hashes — checksums for the selected packages (trust-on-first-use)
You commit .terraform.lock.hcl so CLI, HCP Terraform, and Terraform Enterprise all install the same plugin bits. That is how a teammate on Linux and a pipeline on linux_amd64 stay aligned, provided the lock file includes checksums for those platforms. Packages installed from the public registry with signed checksums usually contribute multi-platform hashes on first install. If you install from a filesystem mirror, you may need terraform providers lock to populate other platforms.
If terraform init changes the lock file, it tells you to review and commit the change. Treat that diff like any other dependency bump.
terraform init -upgrade
Without -upgrade, a recorded lock-file version is reused even when a newer version matches ~> 5.0. With -upgrade, Terraform disregards existing selections, picks the newest version that still satisfies every module's constraint, and rewrites the lock file. -upgrade does not upgrade Terraform CLI itself, and it does not ignore your constraints. To move from AWS provider 5.x to 6.x you change the constraint first, then run terraform init -upgrade.
Deprecated: version on the provider block
provider "aws" {
version = "~> 5.0" # deprecated — do not use this on 004
region = "us-east-1"
}
The version argument on provider is deprecated and HashiCorp documents that it will be removed. You also cannot combine alias and version in the same provider block. The provider block configures how the plugin authenticates and which region or project it targets. Versioning belongs in required_providers. On Terraform 1.12 that split is the expected 004 answer.
004 traps for objective 2a
required_versionis not a provider pin..terraform.lock.hclis not state and is not optional documentation.- Omitting
sourceonly works for HashiCorp-namespace providers. .terraform/providersis a cache; the lock file is the pin you commit.terraform init -upgradestill honors~>/>=/=— it is not a blank check.
On Terraform 1.12, where do you declare a provider's source address and version constraint?
What does .terraform.lock.hcl pin, and why should a team commit it?
What does terraform init -upgrade do to provider selections?