AWS Organizations and Control Tower: Multi-Account Strategy
Running everything in a single AWS account is a recipe for blast-radius disasters, compliance nightmares, and cost confusion. A mature multi-account strategy — built on AWS Organizations and AWS Control Tower — lets you isolate workloads, apply consistent guardrails, delegate billing, and automate account provisioning at scale. This guide covers everything from Organizational Unit design to SCP authoring to automated account vending with Terraform and the Account Factory.
Table of Contents
- AWS Organizations Overview
- OU Design and Account Structure
- Service Control Policies (SCPs)
- AWS Control Tower: Landing Zone
- Guardrails: Preventive vs Detective
- Account Factory and Provisioning
- Multi-Account Strategy Patterns
- AWS IAM Identity Center (SSO)
- Terraform and CDK Automation
- Best Practices
- Frequently Asked Questions
AWS Organizations Overview
AWS Organizations is a free AWS service that lets you group multiple AWS accounts into a hierarchy, apply policies across them, and consolidate billing into a single payer account. Every organization has one management account (formerly "master account") that owns the organization and cannot be a member account simultaneously.
Key concepts you must understand before architecting a multi-account setup:
- Management Account — The root account that creates and manages the organization. Never run production workloads here. Its only job is governance.
- Member Accounts — Individual AWS accounts that belong to the organization. Each gets its own root user, IAM namespace, and service quotas.
- Organizational Units (OUs) — Logical containers for member accounts. Policies attached to an OU apply to all accounts nested inside it, including child OUs.
- Service Control Policies (SCPs) — IAM-like policy documents applied at the root, OU, or account level. They define the maximum permissions any principal in that scope can have — even the account's root user.
- Consolidated Billing — All charges roll up to the management account. Reserved Instance and Savings Plan discounts are shared across the organization automatically.
Enabling AWS Organizations
If you haven't already created an organization, the AWS CLI makes it a single command:
# Create a new organization (run from management account)
aws organizations create-organization --feature-set ALL
# List all accounts in the org
aws organizations list-accounts
# Describe the organization root
aws organizations list-roots
# List OUs under the root
aws organizations list-organizational-units-for-parent \
--parent-id r-xxxx # replace with your root ID
The --feature-set ALL flag enables both consolidated billing AND all policy types including SCPs. Using CONSOLIDATED_BILLING only gives you billing but no SCPs — always choose ALL for a real governance setup.
OU Design and Account Structure
Your OU hierarchy determines how policies cascade. A flat structure is easier to reason about; deep nesting becomes hard to audit. The AWS recommended starting hierarchy looks like this:
Root
├── Security OU
│ ├── Log Archive Account ← CloudTrail, Config, VPC flow logs
│ └── Security Tooling Account ← GuardDuty, Security Hub, Inspector
├── Infrastructure OU
│ ├── Shared Services Account ← DNS, Active Directory, transit gateway
│ └── Network Account ← VPCs, TGW attachments, egress
├── Workloads OU
│ ├── Dev OU
│ │ └── dev-app-a account
│ ├── Staging OU
│ │ └── staging-app-a account
│ └── Prod OU
│ └── prod-app-a account
└── Sandbox OU
└── Developer sandbox accounts (short-lived, loose SCPs)
Creating an OU and moving accounts with the CLI:
# Create an OU under root
aws organizations create-organizational-unit \
--parent-id r-xxxx \
--name "Workloads"
# Create a child OU under Workloads
aws organizations create-organizational-unit \
--parent-id ou-xxxx-yyyyyyy \
--name "Prod"
# Move an account into an OU
aws organizations move-account \
--account-id 123456789012 \
--source-parent-id r-xxxx \
--destination-parent-id ou-xxxx-yyyyyyy
Service Control Policies (SCPs)
SCPs are JSON policy documents that set the permission guardrails for every IAM principal in the accounts they cover. They do not grant permissions — they only restrict them. An action is allowed only if it is permitted by both the SCP and the IAM policy attached to the principal.
FullAWSAccess SCP attached to root allows everything).
SCP Example 1: Deny All Actions Outside Approved Regions
This is one of the most commonly deployed SCPs. It prevents any resource from being created in regions you haven't approved, which limits your attack surface and keeps your compliance scope tight:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"route53:*",
"budgets:*",
"waf:*",
"cloudfront:*",
"sts:*",
"support:*",
"trustedadvisor:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"us-east-1",
"us-west-2",
"eu-west-1"
]
}
}
}
]
}
Note the NotAction — global services like IAM, Route 53, and CloudFront are not region-scoped, so you must exclude them or you'll break your own infrastructure.
SCP Example 2: Protect CloudTrail from Deletion or Disabling
This SCP prevents anyone — including account root users — from deleting or stopping the CloudTrail trail that feeds your central log archive:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCloudTrailModification",
"Effect": "Deny",
"Action": [
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging",
"cloudtrail:UpdateTrail",
"cloudtrail:PutEventSelectors"
],
"Resource": "*"
}
]
}
SCP Example 3: Enforce Mandatory Resource Tagging
This SCP blocks EC2 and RDS resource creation unless the Environment and CostCenter tags are present. Pair this with AWS Config rules for detective enforcement of existing resources:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireTagsOnEC2",
"Effect": "Deny",
"Action": [
"ec2:RunInstances",
"ec2:CreateVolume",
"rds:CreateDBInstance",
"rds:CreateDBCluster"
],
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/Environment": "true"
}
}
},
{
"Sid": "RequireCostCenterTag",
"Effect": "Deny",
"Action": [
"ec2:RunInstances",
"rds:CreateDBInstance"
],
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/CostCenter": "true"
}
}
}
]
}
Attaching an SCP to an OU with the CLI:
# Create the SCP
aws organizations create-policy \
--name "DenyOutsideRegions" \
--description "Block resources outside approved regions" \
--type SERVICE_CONTROL_POLICY \
--content file://deny-regions-scp.json
# Attach to an OU
aws organizations attach-policy \
--policy-id p-xxxxxxxxxxxx \
--target-id ou-xxxx-yyyyyyy
# List all SCPs attached to an OU
aws organizations list-policies-for-target \
--target-id ou-xxxx-yyyyyyy \
--filter SERVICE_CONTROL_POLICY
AWS Control Tower: Landing Zone
AWS Control Tower is an opinionated orchestration layer on top of AWS Organizations. It sets up a pre-configured landing zone — a baseline multi-account environment with security guardrails, centralized logging, and SSO — in about an hour. It's the fastest path from "one account" to "properly governed multi-account setup."
What Control Tower creates automatically during landing zone setup:
- Log Archive account — Receives CloudTrail logs and AWS Config snapshots from all accounts. Read-only access for most users.
- Audit account — Cross-account access for security tooling. GuardDuty, Security Hub, and AWS Config aggregators live here.
- Root OU — With the management account at the top.
- Security OU — Containing the Log Archive and Audit accounts.
- Sandbox OU — For experimental accounts with looser guardrails.
- IAM Identity Center — For federated SSO across all accounts.
- Mandatory guardrails — A set of SCPs and AWS Config rules that cannot be disabled.
Landing Zone Version and Updates
Control Tower landing zones have version numbers. When AWS releases a new version, you update the landing zone from the Control Tower console — it re-runs the baseline CloudFormation stacks across your accounts. As of 2026, landing zone version 3.3 is current, with support for customizing the Audit and Log Archive account names during setup.
Guardrails: Preventive vs Detective
Control Tower guardrails are pre-packaged governance rules. They come in two enforcement modes:
| Type | Mechanism | Effect | Example |
|---|---|---|---|
| Preventive | SCP | Blocks the action before it happens | Disallow deletion of log archive S3 bucket |
| Detective | AWS Config Rule | Detects non-compliant resources and reports them | Detect EC2 instances without instance profiles |
| Proactive | CloudFormation hook | Checks CloudFormation templates before deployment | Validate S3 buckets have encryption enabled |
Guardrails have three guidance levels:
- Mandatory — Always enabled, cannot be disabled. These protect the landing zone itself (e.g., protecting the Log Archive account).
- Strongly Recommended — Disabled by default but AWS recommends enabling them. Examples: disallow public S3 buckets, require MFA for root.
- Elective — Optional rules for specific compliance requirements (PCI-DSS, HIPAA, etc.).
You enable guardrails per OU from the Control Tower console or via the API:
# List all available guardrails
aws controltower list-enabled-controls \
--target-identifier arn:aws:organizations::123456789012:ou/o-xxxx/ou-xxxx-yyyyyyy
# Enable a guardrail on an OU
aws controltower enable-control \
--control-identifier arn:aws:controltower:us-east-1::control/AWS-GR_ENCRYPTED_VOLUMES \
--target-identifier arn:aws:organizations::123456789012:ou/o-xxxx/ou-xxxx-yyyyyyy
Account Factory and Provisioning
Account Factory is Control Tower's self-service account vending machine. It uses AWS Service Catalog under the hood to let authorized users request new accounts with a standardized baseline configuration. Each new account gets:
- The baseline CloudTrail trail configured to ship to Log Archive
- AWS Config enabled and pointing to the Audit account aggregator
- A VPC (optional — you can skip the default VPC)
- An IAM Identity Center permission set pre-assigned to the account
- All mandatory and OUlevel guardrails applied automatically
You can customize the baseline further using Account Factory for Terraform (AFT) — Control Tower's officially supported Terraform integration that runs customization pipelines after every account vending operation.
Multi-Account Strategy Patterns
There is no single right answer for how many accounts to create or how to structure them. The patterns below cover the most common needs:
Pattern 1: Environment-Per-Account (Most Common)
Each environment (sandbox, dev, staging, prod) gets its own account per application team. This is the recommended pattern for most organizations because it provides hard blast-radius isolation and makes cost attribution trivial.
Workloads OU
├── Sandbox OU — short-lived experiments, loose SCPs, auto-expire
├── Dev OU — developers iterate here, moderate SCPs
├── Staging OU — mirrors production topology, strict SCPs
└── Prod OU — production, strictest SCPs, change control required
Pattern 2: Security Foundation Accounts
Security tooling should live in dedicated accounts, separate from workloads. This prevents workload teams from accidentally or intentionally modifying security controls:
- Log Archive Account — Immutable S3 bucket with Object Lock enabled. All accounts ship CloudTrail, Config, VPC flow logs, and ALB access logs here. The SCP on this account denies all S3 delete actions except from the logging service principals.
- Security Tooling Account — GuardDuty administrator, Security Hub aggregator, Amazon Macie, Inspector v2 delegated admin. Your security team has read access here; nobody else does.
- Network Account — AWS Transit Gateway, centralized egress NAT Gateway, Route 53 Resolver rules. Centralizing networking eliminates duplicate NAT Gateway costs and simplifies firewall management.
Pattern 3: Shared Services Account
Resources used across all accounts — internal container registries (ECR), artifact repos (CodeArtifact), Active Directory (AWS Managed AD), and internal APIs — live in a shared services account. Workload accounts access them via VPC peering or Transit Gateway.
AWS IAM Identity Center (SSO)
AWS IAM Identity Center (formerly AWS SSO) is the recommended way to give humans access to multiple AWS accounts. Instead of creating IAM users in every account, you define permission sets centrally and assign them to users or groups from your identity provider.
Key Concepts
- Identity Source — Where users come from: built-in Identity Center directory, Active Directory (via AD Connector or AWS Managed AD), or an external IdP (Okta, Azure AD, Ping) via SCIM provisioning and SAML 2.0.
- Permission Sets — Named bundles of IAM policies (managed or inline) that define what a user can do in an account. Examples:
AdministratorAccess,ReadOnlyAccess,DeveloperAccess. - Account Assignments — The mapping of (user or group) + (permission set) + (account). One assignment lets a user assume that role in that account.
When a developer logs into the SSO portal (https://your-org.awsapps.com/start), they see a list of all accounts they have access to and can click into any of them without ever touching an IAM user or long-lived access key.
Setting Up IAM Identity Center with CLI
# Enable IAM Identity Center (done once in management account)
# This is typically done via console, but you can use the API after
# Create a permission set
aws sso-admin create-permission-set \
--instance-arn arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxx \
--name "DeveloperAccess" \
--description "Developer access for workload accounts" \
--session-duration "PT8H"
# Attach a managed policy to the permission set
aws sso-admin attach-managed-policy-to-permission-set \
--instance-arn arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxx \
--permission-set-arn arn:aws:sso:::permissionSet/ssoins-xxx/ps-xxx \
--managed-policy-arn arn:aws:iam::aws:policy/PowerUserAccess
# Create an account assignment
aws sso-admin create-account-assignment \
--instance-arn arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxx \
--target-id 123456789012 \
--target-type AWS_ACCOUNT \
--permission-set-arn arn:aws:sso:::permissionSet/ssoins-xxx/ps-xxx \
--principal-type GROUP \
--principal-id group-id-from-idp
Using SSO Credentials in CLI and Terraform
# Configure a named profile with SSO
aws configure sso
# Follow prompts: SSO start URL, SSO region, account ID, role name
# Or add manually to ~/.aws/config:
# [profile dev-developer]
# sso_start_url = https://your-org.awsapps.com/start
# sso_region = us-east-1
# sso_account_id = 123456789012
# sso_role_name = DeveloperAccess
# region = us-east-1
# Login
aws sso login --profile dev-developer
# Use the profile
aws s3 ls --profile dev-developer
# In Terraform, use the SSO profile directly:
# provider "aws" {
# profile = "prod-admin"
# region = "us-east-1"
# }
Terraform and CDK Automation
Manual account creation is fine for five accounts. At fifty accounts, you need automation. AWS offers Account Factory for Terraform (AFT) — a reference Terraform solution that wraps Control Tower's Account Factory with a GitOps pipeline.
Account Factory for Terraform (AFT) Architecture
AFT works like this: you push an account request to a Git repo. A CodePipeline picks it up, calls the Account Factory Service Catalog product to create the account, then runs your Terraform customizations (global customizations that apply to all accounts, plus per-account customizations). The pipeline is fully idempotent — re-running it brings accounts back to the desired state.
Terraform: Creating an Account Request in AFT
# account-request/prod-payments.tf
module "prod_payments_account" {
source = "github.com/aws-ia/terraform-aws-control_tower_account_factory//modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "aws-prod-payments@yourcompany.com"
AccountName = "prod-payments"
ManagedOrganizationalUnit = "Prod"
SSOUserEmail = "aws-prod-payments@yourcompany.com"
SSOUserFirstName = "Platform"
SSOUserLastName = "Team"
}
account_tags = {
Environment = "prod"
CostCenter = "payments-team"
Owner = "platform-engineering"
}
change_management_parameters = {
change_requested_by = "terraform"
change_reason = "New payments service account"
}
account_customizations_name = "payments-baseline"
}
Terraform: Deploying SCPs with AWS Organizations Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
profile = "management-admin"
}
# Fetch the organization root
data "aws_organizations_organization" "org" {}
# Create the Prod OU
resource "aws_organizations_organizational_unit" "prod" {
name = "Prod"
parent_id = data.aws_organizations_organization.org.roots[0].id
}
# Deny-regions SCP
resource "aws_organizations_policy" "deny_outside_regions" {
name = "DenyOutsideApprovedRegions"
description = "Block resource creation outside us-east-1 and us-west-2"
type = "SERVICE_CONTROL_POLICY"
content = file("${path.module}/policies/deny-regions.json")
}
# Attach SCP to Prod OU
resource "aws_organizations_policy_attachment" "prod_region_restriction" {
policy_id = aws_organizations_policy.deny_outside_regions.id
target_id = aws_organizations_organizational_unit.prod.id
}
# CloudTrail protection SCP attached to root
resource "aws_organizations_policy" "protect_cloudtrail" {
name = "ProtectCloudTrail"
description = "Prevent CloudTrail from being disabled or deleted"
type = "SERVICE_CONTROL_POLICY"
content = file("${path.module}/policies/protect-cloudtrail.json")
}
resource "aws_organizations_policy_attachment" "root_cloudtrail_protection" {
policy_id = aws_organizations_policy.protect_cloudtrail.id
target_id = data.aws_organizations_organization.org.roots[0].id
}
AWS CDK: Account Baseline Stack
If your team prefers CDK, you can use the aws-cdk-lib/aws-organizations L1 constructs (CloudFormation resources) or community L2 constructs to manage organization resources:
import * as cdk from 'aws-cdk-lib';
import * as organizations from 'aws-cdk-lib/aws-organizations';
export class OrgBaselineStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create Prod OU
const prodOU = new organizations.CfnOrganizationalUnit(this, 'ProdOU', {
name: 'Prod',
parentId: this.node.tryGetContext('orgRootId'),
});
// Create SCP
const denyRegionsSCP = new organizations.CfnPolicy(this, 'DenyRegionsSCP', {
name: 'DenyOutsideApprovedRegions',
description: 'Block resources outside approved regions',
type: 'SERVICE_CONTROL_POLICY',
content: JSON.stringify({
Version: '2012-10-17',
Statement: [{
Sid: 'DenyOutsideRegions',
Effect: 'Deny',
NotAction: ['iam:*', 'sts:*', 'route53:*', 'cloudfront:*'],
Resource: '*',
Condition: {
StringNotEquals: {
'aws:RequestedRegion': ['us-east-1', 'us-west-2'],
},
},
}],
}),
targetIds: [prodOU.ref],
});
}
}
Best Practices
1. Separate Accounts Per Environment
Never share dev and prod in the same account. The cost of a misconfigured IAM policy deleting production data far exceeds the overhead of managing two accounts. Use Control Tower's Account Factory to make this low-friction — provisioning a new account takes 20 minutes and is fully automated.
2. Apply Least Privilege SCPs
Start with FullAWSAccess on the root (Control Tower default) and layer restrictive SCPs on each OU as you descend. Don't try to write a single monolithic "allow only these services" SCP — it's brittle. Instead write targeted deny SCPs for specific risks: deny-regions, deny-cloudtrail-modification, deny-vpc-changes, deny-root-account-usage.
3. Centralized Logging is Non-Negotiable
Every account must ship CloudTrail logs to the Log Archive account's S3 bucket. Enable S3 Object Lock in Compliance mode on the log bucket so no one — including the Log Archive account's root user — can delete logs before the retention period expires. See our CloudWatch Monitoring guide for setting up metric filters and alarms on aggregated logs.
4. Never Run Workloads in the Management Account
The management account has god-mode access to your entire organization. If it's compromised, every account is compromised. Run only governance tooling here: Organizations, Control Tower, SCPs, Consolidated Billing. All actual compute, storage, and databases go in member accounts.
5. Enable GuardDuty Organization-Wide
Designate the Audit account as the GuardDuty delegated administrator. From there, auto-enable GuardDuty in every new account created in the organization. You get threat detection across all accounts with a single management plane and aggregate findings in Security Hub.
6. Use IAM Identity Center, Not IAM Users
Delete the IAM users you created in individual accounts for CLI access. Use IAM Identity Center with short-lived credentials (8-hour session tokens). Integrate with your corporate IdP so accounts are automatically deprovisioned when employees leave. See our IAM Roles and Policies guide for the underlying permission model.
7. Tag Everything, Enforce It with SCPs
Consolidated billing without tagging tells you the total but not the breakdown. Enforce at minimum Environment, CostCenter, and Owner tags via SCP on resource creation. Use AWS Cost Explorer's tag grouping to get per-team cost reports without any manual work.
8. Automate with Terraform or CDK
Every OU, SCP, and account assignment should be in version control. Manual console changes to SCPs are a disaster waiting to happen — one mistyped SCP attached to the root OU can lock everyone out. Use Terraform's AWS Organizations provider (shown above) with a CI/CD pipeline, code review requirement, and plan-before-apply enforcement. Reference our AWS Terraform guide for the provider setup and state management patterns.
Frequently Asked Questions
How many AWS accounts should I have?
A common rule of thumb: at minimum, one account per environment (dev, staging, prod) per application team, plus the security foundation accounts (Log Archive, Audit, Network, Shared Services). A 10-team organization with 3 environments typically ends up with 30–50 workload accounts plus 5–8 infrastructure accounts. That sounds like a lot, but with Control Tower and IAM Identity Center, the operational overhead per account is close to zero.
Can I add existing accounts to an Organization?
Yes. You can invite existing accounts to join your organization from the Organizations console. The invited account's owner receives an email and must accept. After joining, you can apply SCPs immediately, but Control Tower enrollment (which configures the account baseline) is a separate step done from the Control Tower console using the "Enroll account" feature. Enrolling an existing account retrofits the CloudTrail, Config, and VPC baseline — it doesn't destroy existing resources.
What's the difference between an SCP and an IAM permission boundary?
Both restrict the maximum permissions a principal can have, but they operate at different scopes. An SCP applies at the account level (or OU level) and limits all principals in those accounts, including the root user. A permission boundary applies to a specific IAM user or role — it restricts what that individual entity can do, but other entities in the same account are unaffected. SCPs are for organizational governance; permission boundaries are for delegating permission management to teams without letting them escalate their own privileges.
Does Control Tower cost money?
Control Tower itself has no additional charge. You pay for the underlying services it uses: CloudTrail data events (if enabled), AWS Config rules, Service Catalog, and the S3 storage for logs in your Log Archive account. For most organizations, the Control Tower overhead adds $20–80/month depending on the number of accounts and Config rules active.
How do I handle Terraform state for multi-account infrastructure?
Use a dedicated S3 bucket in your management or shared services account for Terraform state, with one state file per account/environment. Enable S3 versioning and DynamoDB state locking. Your CI/CD pipeline assumes a role in the target account using STS AssumeRole (with an assume-role policy that allows only the CI/CD role from the management account to assume it). See the CloudFormation guide for an alternative StackSets-based approach to deploying baselines across accounts.
How do I monitor compliance with SCPs and guardrails?
AWS Config records every configuration change and evaluates Config rules (detective guardrails). Security Hub aggregates findings from Config, GuardDuty, Inspector, and Macie into a single dashboard in your Audit account. For SCP denials specifically, CloudTrail logs every denied API call with the error code AccessDenied and the SCP context. Set up CloudWatch metric filters on CloudTrail to alert on unusual volumes of SCP denials, which can indicate either misconfigured automation or a potential security event.