Cloud

AWS Organizations and Landing Zone: Multi-Account Strategy for Startups

2024-07-03 Gabor Balogh

AWS OrganizationsLanding ZoneSCPsMulti-AccountControl Tower

AWS Organizations and Landing Zone: Multi-Account Strategy for Startups

The conventional wisdom goes something like: "Start with one AWS account. Add more accounts when you need them." This advice comes from people who have never had to untangle a production and development environment sharing the same VPC, the same IAM policies, and the same billing.

The right answer โ€” from practical experience โ€” is to start with multiple accounts from day one. It's not much harder to set up, and it avoids an expensive and painful migration later.

This post explains a multi-account architecture built with AWS Organizations and Pulumi, and why manual SCPs beat Control Tower for most teams.

Why Multi-Account?

Three reasons, in order of importance:

1. Blast Radius Isolation

When a developer accidentally runs terraform destroy without a backend config and it points at the wrong workspace โ€” which has happened to experienced teams โ€” the damage is contained to one account. Production in a separate account is untouched.

This isn't theoretical risk. IAM policies can be misconfigured. S3 buckets can be made public. CloudTrail can be disabled by accident. In a single account, any of these accidents affect everything. In a multi-account model, they affect only the account they occur in.

2. Billing Clarity

With separate accounts per environment, AWS Cost Explorer shows you exactly what development costs versus production โ€” without tags, without cost allocation reports, without custom tooling. The account boundary is the cost boundary.

3. Security and Compliance

Many compliance frameworks (SOC 2, ISO 27001) require separation between production and non-production environments. A dedicated production account, with SCPs preventing anyone from accidentally deploying non-production workloads there, makes audit evidence straightforward to produce.

Account Structure

Management account (889604409053)
โ””โ”€โ”€ gabaltech (root OU)
    โ”œโ”€โ”€ Security OU
    โ”‚   โ””โ”€โ”€ gabaltech-log-archive
    โ”œโ”€โ”€ Workloads OU
    โ”‚   โ”œโ”€โ”€ gabaltech-dev
    โ”‚   โ”œโ”€โ”€ gabaltech-test
    โ”‚   โ””โ”€โ”€ gabaltech-prod
    โ””โ”€โ”€ Sandbox OU
        โ””โ”€โ”€ (ephemeral accounts for experiments)

This is a simplified version of the AWS Landing Zone Accelerator model. Here's what each component does.

Management Account

This is the AWS Organizations root โ€” the account that owns the organization. Use it only for:

  • AWS Organizations configuration and SCPs
  • Billing and consolidated billing
  • Budgets and cost alerts
  • CloudTrail organisation trail

Nothing else runs here. No application workloads, no test resources, no EC2 instances. The management account is treated as a restricted control plane.

Log Archive Account

All CloudTrail logs, Config snapshots, and S3 access logs from all accounts flow to an S3 bucket in the log-archive account. Critically, no one in the other accounts has write access to this bucket โ€” only the CloudTrail service role can write, and only from within the organisation.

This ensures audit logs can't be tampered with even if an account's credentials are compromised.

Workloads Accounts

Dev, test, and prod are separate accounts. Each has its own VPC, its own IAM, its own resource limits, and its own billing. The environments can't communicate with each other by default โ€” cross-account access requires explicit IAM trust relationships.

Sandbox OU

This is where experimentation happens. Accounts in the Sandbox OU can be created on demand (IDP-vended sandboxes via AWS Innovation Sandbox work well here) and deleted when the experiment is done. SCPs on the Sandbox OU enforce guardrails โ€” no resources outside eu-west-2, no accounts created within the sandbox, spending cap enforced by Budget alerts.

Service Control Policies (SCPs)

SCPs are the most powerful tool in AWS Organizations. They define the maximum permissions any identity in an account can have, regardless of what IAM policies grant.

Here are four SCPs to apply at the root OU (affecting all accounts):

1. Prevent Leaving the Organisation

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyLeaveOrganization",
    "Effect": "Deny",
    "Action": "organizations:LeaveOrganization",
    "Resource": "*"
  }]
}

Without this, any account with root access can leave the organisation, escaping all SCPs and consolidated billing. This is a foundational control.

