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 plan and terraform apply
  • Typically defined in the root module
  • Contains settings like region, credentials, aliases, and default tags

One useful mental model is this:

  • required_providers says: I need AWS
  • provider says: 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_REGION or AWS_DEFAULT_REGION
  • AWS_ACCESS_KEY_ID and AWS_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_providers declares the dependency
  • provider configures 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.