Why should a reusable Terraform module never contain its own provider block, and what specifically breaks if it does?
answer
- configure at the root, declare in the child
- one block costs you module for_each
- the caller should choose region and account
- destroying needs the configuration to still exist
- required_providers states needs, not settings
basics
~20 sA module that configures its own provider cannot be used with count, for_each or depends_on, hides region and credentials from the caller, and cannot be removed cleanly because Terraform still needs that configuration to destroy what it created. Declare requirements instead and receive configurations from the caller.
solid answer
~50 sA provider block is a *configuration*, and configurations should be owned by the root module so the caller controls region, credentials and account. If a shared module declares one, three things break. First, Terraform rejects `count`, `for_each` and `depends_on` on any module whose tree contains a provider block, because those would imply multiple copies of a provider configuration — you get the "Module is incompatible with count, for_each, and depends_on" error. Second, the caller loses control: the module hard-codes a region or an assume-role target that you can only change by forking it. Third, and worst, removal becomes painful — state binds resources to that configuration, so deleting the module call deletes the configuration Terraform still needs to destroy those resources. The correct shape is `required_providers` with `configuration_aliases` in the child and a `providers = { ... }` map in the caller.
go deeper
Remember the rule and where each piece goes: provider blocks live in the root module you apply, while shared modules only list required_providers. You are not expected to recite the failure modes yet.
Explain that a provider block in a module blocks count, for_each and depends_on on the module call, and that required_providers declares a need while a provider block sets region and credentials.
Lead with the removal trap — state binds resources to a configuration, so deleting the module deletes the configuration needed to destroy them — and show the configuration_aliases plus providers map shape that fixes it.
Own it as a module-contract standard: forbid provider blocks in shared modules, treat adding a providers map as a major version bump, and be ready to explain the migration cost to teams already consuming the old interface.
## Configuration versus requirement Terraform draws a line that is easy to miss because both live in the same file: - A `provider "aws" { ... }` block **configures** an instance of a provider: region, endpoints, credentials, `assume_role`. - A `required_providers` entry **declares** what a module needs: source address, version constraint, and optionally `configuration_aliases`. The rule that follows is: **root modules configure, child modules declare.** A reusable module states its needs; the composition at the top decides how those needs are met. ## What breaks, concretely ### Repetition meta-arguments stop working Since Terraform 0.13, `count`, `for_each` and `depends_on` are legal on `module` blocks — but not if the module, or anything it calls, contains a provider block. Terraform reports "Module is incompatible with count, for_each, and depends_on". The reason is structural: those meta-arguments would create N instances of the module, and Terraform has no way to create N provider configurations from one block, nor to defer a provider configuration behind `depends_on`. So a single embedded provider block silently costs the module the most common scaling pattern there is, and the failure surfaces at the *caller*, months later, in an error message that says nothing about providers. ### The caller loses the decisions that matter A module with `provider "aws" { region = "us-east-1" }` inside it is a us-east-1 module forever. The caller cannot use it for the DR region, cannot point it at a different account, cannot add `default_tags`, and cannot make it use the credentials the pipeline assumed. Every one of those becomes a fork or a pull request against a shared repository. ### Removal is a trap This is the one that bites in production. State records, per resource, which provider configuration manages it. To destroy a resource Terraform must still be able to *reach* it, which means the configuration has to exist. If the configuration lived inside the module, then deleting the `module` block removes the resources **and** the only configuration that could destroy them, and Terraform stops with a "Provider configuration not present" error. Digging out means re-adding the module temporarily, destroying deliberately, then removing — or manual state surgery. A caller-owned configuration outlives the module call and makes removal a normal one-step operation. ## The shape that works ```hcl # child module — declares, never configures terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" configuration_aliases = [aws.replica] } } } ``` ```hcl # root module — configures, and binds module "service" { source = "./modules/service" for_each = toset(["blue", "green"]) providers = { aws = aws.primary aws.replica = aws.secondary } } ``` With no provider block inside the child, `for_each` on the module is legal again, and both instances get the configurations the root chose. ## The honest exception A **root** module — the thing you actually run `terraform apply` in — must contain provider blocks; that is where configuration belongs. Some teams also have "root-like" modules that are only ever called from one place and never with `for_each`; embedding a provider there works until the day someone reuses it. Treating the rule as absolute for anything published to a registry or shared across repositories costs nothing and avoids a class of bug whose symptom never mentions its cause. ## How to spot it in review Grep a module repository for `^provider "` outside root directories. Anything that turns up is a latent `count`/`for_each` block and a future removal problem. Fixing it is mechanical — delete the block, add `configuration_aliases` if more than one instance was in play, and update callers with a `providers` map — but it is a breaking interface change, so it belongs in a major version bump of the module.
- What exact class of error do you hit if a module containing a provider block is called with for_each?Terraform refuses to plan with "Module is incompatible with count, for_each, and depends_on", pointing at the module call rather than at the provider block that caused it. It applies if any module in the called subtree contains a provider block, so the offending block may be several levels down and take a while to find.
- Root modules do contain provider blocks — why is that not the same mistake?Because the root module is where configuration legitimately belongs: it is the composition point that knows the environment, the account and the credentials, and nobody calls it with `for_each`. The rule is about *reusable* modules. A root module without provider blocks would have nothing to configure the providers it uses.
- How do you migrate an existing shared module that already ships a provider block?Remove the block, add `configuration_aliases` if the module used more than one instance, and require callers to pass a `providers` map. Because the map is now mandatory, this breaks every caller, so release it as a major version of the module and migrate callers deliberately — the provider association in state is unchanged as long as the configuration still points at the same region and account.
saying these in an interview costs you the question
- Says embedding a provider makes the module self-contained
- Thinks required_providers and a provider block are interchangeable
- Believes the module can be deleted once its resources are gone
- Claims count on modules never worked anyway
- Suggests forking the module per region instead of passing providers