2. Prevent Disabling CloudTrail

{
  "Statement": [{
    "Sid": "DenyDisableCloudTrail",
    "Effect": "Deny",
    "Action": [
      "cloudtrail:DeleteTrail",
      "cloudtrail:StopLogging",
      "cloudtrail:UpdateTrail"
    ],
    "Resource": "*"
  }]
}

Audit logs are non-negotiable. This SCP ensures they can't be disabled, even by account admins.

3. Region Lockdown

{
  "Statement": [{
    "Sid": "DenyOutsideEuWest2",
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "StringNotEquals": {
        "aws:RequestedRegion": ["eu-west-2", "us-east-1"]
      },
      "StringNotLike": {
        "aws:PrincipalARN": "arn:aws:iam::*:role/OrganizationAccountAccessRole"
      }
    }
  }]
}

Note us-east-1 is included because some global AWS services (IAM, Route53, CloudFront, S3 bucket creation) use us-east-1 as their API region regardless of where the resource is. The PrincipalARN exception exempts the cross-account access role that Organizations uses to manage member accounts.

4. Deny Root User Activity

{
  "Statement": [{
    "Sid": "DenyRootUser",
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "StringLike": {
        "aws:PrincipalArn": "arn:aws:iam::*:root"
      }
    }
  }]
}

Root user credentials should never be used for day-to-day operations. This SCP enforces that. Legitimate root use cases (changing account email, cancelling the account) are rare enough that temporarily detaching the SCP is acceptable.

Budgets: The Safety Net

For each member account, we create a $10/month budget that alerts at 80%:

new aws.budgets.Budget(`budget-${account.name}`, {
  accountId: account.id,
  budgetType: 'COST',
  limitAmount: '10',
  limitUnit: 'USD',
  timeUnit: 'MONTHLY',
  notifications: [{
    comparisonOperator: 'GREATER_THAN',
    notificationType: 'FORECASTED',
    threshold: 80,
    thresholdType: 'PERCENTAGE',
    subscriberEmailAddresses: ['gabaltech@outlook.com'],
  }],
});

This catches runaway costs before they become a large bill. For a startup, a single developer leaving a GPU instance running can exceed a monthly budget in hours โ€” early alerts matter.

Why Not Control Tower?

AWS Control Tower is AWS's managed landing zone service. It provisions a pre-built account structure, deploys guardrails (enhanced SCPs), and provides a governance dashboard. It sounds appealing.

Reasons to skip it:

  1. It's opinionated about your structure. Control Tower creates specific OUs (Security, Sandbox) with specific naming. If your structure diverges, managing the drift is painful.

  2. It deploys resources you may not want. Control Tower creates a CloudFormation StackSet deploying ~15 resources per account, including a Config aggregator, Config rules, and SNS topics. These cost money and can't easily be removed.

  3. SCPs via Pulumi are transparent. SCPs defined as TypeScript in the codebase are visible, testable in pull requests, and reviewable in git history. Control Tower's guardrails are managed through the console and are harder to audit.

  4. Control Tower requires ongoing maintenance. Landing Zone updates drop periodically and require manual application through the console. Owning a simpler setup gives you full understanding and control.

For teams without IaC experience or who want a compliance-certified landing zone out of the box, Control Tower is reasonable. For teams comfortable with Pulumi or Terraform, managing it yourself gives more control and lower cost.

Setting It Up

The full organization structure in Pulumi takes about 150 lines of TypeScript. The high-level steps:

  1. Enable Organizations on the management account
  2. Create OUs under the root
  3. Create member accounts (AWS will email you a verification for each)
  4. Apply SCPs to OUs
  5. Create Budgets for each account
  6. Set up the CloudTrail organization trail writing to the log-archive account

The aws.organizations.Account resource creates member accounts asynchronously โ€” account creation can take up to 60 seconds. Pulumi handles this gracefully with its resource graph.

Total setup time for a new organization: one day of engineering, including account email setup (+ aliases on the management account email work well) and IAM bootstrapping.

Back to Blog

About the Author

Gabor Balogh is a senior cloud platform engineer and DevOps consultant building infrastructure-as-code and developer platforms.