How do you structure Terraform for a multi-account AWS organization?
Quick Answer
Use a hub-and-spoke model with separate state files per account, shared modules for common patterns, and assume-role providers to manage cross-account resources. Structure repositories with account-level directories, environment-level workspaces, and a central module registry for VPC, IAM, and security baseline configurations.
Detailed Answer
Structuring Terraform for a multi-account AWS organization requires solving three interconnected problems: provider authentication across accounts, state isolation between accounts, and code reuse across similar environments. Think of it like managing a franchise: each store (account) runs the same playbook but has its own inventory (state), and headquarters (management account) needs oversight into all of them.
The foundation is AWS Organizations with a well-defined account structure. Typically you have a management account (for Organizations API and billing), a security account (for GuardDuty, SecurityHub, CloudTrail aggregation), a shared-services account (for CI/CD, artifact repositories, DNS), and then workload accounts per environment or per team. Each account gets its own Terraform state file to ensure blast radius isolation: a misconfigured apply in the staging account cannot corrupt production state.
For provider configuration, the recommended pattern is assume-role chaining. Your CI/CD pipeline authenticates to a central deployment account using OIDC (for GitHub Actions) or instance profiles (for Jenkins on EC2), then assumes environment-specific roles in target accounts. Each account has a TerraformExecutionRole with least-privilege permissions. The provider block uses assume_role with the target account's role ARN, and you parameterize the account ID using variables or a map lookup.
The repository structure typically follows one of two patterns. The first is a monorepo with directory-per-account: infrastructure/accounts/production/, infrastructure/accounts/staging/, each with their own backend configuration and tfvars. The second is a module-based approach where a single root configuration uses for_each over a map of accounts to deploy baseline resources. The monorepo approach is simpler to reason about but leads to code duplication. The module approach is DRY but increases blast radius.
Shared modules are the cornerstone of multi-account management. You create versioned modules for common patterns: a VPC module that enforces CIDR allocation from a central IPAM, a security-baseline module that deploys Config rules and GuardDuty, an IAM-baseline module that creates standard roles. These modules are published to a private Terraform registry or referenced via Git tags.
Production gotchas are numerous. Cross-account resource references require careful handling: you cannot reference a resource in another account's state without remote state data sources or SSM Parameter Store lookups. VPC peering across accounts needs accepter-side resources managed by the accepter account's Terraform. Service Control Policies in the management account can block Terraform operations in child accounts if not carefully scoped. And state backend permissions must be tightly controlled: each account's Terraform role should only access its own state prefix in S3.