Multi-Account AWS with Terraform

Khimananda Oli 9 min read Virtualization
Multi-Account AWS with Terraform

By Khimananda Oli | Last reviewed: August 2026

Managing a single AWS account is manageable; managing twenty without automation is an operational disaster waiting to happen. Implementing multi-account AWS with Terraform provides the necessary isolation for security, billing, and compliance, but it introduces significant complexity in state management and identity federation. This guide moves beyond basic provisioning to show you how to architect a scalable landing zone that enforces governance while allowing development teams to move fast.

Why adopt a multi-account AWS with Terraform strategy?

The primary driver for adopting a multi-account AWS with Terraform strategy is the principle of least privilege applied at the infrastructure level. In a single-account model, a misconfigured S3 bucket policy or an overly permissive IAM role can expose your entire production dataset. By separating workloads into distinct accounts (e.g., Production, Staging, Security, Shared Services), you create hard blast-radius containment. If a staging environment is compromised via a supply chain attack, the attacker cannot laterally move to production because there is no network path and no shared identity boundary.

Beyond security, multi-account architectures simplify billing and compliance. For teams in Nepal serving global clients requiring SOC 2 or ISO 27001 certification, auditors expect clear separation of duties and environments. Tagging resources helps, but account-level separation provides cryptographic proof of isolation. When combined with Infrastructure as Code, specifically Terraform, this structure becomes reproducible. You aren't just clicking through the console; you are defining organizational units (OUs), service control policies (SCPs), and VPC peering relationships as code. This ensures that every new account created for a client or project inherits the exact same security baseline, eliminating configuration drift that plagues manual setups.

Management AccountSecurity OUWorkloads OUShared Services OUAudit / Log ArchiveProduction AccountStaging AccountNetwork HubTerraform manages OUs, SCPs, and Account Creationvia the Management Account API
Figure 1: Recommended multi-account AWS with Terraform organizational hierarchy isolating security, workloads, and shared services.

For engineers familiar with AWS IAM best practices for least-privilege access, the multi-account model is the ultimate expression of those principles. It shifts security from runtime enforcement (which can be bypassed) to structural enforcement (which cannot).

How do you structure Terraform state for multiple AWS accounts?

The most common failure mode in multi-account AWS with Terraform projects is improper state management. You must never store the state of multiple accounts in a single S3 bucket or a single state file. If your production state and staging state share a file, a `terraform destroy` targeting staging could accidentally wipe production resources due to dependency resolution errors or human error during apply.

Isolate state per account or layer

Adopt a directory structure that mirrors your account topology. Each account should have its own backend configuration. This ensures that operations on one environment never touch the state of another. A practical layout looks like this:

infrastructure/
├── _global/              # Organizations, SCPs, Route53
│   └── main.tf           # Backend: s3://mgmt-tf-state/global/terraform.tfstate
├── security/             # Audit, Log Archive accounts
│   └── main.tf           # Backend: s3://sec-tf-state/security/terraform.tfstate
├── prod/                 # Production workload account
│   └── main.tf           # Backend: s3://prod-tf-state/prod/terraform.tfstate
└── modules/              # Reusable VPC, EKS, RDS modules

Remote backend configuration with encryption

Your backend configuration must enforce encryption and locking. For multi-account setups, the state bucket itself should reside in a dedicated "Shared Services" or "Management" account, not in the workload account. This prevents a compromised workload account from deleting its own audit trail. Configure your backend to use cross-account access:

terraform {
  backend "s3" {
    bucket         = "shared-services-tf-state"
    key            = "prod/app-v1/terraform.tfstate"
    region         = "ap-south-1"
    encrypt        = true
    dynamodb_table = "tf-state-lock"
    # Assume a role in the shared services account
    role_arn       = "arn:aws:iam::111122223333:role/TerraformStateAccess"
  }
}

This pattern aligns with Terraform state management and remote backends fundamentals but adds the critical cross-account dimension required for enterprise safety. Always enable versioning on the S3 state bucket to allow recovery from corruption.

How do you configure CI/CD authentication for multi-account AWS with Terraform?

Hardcoding AWS access keys in GitHub Actions or GitLab CI is a security anti-pattern that fails audits immediately. For multi-account AWS with Terraform, you must implement OpenID Connect (OIDC). OIDC allows your CI/CD provider to assume specific IAM roles in target accounts based on the repository, branch, and workflow context, eliminating static credentials entirely.

