Back to articles

A Simple but Effective Multi-Account AWS Strategy

When teams start with AWS, everything usually goes into a single account. It's simple, fast, and works—until it doesn't. As your infrastructure grows, a single account becomes a liability: unclear costs, tangled permissions, and one misconfiguration away from affecting everything.

A multi-account strategy isn't just for enterprises. Even small teams benefit from the separation it provides.

Why Multiple Accounts?

1. Blast Radius Containment

If something goes wrong—a security breach, a runaway Lambda, a deleted resource—it's contained to one account. Production stays safe when someone experiments in development.

2. Clear Cost Attribution

With separate accounts, your AWS bill breaks down naturally. No more tagging gymnastics to figure out which team or environment is spending what.

3. Simplified IAM

Instead of complex IAM policies trying to isolate resources within one account, you get hard boundaries. Developers can have admin access in dev without risking production.

4. Service Quotas

Each account gets its own service limits. A traffic spike in production won't exhaust quotas needed by your CI/CD pipelines.

A Practical Account Structure

Here's a straightforward structure that works for most teams:

Rendering diagram…

Management Account

The management account is the root of your AWS Organization. It exists primarily for governance, not workloads. Here you configure AWS Organizations, define Service Control Policies (SCPs), and manage consolidated billing. This account should have minimal resources—think of it as the control plane for your entire cloud estate.

Who has access: Platform/infrastructure team only. No developers should need access here. Protect root credentials with MFA and rarely (if ever) use them.

What lives here: AWS Organizations configuration, SCPs, billing dashboards, and organization-wide settings. That's it.

Development Account

This is the playground. Developers have broad permissions (often full admin) to experiment, debug, and iterate quickly. Breaking things here is expected and acceptable—that's the whole point.

Who has access: All developers with elevated permissions. The goal is to remove friction from day-to-day work.

What lives here: Development versions of your applications, test databases with synthetic data, experimental features, and anything that needs rapid iteration. Use smaller instance sizes and relaxed security groups to keep costs low and debugging easy.

Staging Account

Staging mirrors production as closely as possible. It's where you validate that code works in a production-like environment before the real deployment. QA testing, integration tests, and final sign-offs happen here.

Who has access: Developers with limited permissions (read-heavy, deploy via CI/CD), QA engineers, and automated pipelines.

What lives here: A near-replica of production infrastructure, but possibly at smaller scale. Data should be anonymized copies of production or realistic synthetic data. This environment catches the "works on my machine" problems.

Production Account

This is where your customers interact with your product. Stability, security, and reliability are paramount. Changes here go through CI/CD pipelines with proper approvals—no manual deployments.

Who has access: CI/CD pipelines for deployments, on-call engineers with read access and emergency deploy capabilities. No one should be making ad-hoc changes.

What lives here: Your live applications, real customer data (encrypted and backed up), production databases, and customer-facing APIs. Resources are right-sized with auto-scaling, and security groups are locked down tight.

Shared Services Account

This account holds resources that multiple environments need access to. Instead of duplicating infrastructure across dev, staging, and production, you centralize it here and grant cross-account access.

Who has access: Platform team for management, CI/CD pipelines for pushing artifacts, and other accounts for pulling shared resources.

What lives here: Container registries (ECR), DNS hosted zones (Route 53), CI/CD infrastructure, Terraform state buckets, shared VPN/bastion hosts, and artifact repositories. Centralizing these reduces duplication and ensures consistency.

Security/Audit Account

The security account is your tamper-proof audit trail and security monitoring hub. By isolating security tooling in its own account, you ensure that even a full compromise of a workload account can't erase evidence or disable alerts.

Who has access: Security team and compliance auditors. Workload accounts can write logs here but cannot read or delete them.

What lives here: CloudTrail logs aggregated from all accounts, GuardDuty findings, AWS Config rules, Security Hub dashboards, and long-term log archives. This account receives data from everywhere but pushes changes nowhere—it's intentionally read-heavy and isolated.

