skip to content

How would you manage resources in two different AWS accounts from a single Terraform root module, and what does that layout cost you?

level: middleimportance: should knowfreq 48%

answer

  1. one alias per account, not per region
  2. each alias assumes its own role
  3. the runner must be trusted by both roles
  4. aws_caller_identity proves where you landed
  5. two accounts, one state file

basics

~20 s

Declare one aliased aws provider per account, each with its own assume_role block naming a role in that account, and select the right alias on each resource or pass it into modules. The cost is that both accounts share one state file and one blast radius.

solid answer

~50 s

The mechanism is the same alias pattern, with the account rather than the region as the axis. You declare an aliased `provider "aws"` per account — say `aws.network` and `aws.workload` — and give each one an `assume_role` block whose `role_arn` points at a Terraform execution role in that account. Resources choose their account with `provider = aws.network`; modules receive it through the `providers` map. The identity running Terraform needs permission to assume both roles, and each target role's trust policy must allow that principal, so the pipeline identity is the hinge of the whole design. The tradeoff is real: one root module means one state file spanning two accounts, one lock, one blast radius, and anyone who can read that state can read outputs from both. The alternative is a root module per account wired together with published outputs, trading convenience for isolation.

code

hcl · 17 lines
hcl
provider "aws" {
  alias  = "workload"
  region = "us-east-1"

  assume_role {
    role_arn     = "arn:aws:iam::222222222222:role/TerraformExecution"
    session_name = "terraform-workload"
  }
}

data "aws_caller_identity" "workload" {
  provider = aws.workload
}

output "workload_account_id" {
  value = data.aws_caller_identity.workload.account_id
}

go deeper

for a junior

Know that account, like region, is a property of the provider configuration, so a second account means a second aliased provider block — and that credentials never belong in the HCL itself.

for a middle

Describe the assume_role block per alias and the resource-level or module-level selection, and explain that the running identity must be permitted to assume both roles and trusted by them.

for a senior

Diagnose the failure modes — an AccessDenied that names a resource rather than the provider, or a copied block with an unchanged role_arn — and weigh one shared state file against a root module per account.

for a principal

Own the boundary decision: putting two accounts in one state undoes part of the isolation the multi-account model bought. Define when coupling justifies it, who may read that state, and how the estate is split otherwise.

## Same mechanism, different axis Cross-account is not a different feature from multi-region — it is the same `alias` mechanism with different arguments. What changes per alias is not `region` but the identity the provider assumes: ```hcl provider "aws" { alias = "network" region = "us-east-1" assume_role { role_arn = "arn:aws:iam::111111111111:role/TerraformExecution" session_name = "terraform-network" } } provider "aws" { alias = "workload" region = "us-east-1" assume_role { role_arn = "arn:aws:iam::222222222222:role/TerraformExecution" session_name = "terraform-workload" } } ``` Every resource then declares its account the same way it would declare its region: `provider = aws.workload`. Modules get theirs through `providers = { aws = aws.workload }`, and a module that genuinely spans accounts — a VPC peering module, for instance — declares two slots with `configuration_aliases` and receives both. ## The identity chain This layout has one prerequisite that trips people up more often than the HCL does. Terraform runs as a single base identity — a CI role, an OIDC-federated role, a developer's SSO session — and each aliased provider turns that base identity into a per-account session. Two things must line up: 1. The base identity is allowed `sts:AssumeRole` on both role ARNs. 2. Each target role's **trust policy** names that base identity as a principal. Miss either and the symptom is an `AccessDenied` on the *first API call the provider makes*, not on the provider block itself, so the error names some innocuous resource. A quick diagnostic is a `data "aws_caller_identity"` per alias, output at plan time, proving which account each configuration actually landed in. ## Verification is cheap; do it ```hcl data "aws_caller_identity" "workload" { provider = aws.workload } output "workload_account" { value = data.aws_caller_identity.workload.account_id } ``` One aliased data source per account catches the classic copy-paste bug where the second block was duplicated but the `role_arn` was never changed — a mistake that otherwise shows up as resources quietly created twice in the same account. ## What the layout costs One root module touching two accounts means: - **One state file for both.** The lock is shared, so a long apply in one account blocks the other. Splitting later means state surgery. - **One blast radius.** A destroy, a bad refactor, or a `-target` mistake reaches both accounts in one run. - **State access equals cross-account visibility.** Whoever can read the state backend sees resource attributes from both accounts, which is exactly the boundary a multi-account strategy was meant to establish. - **One apply cadence.** The two accounts can no longer be changed on independent schedules or by independent teams. ## When to do it anyway Cross-account in one root module is right when the resources are genuinely *coupled* and must be created together: a VPC peering connection and its accepter, a cross-account IAM role and the principal that trusts it, a KMS key and a grant in the consuming account, a Route 53 record in a shared DNS account pointing at a workload load balancer. In those cases splitting the root module means one side has to publish an output the other reads, plus an ordering dance across two pipelines — genuinely worse. When the accounts are merely *related* — a production and a staging copy of the same stack — the alias trick is a false economy. One root module per account, each with a single default provider and its own state, is simpler, more isolated, and lets the two move at different speeds. ## The interview answer Show the mechanism in one breath — an aliased provider per account, each assuming a role in that account, selected per resource or passed into modules — and then immediately name the coupling cost. Candidates who stop at the syntax sound like they have only read the documentation; the ones who mention shared state and shared blast radius sound like they have operated it.

  • You copy an aliased provider block for the second account and everything lands in the first. What happened?
    Almost certainly the `role_arn` was not updated in the copy, so both aliases assume the same role and resolve to the same account. Terraform will not warn you — the configuration is valid. Adding an `aws_caller_identity` data source per alias and outputting the account ID turns this into a visible plan-time fact rather than a silent duplication.
  • When would you split the two accounts into separate root modules instead?
    When the resources are related but not coupled — a production and a staging copy of the same stack, or two teams' accounts. Separate roots give each account its own state, lock, blast radius and release cadence. Keep them together only when objects must be created in lockstep, such as a peering connection and its accepter.

saying these in an interview costs you the question

  • Puts long-lived access keys for the second account in HCL
  • Thinks a provider alias can switch accounts without a role or credentials
  • Assumes the second account's role trusts the runner by default
  • Ignores that both accounts now share one state file and lock
  • Believes each account automatically gets its own state

context