In a Terraform configuration, what does the `required_providers` block do, and why does each entry need a `source` as well as a `version`?
answer
- three parts to every entry
- local name is only local
- address has a namespace
- registry defaults to the hashicorp namespace
- source identifies, version constrains
basics
~20 srequired_providers declares every provider plugin a Terraform module depends on. The source argument is the registry address that tells Terraform exactly which plugin to download; version constrains which releases are acceptable. Without an explicit source, Terraform assumes the hashicorp namespace.
solid answer
~40 s`required_providers` lives inside the `terraform` block and lists each provider the module needs. Each entry has three parts: the **local name** (the map key, which is also the prefix on resource types — `aws` for `aws_s3_bucket`), a **`source`** address of the form `[host/]namespace/type` such as `hashicorp/aws` or `cloudflare/cloudflare`, and a **`version`** constraint such as `~> 5.0`. `source` matters because the local name alone is ambiguous: many namespaces publish a provider called `aws`, and if you omit `source`, Terraform defaults to `registry.terraform.io/hashicorp/<local name>`, which silently fails for any community provider. `version` is a constraint, not a pin — the exact release Terraform settles on is recorded in `.terraform.lock.hcl` at `init` time.
code
hcl · 17 linesterraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
# The map key is the local name: it is the prefix on resource types.
resource "aws_s3_bucket" "logs" {
bucket = "example-logs"
}go deeper
Be able to write the block from memory and name its three parts: local name, source address, version constraint. Say plainly that resource type prefixes come from the local name.
Explain that source addresses are namespace/type on a registry host, that the hashicorp namespace is only a default, and that Terraform intersects constraints across root and child modules before selecting one version.
Show the production discipline: permissive constraints in shared modules, a meaningful constraint at the root, and the committed lock file as the thing that actually makes a run reproducible across laptops and CI.
Own the estate-level policy — which namespaces are allowed, whether an internal registry or mirror sits in front of the public one, and how provider dependencies are audited across many repositories.
## What the block is for Terraform Core does not know how to talk to AWS, Cloudflare or Datadog. Every resource type is implemented by a **provider plugin**, a separate binary that Terraform downloads during `terraform init`. `required_providers` is how a module tells Terraform which plugins it needs and which releases of them are acceptable. It is a nested block inside the top-level `terraform` block: ```hcl terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } cloudflare = { source = "cloudflare/cloudflare" version = "~> 4.0" } } } ``` ## The three parts of an entry **The local name** is the map key (`aws`, `cloudflare`). It is the name used everywhere else in *this* module: the prefix of resource and data-source types (`aws_s3_bucket`, `data.aws_ami`) and the label on a `provider "aws"` block. Local names are per-module — nothing forces two modules to use the same one, which is exactly why the source address has to be spelled out. **`source`** is the provider's global address, written `[<HOSTNAME>/]<NAMESPACE>/<TYPE>`. The hostname defaults to `registry.terraform.io`, so `hashicorp/aws` expands to `registry.terraform.io/hashicorp/aws`. The namespace is the publishing organization; the type is the provider's name. Two providers with the same type but different namespaces are entirely different plugins. **`version`** is a constraint string — one or more comma-separated conditions using `=`, `!=`, `>`, `>=`, `<`, `<=` or `~>`, all of which must hold. It expresses a *range* of acceptable releases, not a single build. ## Why `source` became mandatory Before Terraform 0.13, providers were assumed to be published by HashiCorp, and the local name was the whole identity. That made third-party providers awkward and made it impossible to state, unambiguously, which plugin a module wanted. From 0.13 onward every provider has a full source address, and any provider outside the `hashicorp` namespace **must** be declared. Omitting `source` is not an error at parse time — Terraform quietly assumes `hashicorp/<local name>` and then fails at `init` with a registry lookup error for a provider that was never published under that namespace. That is one of the most common first-day Terraform errors, and it looks like a network problem rather than a configuration mistake. ## Where the block goes Every module that declares resources should declare the providers it uses, including child modules. A child module declares `required_providers` but should **not** configure the provider — provider configuration is inherited from the root. Terraform intersects the constraints from the root module and all child modules and picks a single version that satisfies all of them; if the ranges do not overlap, `init` fails with a message naming the conflicting modules. This is why a child module that pins a provider to an exact version is antisocial: it makes the whole configuration unupgradable. ## What happens at init `terraform init` resolves all provider constraints, picks the newest release satisfying them, downloads the plugin into `.terraform/providers/`, and records the chosen version plus checksums in `.terraform.lock.hcl` in the root module directory. `.terraform/` is a local cache and is gitignored; the lock file is committed. On later runs, `init` reuses the locked version rather than re-resolving, so everyone on the team and in CI runs the same plugin build. ## Common mistakes Writing only `version = "~> 5.0"` with no `source` for a community provider; assuming the local name must match the namespace (`cloudflare/cloudflare` happens to repeat, `hashicorp/aws` does not); expecting `version` to guarantee a specific build (it is a range — the lock file is what makes it reproducible); and putting `required_providers` at the top level of the file instead of inside `terraform { }`.
- If a child module and the root module both declare a constraint for the same provider, which one wins?Neither — Terraform intersects them. It gathers the constraints from every module in the configuration and selects one version that satisfies all of them, because a single provider instance serves the whole configuration. If the ranges do not overlap, `init` fails and names the modules involved. That is why child modules should use permissive constraints like `>= 5.0` rather than exact pins.
- Should a reusable child module declare required_providers, configure the provider, or both?Declare only. A child module states which providers it needs and roughly which versions, so Terraform can resolve plugins and so the module documents its dependencies. It should not contain a configured `provider` block: provider configuration is inherited from the root, and a module that configures its own provider cannot be used with multiple configurations and is much harder to remove from a configuration later.
- What is the difference between the version constraint in required_providers and the version recorded in the lock file?The constraint is a range of acceptable releases, written by a human and committed in the configuration. The lock file records the single release Terraform actually selected inside that range, plus its checksums. The constraint answers "what would we accept"; the lock file answers "what did we run". Reproducibility comes from the lock file, not from the constraint.
saying these in an interview costs you the question
- Thinking source is optional for any provider
- Believing the local name must equal the namespace
- Treating version = "~> 5.0" as an exact pin
- Configuring providers inside reusable child modules
- Assuming providers ship inside the Terraform binary