Setting up OIDC trust relationships

Create an OIDC Identity Provider in each target AWS account (or centrally via Organizations) that trusts your CI/CD provider. Then, create IAM roles with trust policies restricted to specific repositories:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::TARGET_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/infra-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

This ensures that only merges to `main` in your specific infrastructure repo can assume the production deployment role. Feature branches might assume a different, more restricted role for validation only. This approach is significantly safer than long-lived keys and is now the standard for secure deployments to AWS from GitHub Actions with OIDC.

GitHub Actions(CI/CD Runner)AWS STSOIDC ProviderProd AccountDeploy RoleStaging AccountValidate RoleShared StateS3 + DynamoDB1. JWT Token2. Assume Role3. Read/Write State4. Provision Resources
Figure 2: Secure OIDC authentication flow enabling CI/CD to assume roles across multi-account AWS with Terraform without static keys.

Should you use AWS Control Tower or plain Organizations with Terraform?

This is the most frequent architectural decision when building multi-account AWS with Terraform. Both provide account factories and governance, but they serve different maturity levels and operational preferences. Control Tower adds a managed overlay that automates guardrails, landing zones, and account vending, while plain Organizations gives you raw API control with higher maintenance overhead.

CriteriaAWS Control TowerAWS Organizations (Plain)
Setup ComplexityLow (Managed Landing Zone)High (Build everything manually)
Terraform IntegrationModerate (Use CT-specific resources)Direct (Full API control)
Guardrails/PoliciesPre-packaged (NIST, CIS benchmarks)Custom SCPs only
Account VendingSelf-service via Service CatalogCustom Lambda/Terraform module
CostAdditional fees for managed servicesFree (Organizations is free)
Best ForCompliance-heavy, regulated industriesCustom platforms, startups, cost-sensitive

In practice, if you are pursuing SOC 2 or ISO 27001 and lack a dedicated platform team, start with Control Tower. The pre-built guardrails save months of policy authoring. However, if you need precise control over VPC topologies, custom tagging strategies, or want to avoid the additional CloudFormation stack sets that Control Tower deploys automatically, plain Organizations with reusable Terraform modules offers more flexibility. Many teams in Nepal's growing tech sector start with Organizations to minimize costs and migrate to Control Tower only when audit demands exceed their engineering bandwidth.

How do you manage networking and dependencies across accounts?

Network topology is where multi-account AWS with Terraform implementations typically stall. You cannot simply reference a VPC ID from another account's state file without creating tight coupling. Instead, adopt a hub-and-spoke model managed through a dedicated Network account.

Cross-account data sharing patterns

Use Terraform's `data` sources or AWS Resource Access Manager (RAM) to share resources without direct state dependencies. For example, share Transit Gateway attachments via RAM rather than exporting/importing IDs:

  1. Define shared resources in the Network account: Create Transit Gateways, Direct Connect gateways, and centralized DNS zones.
  2. Share via AWS RAM: Use `aws_ram_resource_share` to invite workload accounts to attach to the Transit Gateway.
  3. Consume in workload accounts: Use `data "aws_ec2_transit_gateway"` to look up the shared TGW by tag or ID, avoiding hardcoded cross-account references.
  4. Manage DNS centrally: Share Route 53 private hosted zones via RAM so all accounts resolve internal services consistently.

This decoupling means you can deploy the Network account independently. Workload accounts can be provisioned in parallel without waiting for network state outputs. For teams running Kubernetes, this pattern integrates cleanly with Amazon EKS deployments where each cluster lives in its own account but shares egress and ingress paths through the central hub.

Transit Gateway(Network Account)Prod VPC10.10.0.0/16Staging VPC10.20.0.0/16Security VPCInspection / FWOn-Prem / VPNDX / Site-to-SiteCentralized routing, inspection, and hybrid connectivity via RAM-shared attachments
Figure 3: Hub-and-spoke network topology enabling scalable multi-account AWS with Terraform using Transit Gateway and RAM sharing.

Secure multi-account AWS with Terraform implementation checklist

