Multiple Regions and Accounts
Production AWS estates span regions for latency and disaster recovery, and accounts for isolation and blast-radius control. Terraform reaches both through the tools from the previous topic: aliased providers for regions, and assume_role providers for accounts.
The mechanics are the easy part. The decision that actually matters is how much of that multi-account topology belongs in a single state versus separate states. Cramming a dozen accounts into one apply maximizes blast radius — the exact thing multiple accounts were created to reduce. An elegant single state that can destroy your whole estate in one wrong plan is not elegant.
Multi-Region With Aliases
Multi-region within one account is the simplest case: one provider per region, each resource targeting the region it belongs in. The pattern below puts a primary bucket in us-east-1 and a replica in us-west-2 for disaster recovery, with replication wiring referencing both. Because both regions share one account, the credential chain is identical — only the region differs.
provider "aws" { region = "us-east-1" } provider "aws" { alias = "dr" region = "us-west-2" } resource "aws_s3_bucket" "primary" { bucket = "app-data-primary" } resource "aws_s3_bucket" "replica" { provider = aws.dr bucket = "app-data-replica" }
Cross-Account With assume_role
Crossing accounts adds role assumption. A provider configured with assume_role assumes an IAM role in the target account and manages resources there, so a single run can baseline a brand-new workload account from your tooling account. The role assumption fails with an opaque AccessDenied unless the target role's trust policy permits the principal doing the assuming — the most common cross-account stumbling block, and one that lives in the target account's IAM, not in your Terraform.
provider "aws" { alias = "sandbox" region = "us-east-1" assume_role { role_arn = "arn:aws:iam::555566667777:role/OrgTerraform" session_name = "tf-sandbox-baseline" } } resource "aws_iam_account_password_policy" "strict" { provider = aws.sandbox minimum_password_length = 14 }
State Topology
The structural choice is whether many accounts and regions share one state or each gets an isolated state. One state makes cross-references trivial — every resource can read every other's attributes directly. It also means every plan refreshes the entire estate, every apply can touch any account, and one lock blocks everyone. Isolated state per account contains failures and keeps plans fast, at the cost of wiring cross-account values through data sources or a parameter store rather than direct references.
One state across many accounts — simplifies cross-references because everything is in one place, but every apply can touch every account, plans are slow because they refresh the whole estate, and a single lock serializes the entire org. Reach for it only on a small, tightly-coupled set of accounts you genuinely manage as one unit.
Isolated state per account — contains a bad apply or a stuck lock to one account, keeps plans fast, and matches the isolation accounts were created for. The cost is passing cross-account values through SSM Parameter Store or published outputs instead of direct references. This is the default for production.
Account Factory Patterns
At scale, the accounts themselves become infrastructure. AWS Organizations and Control Tower provision and baseline new accounts — a guardrail set, a logging configuration, a standard IAM role — and Terraform drives that factory. A new team gets an account created, baselined, and handed over through the same plan-and-apply workflow as any other resource, instead of a manual click-through that drifts from the standard within a week.
Blast Radius
The recurring lesson is that blast radius is an operational property, not an aesthetic one. A single state spanning every account looks clean in a repo and is a liability in an incident: one wrong destroy, one bad plan applied in a hurry, one corrupted state, and the failure is org-wide instead of contained to one account. Split production into its own state and pipeline, separate from non-prod, so the worst a mistake can do is bounded by the account it happened in.
- Managing dozens of accounts from one root state, so a single bad apply or a stuck lock affects the entire estate at once instead of one account.
- Hardcoding cross-account ARNs instead of assuming roles or reading the value from a parameter store, so an account renumber or rebuild breaks every reference.
- Forgetting that an
assume_roleprovider needs the target role's trust policy to permit the source principal — the run fails with an opaqueAccessDeniedthat points nowhere useful. - Mixing prod and non-prod accounts in one state, defeating the isolation the accounts were created to provide and letting a non-prod mistake reach prod.
- Building one giant multi-account state because the cross-references are convenient, then discovering at incident time that the convenience cost you containment.
- Isolate state per account, and often per region, to contain blast radius and keep each plan fast by refreshing only its own scope.
- Use
assume_roleproviders for cross-account management, each backed by a least-privilege role scoped to what that state manages. - Pass cross-account values through SSM Parameter Store or published outputs rather than hardcoding ARNs that rot when an account is rebuilt.
- Separate production accounts into their own state and pipeline from non-prod, so a non-prod mistake can never reach a production resource.
- Drive account creation and baselining through Organizations or Control Tower in Terraform, so every new account starts from the same reviewed standard.
Knowledge Check
Why does isolated state per account usually beat one big multi-account state in production?
- It contains a bad apply or stuck lock to one account and keeps each plan fast
- It is the only topology that allows cross-account references between resources at all
- It removes the need to assume any IAM roles when crossing account boundaries
- It lets a single shared state lock serialize every apply across the whole org
A cross-account assume_role provider fails with AccessDenied. What is the most likely cause?
- The target role's trust policy does not permit the source principal to assume it
- The aliased provider block is missing its required region argument for the target account
- Terraform cannot assume IAM roles across separate accounts at all
- The lock file lacks a provider hash for the target account's platform
What is the main downside of managing many accounts from a single Terraform state?
- Every apply can touch every account, so one bad plan has an estate-wide blast radius
- Cross-account references between resources become impossible to express
- Terraform refuses to assume more than one IAM role within a single run
- Provider-level default tags stop applying once a resource crosses an account boundary
How should cross-account values like a shared ARN be passed between isolated states?
- Through SSM Parameter Store or published outputs, read as a data source
- Hardcoded into each consuming configuration as a literal string ARN
- By merging the two states into one so the value is directly referenceable
- Through the dependency lock file, which is built to record shared ARNs
You got correct