AWS
Terraform
Cloud Deployment
Multi-Account Architecture
Infrastructure as Code

Deploying to multiple AWS accounts with Terraform?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Deploying Terraform into multiple AWS accounts is a standard pattern for separating development, staging, and production. The key is to keep one source of truth for infrastructure code while isolating credentials, state, and permissions per account.

The cleanest design uses AWS IAM roles for cross-account access, provider aliases in Terraform, and separate state for each environment. That structure scales from two accounts to dozens without turning deployments into manual credential switching.

Use AWS Profiles That Assume Roles

Start by making account access explicit in the AWS CLI configuration. A common pattern is to authenticate once with a base identity and then assume a deployment role in each target account.

ini
1[profile engineering]
2region = us-east-1
3
4[profile dev-admin]
5source_profile = engineering
6role_arn = arn:aws:iam::111111111111:role/TerraformDeployRole
7region = us-east-1
8
9[profile prod-admin]
10source_profile = engineering
11role_arn = arn:aws:iam::222222222222:role/TerraformDeployRole
12region = us-east-1

With this setup, Terraform does not need long-lived access keys for every account. It can rely on short-lived role sessions, which is better for security and easier to audit. Before touching Terraform, verify the role assumptions work:

bash
aws sts get-caller-identity --profile dev-admin
aws sts get-caller-identity --profile prod-admin

If those commands return the expected account IDs, the authentication layer is ready.

Model Accounts with Provider Aliases

Terraform can talk to multiple AWS accounts in one configuration by declaring multiple aws providers. Each provider alias represents one account or one access pattern.

hcl
1variable "region" {
2  type    = string
3  default = "us-east-1"
4}
5
6provider "aws" {
7  alias   = "dev"
8  profile = "dev-admin"
9  region  = var.region
10}
11
12provider "aws" {
13  alias   = "prod"
14  profile = "prod-admin"
15  region  = var.region
16}
17
18module "dev_vpc" {
19  source = "./modules/vpc"
20
21  providers = {
22    aws = aws.dev
23  }
24
25  name_prefix = "dev"
26  cidr_block  = "10.10.0.0/16"
27}
28
29module "prod_vpc" {
30  source = "./modules/vpc"
31
32  providers = {
33    aws = aws.prod
34  }
35
36  name_prefix = "prod"
37  cidr_block  = "10.20.0.0/16"
38}

This keeps account selection near the module that uses it. It also avoids a fragile pattern where operators manually export a different profile and rerun the same command. For a multi-account setup, explicit provider aliases are easier to review and much harder to misuse.

Keep Terraform State Separate Per Account

Do not store development and production resources in the same state file. Separate state is what prevents a routine change in one account from accidentally affecting another.

One practical pattern is to keep small backend configuration files outside the Terraform code:

hcl
1# backends/dev.hcl
2bucket = "terraform-state-dev"
3key    = "network/terraform.tfstate"
4region = "us-east-1"
5
6# backends/prod.hcl
7bucket = "terraform-state-prod"
8key    = "network/terraform.tfstate"
9region = "us-east-1"

Then initialize each deployment target explicitly:

bash
1terraform init -backend-config=backends/dev.hcl
2terraform plan
3
4terraform init -backend-config=backends/prod.hcl -reconfigure
5terraform plan

You can also split accounts into separate root modules if the infrastructure differs substantially. Provider aliases work well when the architecture is mostly shared. Separate roots are usually simpler when environments have meaningfully different stacks or release cycles.

Make Modules Account-Aware but Not Account-Coupled

Reusable Terraform modules should accept inputs such as CIDR ranges, naming prefixes, and feature flags, but they should not hard-code account IDs unless there is a strong reason. If a module truly needs the account ID, obtain it from the caller or from a data source.

hcl
1data "aws_caller_identity" "current" {}
2
3resource "aws_s3_bucket" "logs" {
4  bucket = "app-logs-${data.aws_caller_identity.current.account_id}"
5}

That small detail matters because modules copied between accounts often break when account-specific values are embedded directly in resource names or policies.

Common Pitfalls

The most common mistake is sharing one state file across multiple accounts. That makes plans harder to reason about and increases the blast radius of every apply.

Another problem is hiding account selection in shell state, such as switching AWS_PROFILE manually and hoping the correct account is active. Explicit provider aliases are far safer because the target account is visible in the code.

Teams also underestimate IAM setup. Terraform can only be repeatable if every target account exposes a predictable deployment role with the permissions your modules require.

Finally, avoid assuming Terraform workspaces alone solve multi-account design. Workspaces can help with environment naming, but they are not a substitute for proper credential separation and separate state layout.

Summary

  • Use AWS IAM roles and CLI profiles to access each account safely.
  • Declare one Terraform aws provider alias per target account.
  • Pass providers into modules explicitly so account selection is visible in code.
  • Keep state isolated per account, usually with separate backends or root modules.
  • Treat IAM role design and state layout as core parts of the solution, not afterthoughts.

Course illustration
Course illustration

All Rights Reserved.