← Blog

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

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

provider

One useful mental model is this:


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:

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:

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:

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.

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.