Real-World Example: Web Application

Let's say you're running a Next.js app with a backend API. Here's how resources map across accounts:

Rendering diagram…

What Lives Where

Shared Services Account:

  • ECR repositories (images used by all environments)
  • Route 53 hosted zones (DNS for all domains)
  • Shared VPN or bastion infrastructure
  • Terraform state buckets

Development Account:

  • Smaller instance sizes (cost optimization)
  • Relaxed security groups for debugging
  • Developers can deploy directly
  • Data can be synthetic or anonymized

Production Account:

  • Right-sized, auto-scaling resources
  • Strict security groups and NACLs
  • Deployments only through CI/CD
  • Real customer data (encrypted, backed up)

Setting It Up: Step by Step

1. Create an AWS Organization

# From your management account
aws organizations create-organization --feature-set ALL

2. Create Member Accounts

# Create each workload account
aws organizations create-account \
  --email dev-aws@yourcompany.com \
  --account-name "Development"

aws organizations create-account \
  --email prod-aws@yourcompany.com \
  --account-name "Production"

3. Set Up Cross-Account Access

Create a role in each member account that your CI/CD can assume:

# In each member account (Terraform)
resource "aws_iam_role" "deploy_role" {
  name = "GitHubActionsDeployRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::SHARED_ACCOUNT_ID:root"
      }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "sts:ExternalId" = "your-external-id"
        }
      }
    }]
  })
}

4. Apply Service Control Policies (SCPs)

Guardrails at the organization level:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "RequireIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    }
  ]
}

Cost Management Across Accounts

Consolidated Billing

All accounts roll up to the management account's bill. You get:

  • Volume discounts across all accounts
  • Single invoice
  • Cost Explorer with account-level filtering

Budget Alerts Per Account

resource "aws_budgets_budget" "account_monthly" {
  name         = "monthly-account-budget"
  budget_type  = "COST"
  limit_amount = "500"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 80
    threshold_type           = "PERCENTAGE"
    notification_type        = "ACTUAL"
    subscriber_email_addresses = ["alerts@yourcompany.com"]
  }
}

Common Patterns

Pattern 1: Shared VPC with Transit Gateway

For workloads that need to communicate across accounts:

Rendering diagram…

Pattern 2: Centralized Logging

All CloudTrail and application logs flow to the security account:

Rendering diagram…

Pattern 3: Shared Container Registry

ECR in shared services, cross-account pull permissions:

# In shared services account
resource "aws_ecr_repository_policy" "cross_account" {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "AllowCrossAccountPull"
      Effect = "Allow"
      Principal = {
        AWS = [
          "arn:aws:iam::DEV_ACCOUNT_ID:root",
          "arn:aws:iam::PROD_ACCOUNT_ID:root"
        ]
      }
      Action = [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ]
    }]
  })
}

Starting Small

You don't need all accounts on day one. A practical progression:

  1. Start: Single account (where you are now)
  2. First split: Separate production from everything else
  3. Add logging: Create a security/audit account
  4. Mature: Add staging, shared services as needed

Each step adds value without requiring a massive migration.

Tools That Help

  • AWS Control Tower: Automates account creation with best practices
  • AWS SSO / IAM Identity Center: Single sign-on across all accounts
  • Terragrunt: Manage Terraform across multiple accounts
  • aws-vault: Securely manage credentials for multiple accounts locally

Key Takeaways

  1. Separation is protection—isolated accounts limit blast radius
  2. Start with prod vs non-prod—even two accounts is better than one
  3. Centralize what's shared—DNS, container images, CI/CD
  4. Use SCPs for guardrails—prevent mistakes at the organization level
  5. Consolidated billing still works—you don't lose volume discounts

A multi-account strategy isn't overhead—it's insurance that pays dividends in clarity, security, and peace of mind.