The Terraform Provider Block You Forgot to Write
You write some Terraform, run terraform plan, and everything looks good. You push it, your teammate pulls it, runs terraform plan, and gets an error. The code didn’t change. The only difference is the machine.
The culprit could be a missing provider block in the root module for a provider that needs explicit configuration, like AWS. On your machine, the missing configuration may have been supplied implicitly by your local environment. On your teammate’s machine, which had the audacity to be set up differently, the plan fails.
Two Blocks, Two Jobs
Terraform has two provider-related blocks that look related but do completely different things.
required_providers: the shopping list
terraform {
required_providers {
aws = {
source = "hashicorp/aws" # Download from HashiCorp's registry
version = ">= 4.0, < 6.0" # Version 4.x or 5.x
}
# Third-party provider example
datadog = {
source = "DataDog/datadog" # Not from HashiCorp
version = "~> 3.0"
}
}
}
Terraform uses this block during terraform init to determine which provider plugins to install, where to get them from, and which versions are acceptable.
It declares a dependency, but it does not configure how Terraform should connect to that provider. You will typically see required_providers in both root modules and child modules.
provider: the connection config
provider "aws" {
region = "us-east-1" # Default region to deploy resources
profile = "production" # Use this profile from ~/.aws/credentials
default_tags {
tags = {
ManagedBy = "terraform"
Environment = "prod"
}
}
}
Terraform uses provider blocks when evaluating and executing plans. This is where you configure how Terraform talks to the provider: region, credentials, aliases, default tags, and other provider-specific settings.
This block is typically defined in the root module, and child modules usually use that configuration unless you explicitly pass an alternate provider configuration.
required_providers
- Declares the provider dependency
- Used during
terraform init - Common in both root modules and child modules
- Contains source addresses and version constraints
provider
- Configures how Terraform uses the provider
- Used during
terraform planandterraform apply - Typically defined in the root module
- Contains settings like region, credentials, aliases, and default tags
One useful mental model is this:
-
required_providerssays: I need AWS -
providersays: Cool. Which AWS, exactly?
The Implicit Provider Trap
Here’s the problem.
When resources use a provider and you haven’t defined a default provider block, Terraform may still try to use that provider’s default configuration. For AWS, that often means looking to things like:
-
AWS_REGIONorAWS_DEFAULT_REGION -
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY ~/.aws/credentials~/.aws/config- IAM instance profile or task role credentials
If the environment has what it needs, the plan may succeed. If not, it fails.
That is what makes this bug sneaky. Your laptop may already have the missing pieces, so everything looks fine. Then someone else runs the same code in CI, on a teammate’s machine, or in a fresh environment, and it breaks.
Somewhere in your shell profile may be a completely undocumented AWS_REGION holding the entire deployment together with chewing gum and optimism.
A real example
Take this root module that manages SSM Parameter Store values:
# demo/main.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
module "app_config" {
source = "./modules/param_store"
parameters = {
"/myapp/database/host" = "db.example.com"
"/myapp/database/port" = "5432"
"/myapp/feature/dark_mode" = "enabled"
}
tags = {
Environment = "dev"
Project = "demo"
}
}
Notice what’s missing: there is no provider "aws" {} block in the root module.
terraform init can still succeed because Terraform has enough information to install the AWS provider plugin. terraform plan may also succeed, but only because AWS configuration was found somewhere in your local environment, such as a region in your shell or a default profile in your AWS config files.
That does not make the configuration portable.
Run this same code in a clean environment, and you may see an error like this:
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Invalid provider configuration
│
│ Provider "registry.terraform.io/hashicorp/aws" requires explicit configuration. Add a provider block to the root module and configure the provider's required
│ arguments as described in the provider documentation.
│
╵
╷
│ Error: invalid AWS Region:
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on <empty> line 0:
│ (source code not available)
│
╵
The fix is an explicit provider block in the root module:
provider "aws" {
region = "us-east-1"
}
Now the region is explicit and consistent across every machine that runs this code.
Without that explicit configuration, a local plan can give you a false sense of safety. CI is often where hidden local configuration stops covering for incomplete Terraform.
Why Reusable Child Modules Usually Shouldn’t Configure Providers
The child module (modules/param_store/main.tf) correctly declares what it needs but does not configure the AWS provider:
# demo/modules/param_store/main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
resource "aws_ssm_parameter" "this" {
for_each = var.parameters
name = each.key
type = "String"
value = each.value
tags = var.tags
}
No provider "aws" {} block. That is usually the right design for a reusable child module.
In most reusable-module patterns, child modules declare provider requirements and the root module owns provider configuration. That keeps credentials and environment-specific settings in one place, and it lets you pass different provider configurations to the same module when needed, such as:
- different AWS regions
- different AWS accounts
- aliased provider configurations
Reusable modules should declare what they need. Root modules should make adult decisions.
Knowing What Provider Blocks You Need
This problem can be hard to catch with automated tooling. In many common setups, static analysis will not flag it by default, and terraform plan only exposes it when the execution environment lacks the fallback settings your machine happens to have.
A very useful command here is:
terraform providers
Run it when adding or consuming a new module, and Terraform will print the provider dependency tree across your configuration.
For configurations using only local modules, this often works without terraform init. For remote modules, initialize first.
Example output:
Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/aws]
└── module.app_config
└── provider[registry.terraform.io/hashicorp/aws]
Use that output as a checklist against the provider {} blocks in your root module.
If a provider shows up in the dependency tree, ask yourself:
- Does the root module intentionally configure this provider?
- Is this configuration explicit, or is it only working because of my local environment?
- Would this still work for a teammate or in CI?
If your Terraform depends on undocumented shell state, that is not convenience. That is suspense.
Final Takeaway
required_providers and provider are related, but they solve different problems.
-
required_providersdeclares the dependency -
providerconfigures how Terraform uses it
If you are writing a reusable child module, declare the providers you require. If you are writing the root module, be explicit about provider configuration instead of relying on defaults from your machine.
Be explicit in root modules. Your future teammates deserve better than inheriting your laptop’s personality.
Enjoy Reading This Article?
Here are some more articles you might like to read next: