In Terraform, why can't you choose a provider alias from a variable — for example provider = aws[var.region] — and how do you deploy the same module across a list of regions instead?
answer
- the provider argument is a reference, not an expression
- the binding is resolved before values are
- no loop over provider blocks
- arguments inside the block may use variables
- repeat the module, or repeat the run
basics
~20 sThe provider meta-argument takes a static reference, not an expression: Terraform resolves provider configurations before evaluating variables, so an alias cannot be computed. Fan out by repeating the module block once per region with an explicit providers map, or by running one root module per region.
solid answer
~50 sProvider configurations sit below the expression layer. Terraform has to know which provider instance owns each resource in order to build the graph and to record the association in state, and that happens before variable values are folded in — so the `provider` meta-argument accepts a static reference like `aws.eu` and nothing else. There is no loop over provider configurations either, which rules out generating one per element of a list. The two workable patterns are: repeat the `module` block once per region, each with its own `providers = { ... }` map — explicit and reviewable, but linear in the number of regions; or make the root module single-region with `region = var.region` on its one provider block and run it once per region against separate state, driven by directories, workspaces or a pipeline matrix. The second scales better and shrinks blast radius; the first is right when the regions must move together.
code
hcl · 25 linesprovider "aws" {
alias = "us"
region = "us-east-1"
}
provider "aws" {
alias = "eu"
region = "eu-west-1"
}
module "edge_us" {
source = "./modules/edge"
providers = {
aws = aws.us
}
}
module "edge_eu" {
source = "./modules/edge"
providers = {
aws = aws.eu
}
}go deeper
Just remember the rule: the provider argument takes a fixed reference like aws.eu, so you cannot pick it with a variable. Copying the module block per region is a normal thing to see.
Explain why — the resource-to-provider binding is structural and recorded in state, so it is resolved before variables are — and note that a provider block's own arguments can still use variables.
Present both fan-out shapes and their consequences: repeated module blocks give one plan and one blast radius, a parameterised root run per region gives isolated state, locks and staged rollout. Say which you would use at two regions and at twelve.
Frame it as estate design rather than a syntax limit. Decide where the region boundary sits, whether generated roots are worth the extra build step, and how a region-by-region rollout with independent state fits the organisation's change process.
## The layering reason Almost everything in HCL is an expression, which is why the restriction feels arbitrary until you see where provider configurations live. Terraform must resolve, for every resource in the configuration, **which provider configuration manages it** — before it can construct the dependency graph and before it can compare the configuration against state, where that association is recorded. That resolution is structural, not evaluated. Consequently: - `provider = aws.eu` is a **reference to a configuration**, in the same family as a resource address, not a string and not an expression. - `provider = aws[var.region]`, `provider = "aws.${var.env}"` and `provider = local.chosen` are all invalid. - There is no `for_each` on a `provider` block, so you cannot manufacture one configuration per element of a collection. The part people miss is that this restriction applies to *selecting* a configuration, not to *configuring* one. A provider block's arguments are ordinary expressions: ```hcl provider "aws" { region = var.region # perfectly legal } ``` That single fact is the escape hatch, and it points straight at the second pattern below. ## Pattern 1 — repeat the module block When a handful of regions must be created and changed together, write them out: ```hcl provider "aws" { alias = "us", region = "us-east-1" } provider "aws" { alias = "eu", region = "eu-west-1" } module "edge_us" { source = "./modules/edge" providers = { aws = aws.us } } module "edge_eu" { source = "./modules/edge" providers = { aws = aws.eu } } ``` It is repetitive, and that repetition is the honest cost of a static provider layer. In exchange, the plan shows both regions at once, cross-region references work naturally, and review is trivial because every region is visible in the file. It does not scale: at fifteen regions this is fifteen near-identical blocks and one enormous state file where a single failing API call blocks every region's apply. Note this pattern *requires* the module to be provider-clean. If `./modules/edge` contained its own `provider` block you could not even use `for_each` on it, and you would be forced into copies anyway. ## Pattern 2 — one root module, run per region The scaling answer inverts the problem. Make the root module single-region, with one default provider configured from a variable, and let the *runner* supply the region: ```hcl provider "aws" { region = var.region } ``` Each region then gets its own state — separate backend keys, separate directories, or a workspace per region — and the pipeline iterates: a matrix job per region, or a small wrapper that applies each in turn. What you gain is substantial: independent state and locks, a blast radius of one region, the ability to roll a change out region by region and stop when the third one fails, and no giant plan. What you lose is the single-plan view and easy cross-region references, which now have to go through published outputs or data sources. ## Pattern 3 — generate the configuration Some teams template the root modules from a list of regions with a small script, Terragrunt, or a CI step, so the repetition exists on disk but not in the source of truth. This is legitimate at scale, but it adds a build step in front of `terraform plan`, and reviewers now diff generated code. Reach for it when the count of near-identical roots genuinely becomes unmanageable, not as a default. ## Choosing Ask whether the regions are one system or many copies of a system. An active-active pair with a replication link between them is one system: keep them in one root module with two aliases and accept the repetition. Twelve edge regions running the same stack independently are copies: one parameterised root module, applied twelve times, with twelve states. The static provider layer pushes you toward the second answer as the estate grows, and that pressure is generally healthy — it is the same pressure that keeps state files small. ## What to say in the room Lead with the reason, not the rule: provider configurations are resolved before expressions are evaluated because the resource-to-provider binding is part of the graph and part of state. Then give both patterns and say which one you would pick at two regions and which at twelve. Candidates who only know "you can't do that" sound stuck; the reason plus the two shapes sounds like someone who has hit it.
- If aliases can't be chosen dynamically, why can a provider block still say region = var.region?Because that configures a configuration rather than selecting one. The identity of the provider instance is fixed by the block's position and alias, which is what Terraform needs before evaluation; its arguments are ordinary expressions resolved afterwards. That asymmetry is exactly what makes the single-region, parameterised root module the scaling pattern.
- At what point would you stop adding aliases and split into a root module per region?Roughly when the regions stop being one system. Two or three coupled regions with replication between them belong in one root module. Once regions are independent copies, or the count reaches the point where one failed API call blocks every region's apply and the plan is unreviewable, split — each region getting its own state, lock and rollout step.
- Can you at least remove a region by deleting its provider block once you no longer want it?Not in one step. State binds each resource to its provider configuration, so Terraform needs that configuration present in order to destroy those resources. Destroy or move them first — with the block still in place — and delete the provider block in a follow-up change, otherwise planning fails with a "Provider configuration not present" error.
saying these in an interview costs you the question
- Thinks the provider argument accepts any expression
- Expects for_each on a provider block to work
- Assumes an input variable can switch regions per resource
- Claims Terragrunt is the only way to handle many regions
- Says one state file for twenty regions is fine