Building multi-account AWS with Terraform correctly requires discipline beyond just writing HCL. Before you run your first apply against a production organization, verify these operational safeguards:

  • Enable CloudTrail in the Management Account: Ensure it logs to a centralized, immutable S3 bucket in the Security account. This is non-negotiable for forensics.
  • Apply Service Control Policies (SCPs): Deny dangerous actions globally (e.g., `organizations:LeaveOrganization`, `cloudtrail:StopLogging`) at the OU level. SCPs act as a permission boundary that even root users cannot bypass.
  • Tag Everything Automatically: Use Terraform's `default_tags` provider block to enforce CostCenter, Environment, and Owner tags on every resource. Untagged resources in multi-account setups become orphaned costs quickly.
  • Implement Budget Alerts: Create AWS Budgets via Terraform for each account. Set alerts at 50%, 80%, and 100% of forecasted spend. In Nepal's cost-sensitive market, catching a runaway Lambda loop early saves real money.
  • Test Destroy Plans: Regularly run `terraform plan -destroy` in non-production to verify what would be deleted. Unexpected dependencies often surface only during destruction.

This architecture scales from three accounts to three hundred without changing fundamental patterns. The initial investment in proper state isolation, OIDC authentication, and network decoupling pays dividends during every future deployment and audit cycle.

Next steps for your multi-account journey

If you are transitioning from a single-account setup or inheriting a messy multi-account environment, start by auditing your current state against the patterns described here. Focus first on state isolation and credential security; networking and governance can be refactored incrementally. For teams needing hands-on guidance or a comprehensive review of their existing multi-account AWS with Terraform setup, reach out to discuss your infrastructure challenges. Whether you're preparing for compliance audits or optimizing costs across regions, getting the foundation right now prevents costly rework later.

Frequently Asked Questions

Use provider aliases in your configuration to target specific accounts. Define separate aws provider blocks with unique alias names and distinct assume_role configurations pointing to each target account's IAM role ARN for isolated resource management.

Store remote state in a dedicated management account S3 bucket with DynamoDB locking. Enable server-side encryption and versioning, then configure separate state files per environment or account using distinct key prefixes to prevent cross-account corruption.

No. Workspaces share state and variables, creating dangerous coupling between accounts. Use separate state files or directory structures per account instead to maintain strict isolation boundaries required for production multi-account AWS architectures.

Configure the assume_role block within aliased providers specifying the target role ARN, session name, and external ID if required. Ensure the calling identity has sts:AssumeRole permissions on the source account's trust policy.

The execution identity requires sts:AssumeRole on trusted roles in target accounts. Target roles need least-privilege policies allowing only required actions like ec2: or s3: for specific resources, never AdministratorAccess in production environments.

Deploy shared infrastructure like transit gateways or DNS zones in a dedicated networking account. Expose outputs via Terraform Cloud workspaces or S3 state data sources that other account configurations consume as read-only inputs.

Not strictly, but it reduces boilerplate significantly. Terragrunt handles dependency ordering, DRY provider configurations, and hierarchical variable inheritance across dozens of account directories better than native Terraform alone in 2026.

Never store secrets in state or variables. Use AWS Secrets Manager or Parameter Store in each account, referencing them via data sources at apply time. Alternatively integrate HashiCorp Vault with dynamic credentials for short-lived access.

Usually missing trust relationships or insufficient IAM policies. Verify the source principal is listed in the target role's trust policy and that inline/session policies don't restrict the assumed role beyond intended permissions.

Create account-level wrapper modules that compose shared networking, security, and workload modules. Pass account-specific variables like CIDR ranges and tags through tfvars files while keeping core module logic reusable and parameterized.

Yes, but use targeted plans per account first. Parallel planning risks rate limiting and makes output unreadable. Implement CI pipelines that serialize plans by account dependency order before aggregating results for review.

Map account relationships explicitly before coding. Shared services should depend on nothing; workload accounts depend only on shared services. Break cycles by extracting common interfaces into separate state files consumed via data sources.

Enforce mandatory tags like Account, Environment, and CostCenter via SCPs and Terraform default_tags in every provider block. This ensures consistent cost allocation reports in AWS Organizations without manual remediation.

Terraform itself is free; costs come from API calls and state storage. Budget approximately five dollars monthly per account for S3 requests, DynamoDB operations, and CloudWatch Logs during active development cycles in 2026.

Use OIDC federation between your CI platform and AWS IAM Identity Center. This eliminates long-lived access keys entirely, issuing temporary credentials scoped to specific accounts and roles per pipeline job automatically.