
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Running everything in one AWS account works until a staging deploy touches production data or a billing spike hits the wrong card. Multi-Account AWS with Terraform gives you account boundaries, separate blast radius, and repeatable infrastructure across dev, staging, and prod. If you already treat infrastructure as code with Terraform as production discipline, extending that model across accounts is the natural next step. This guide walks through Organizations setup, provider configuration, state design, and patterns I have seen work on real deployments — including the mistakes that waste weekends.
Why should you use Multi-Account AWS with Terraform instead of one account?
AWS recommends multiple accounts for security, billing isolation, and workload separation. Terraform gives you a single workflow to provision VPCs, RDS instances, and IAM roles consistently across all of them. Without that discipline, each account becomes a snowflake configured by console clicks.
On client projects where I handle both application code and Linux server administration, the same principle applies at the cloud layer. Separate accounts for production and non-production stop a misconfigured security group in dev from exposing prod databases. Service Control Policies at the organization level enforce guardrails Terraform alone cannot guarantee.
A practical account map for a mid-size team looks like this:
- Management account — Organizations, billing consolidation, SCPs only; no application workloads.
- Security / audit — CloudTrail aggregation, GuardDuty admin, read-only compliance tooling.
- Shared services — Terraform state buckets, ECR repos, CI runners, DNS if centralised.
- Workload accounts — One or more per environment or per product line.
For a Laravel application on EC2 — similar to deployments I maintain with Deployer 7 and GitLab CI — you might run dev in one account and prod in another. The app code stays identical; only Terraform variables and account IDs change per environment.
How do you set up AWS Organizations before running Terraform?
Start in the management account. Enable AWS Organizations, create Organizational Units, and invite or create member accounts. Do not run application Terraform from the management account root user — ever.
Create the organization and member accounts
Use the AWS CLI or a bootstrap Terraform stack in the management account. Account creation via Organizations is idempotent when you tag accounts consistently.
aws organizations create-organization --feature-set ALL
aws organizations create-account \
--email aws-dev@yourdomain.com \
--account-name "workload-dev" \
--role-name OrganizationAccountAccessRole
aws organizations create-account \
--email aws-prod@yourdomain.com \
--account-name "workload-prod" \
--role-name OrganizationAccountAccessRole
The OrganizationAccountAccessRole is created automatically in each new account. Your Terraform execution role in the shared-services account will assume this role during the bootstrap phase, then switch to a tighter custom role afterward.
Apply Service Control Policies before workloads land
SCPs restrict what member accounts can do regardless of IAM permissions inside them. Common baseline policies deny root user access keys, restrict regions to ap-south-1 and us-east-1, and block public S3 ACLs. Apply SCPs at the OU level before any workload Terraform runs — retrofitting is painful.
If you adopt AWS Control Tower, it provisions the OU structure, baseline guardrails, and account factory for you. You can still manage custom resources with Terraform in member accounts; Control Tower handles the landing zone, not every S3 bucket.
How do you configure Terraform providers for multiple AWS accounts?
The HashiCorp AWS provider supports assume_role blocks and provider aliases. One root module can target multiple accounts in a single plan when configured correctly. Pin the provider version — drift in provider behaviour breaks multi-account pipelines silently.
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.70"
}
}
}
provider "aws" {
region = "ap-south-1"
assume_role {
role_arn = "arn:aws:iam::111111111111:role/TerraformExecutionRole"
session_name = "terraform-shared-services"
}
}
provider "aws" {
alias = "dev"
region = "ap-south-1"
assume_role {
role_arn = "arn:aws:iam::222222222222:role/TerraformExecutionRole"
session_name = "terraform-dev"
}
}
provider "aws" {
alias = "prod"
region = "ap-south-1"
assume_role {
role_arn = "arn:aws:iam::333333333333:role/TerraformExecutionRole"
session_name = "terraform-prod"
}
}
Pass the alias into modules explicitly. A common mistake is forgetting providers = { aws = aws.dev } inside a child module, which silently deploys to the default account.
IAM trust policies that actually work
Each workload account needs a TerraformExecutionRole trusted by the shared-services CI role. Keep the trust policy narrow — one principal ARN, optional external ID for third-party runners.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::999999999999:role/GitLabTerraformRunner"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "your-org-terraform-external-id"
}
}
}
]
}
Attach least-privilege policies per account. Dev roles might manage EC2 and RDS; prod roles should require manual approval in your CI pipeline before terraform apply. This mirrors how I gate production Deployer releases behind tagged pipelines on sister sites sharing the same GitLab CI workflow.
For deeper provider alias patterns across clouds, see the companion post on Terraform provider aliases for multi-cloud — the assume_role mechanics transfer directly to AWS-only setups.
Where should remote state live in a multi-account AWS setup?
Never store Terraform state in the same account it manages if you can avoid it. A dedicated shared-services account hosts S3 buckets and DynamoDB lock tables. State files contain secrets — treat the bucket like a database backup.
The pattern I recommend matches what we document for managing Terraform state safely:
- Create one S3 bucket per environment or one bucket with strict prefix isolation.
- Enable versioning, SSE-KMS encryption, and block all public access.
- Use a DynamoDB table for state locking — one table can serve multiple state keys.
- Restrict bucket policy to Terraform execution role ARNs only.
- Enable S3 access logging to the audit account.
terraform {
backend "s3" {
bucket = "org-terraform-state-ap-south-1"
key = "workloads/dev/vpc/terraform.tfstate"
region = "ap-south-1"
encrypt = true
kms_key_id = "arn:aws:kms:ap-south-1:999999999999:key/abc-123"
dynamodb_table = "terraform-locks"
role_arn = "arn:aws:iam::999999999999:role/TerraformStateAccessRole"
}
}
Split state by blast radius, not by convenience. A VPC stack, an EKS cluster stack, and an application stack should each have separate state files. One giant state across three accounts makes every plan slow and every mistake catastrophic. Terragrunt helps keep backend configuration DRY when you have dozens of stacks.
Cross-account state access requires the state bucket policy to trust workload-account roles only for their prefix. Dev roles read/write workloads/dev/*; prod roles cannot touch dev prefixes. Validate this with Checkov scans on Terraform before merging.
What deployment patterns work best for Multi-Account AWS with Terraform?
Three patterns cover most teams. Pick based on team size, release cadence, and how much autonomy each product line needs.
| Pattern | Structure | Best for | Trade-off |
|---|---|---|---|
| Monorepo + aliases | One repo, provider aliases per account | Small teams, shared modules | Plan scope grows; careful CI filtering needed |
| Repo per account | Separate root modules per account | Strong isolation, different teams | Module versioning overhead |
| Hub + Terragrunt | Shared modules, account/env folders | 10+ accounts, DRY backends | Learning curve for Terragrunt |
Reusable modules across accounts
Extract VPC, ECS, and RDS patterns into versioned modules — the same approach as reusable Terraform modules. Pass account-specific CIDR blocks and instance sizes as variables. Tag every resource with Environment, ManagedBy = terraform, and AccountId for cost allocation.
module "vpc_dev" {
source = "git::https://gitlab.com/org/terraform-modules.git//vpc?ref=v2.4.0"
providers = {
aws = aws.dev
}
cidr_block = "10.10.0.0/16"
environment = "dev"
account_name = "workload-dev"
}
module "vpc_prod" {
source = "git::https://gitlab.com/org/terraform-modules.git//vpc?ref=v2.4.0"
providers = {
aws = aws.prod
}
cidr_block = "10.30.0.0/16"
environment = "prod"
account_name = "workload-prod"
}
Pin module refs to tags, not main. A breaking change in a shared VPC module should not auto-deploy to prod on the next plan.
CI pipeline design
Run terraform plan on every merge request. Gate prod applies behind manual approval and branch protection. Use OIDC federation from GitLab or GitHub Actions to assume the shared-services role — no long-lived access keys in CI variables.
Structure pipelines so a change to dev modules does not trigger prod plans. Path-based rules or Terragrunt run-all plan with include filters keep feedback fast. For drift checks after deploy, schedule read-only plans nightly — the same mindset as Terraform drift detection strategies.
On an enterprise booking platform or similar multi-environment app, this pipeline model prevents a developer from accidentally applying staging RDS sizing to production. The code review sees the plan output before anyone clicks approve.
What mistakes break Multi-Account AWS with Terraform in production?
These failures show up repeatedly across teams new to multi-account setups. Most are fixable in an afternoon if you catch them early.
Hard-coded account IDs in modules
Account IDs belong in tfvars or Terragrunt config, not inside shared modules. Pass role_arn as a variable so the same module works in every account without forking.
Missing provider passthrough in nested modules
Terraform does not inherit provider aliases into child modules automatically. Every nested module needs a configuration_aliases declaration and explicit provider mapping. Run terraform validate in CI — it catches some but not all alias errors.
State bucket in a workload account
If the prod account holds its own state bucket and an attacker compromises prod IAM, they can read and rewrite state. Centralise state in shared services with cross-account bucket policies.
Skipping secrets management
Database passwords and API keys in tfvars files end up in state as plaintext. Use AWS Secrets Manager or SSM Parameter Store and reference values with data sources. Our guide on multi-cloud secrets management covers rotation patterns that apply directly here.
Compare your approach against AWS CloudFormation fundamentals if stakeholders ask why Terraform over native tooling. StackSets solve multi-account deployment too, but Terraform wins when you already manage Azure or on-prem with the same workflow — see managing multi-cloud state with Terraform for that broader picture.
Before your first prod apply, validate JSON policies with the JSON formatter tool and run a manual assume_role test from your CI runner account. A two-minute STS check saves hours of permission debugging.
For teams building custom platforms — not just infra — pairing this setup with enterprise application development or custom software development keeps application and infrastructure lifecycles aligned. Several sister sites I maintain on shared EC2 use the same GitLab CI discipline even when the cloud layer is simpler than full multi-account AWS.
Official references worth bookmarking: the HashiCorp AWS provider documentation for assume_role syntax, and the AWS Organizations user guide for SCP and OU behaviour.
Key Takeaways
- Enable AWS Organizations first, apply SCPs at the OU level, and keep the management account free of workloads.
- Use provider aliases with explicit
assume_roleblocks — never deploy to member accounts from the management root. - Host remote state in a shared-services account with KMS encryption, versioning, and prefix-scoped IAM policies.
- Split state by stack and environment; pin module and provider versions to tagged releases.
- Authenticate CI via OIDC, gate prod applies behind manual approval, and scan Terraform with Checkov before merge.
- Start with a monorepo and provider aliases; adopt Terragrunt when account count or DRY backend config demands it.
People Also Ask
Can one Terraform workspace manage multiple AWS accounts?
Terraform workspaces isolate state within a single backend key — they do not isolate AWS accounts by themselves. For multi-account setups, use separate state keys or separate root modules with provider aliases. Workspaces suit environment variants within one account, not cross-account boundaries.
Do you need AWS Control Tower for Multi-Account AWS with Terraform?
No. Control Tower accelerates landing-zone setup with pre-built OUs and guardrails. You can create Organizations, accounts, and IAM roles manually or with bootstrap Terraform. Control Tower and custom Terraform coexist — many teams use Control Tower for account vending and Terraform for workload resources.
How much does a multi-account AWS setup cost?
AWS accounts are free; you pay for resources inside them. Expect roughly Rs 3,000–8,000/month (~USD 22–60) for baseline shared-services tooling — state storage, CloudTrail, GuardDuty — before application workloads. Actual spend depends on instance sizes and data transfer across accounts.
Is Terragrunt required for multi-account Terraform?
Not required. Terragrunt reduces repetition when you have many similar stacks across accounts. Teams with three to five accounts often succeed with a monorepo, provider aliases, and copy-paste backend blocks. Add Terragrunt when backend and variable duplication becomes the main maintenance burden.
Build your multi-account foundation the right way
Multi-Account AWS with Terraform is not optional once you have production workloads and more than one engineer touching infrastructure. Start with Organizations, centralise state, wire assume_role correctly, and split stacks before complexity compounds. The patterns here mirror what I apply on production deployments — boring infrastructure that survives staff turnover and 2 a.m. incidents.
If you need help designing a landing zone, migrating from a single account, or integrating Terraform into your existing GitLab CI pipeline, contact us to discuss your setup. For related reading, explore Terraform workspaces and environments and the Notary Kathmandu deployment pipeline as examples of disciplined release workflows.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

