Implement AWS Control Tower for a 20-Account Organization
Set up AWS Control Tower with Account Factory, custom guardrails via SCPs, account vending automation with AFT, and developer self-service sandbox accounts.
Your company has grown from 3 accounts to 20 in the past year, all created manually with inconsistent naming, tagging, and security baselines. The platform team spends 4 hours setting up each new account. Engineers complain they wait 2 weeks for a dev account. You're asked to implement AWS Control Tower to automate account vending and enforce governance without slowing teams down.
The Problem
Without Control Tower, each new AWS account is a blank canvas. Security baselines drift. CloudTrail might be enabled in one account, disabled in another. IAM roles get created inconsistently. Compliance evidence collection requires manual effort across 20 accounts.
Control Tower provides: a single enrollment process, automated guardrails, and a landing zone that every new account inherits.
Step 1: OU Structure Design
Design your OU hierarchy before enabling Control Tower — reorganizing OUs after enrollment is disruptive:
Root
└── AWS Control Tower (managed by Control Tower)
├── Security OU ← Log Archive + Audit accounts (auto-created)
│
├── Infrastructure OU
│ ├── Network Account ← Transit Gateway, DNS
│ └── Shared Services ← ECR, artifact repos
│
├── Workloads OU
│ ├── Production OU ← Strict SCPs, change control
│ │ └── prod-app-* accounts
│ └── Non-Production OU ← Relaxed SCPs
│ ├── staging-app-* accounts
│ └── dev-app-* accounts
│
└── Sandbox OU ← Self-service, auto-expires 30 days
└── eng-sandbox-* accounts
Guardrail strategy per OU:
| OU | Preventive Guardrails | Detective Guardrails |
|---|---|---|
| All OUs | Deny root usage, deny non-compliant regions | CloudTrail enabled, MFA on root |
| Production OU | Deny internet gateway creation without approval, deny direct S3 public access | Detect unencrypted EBS volumes |
| Sandbox OU | Budget alert at $200/month | Detect EC2 without required tags |
Step 2: Enable Control Tower
# Control Tower is enabled via the AWS Console only (no CLI/API)
# Navigate to: AWS Console → Control Tower → Set up landing zone
# Pre-requisites:
# 1. Management account must NOT have existing CloudTrail or AWS Config in target regions
# 2. IAM Identity Center must not already be configured
# 3. You need 3 accounts minimum: management, log archive, audit
# After enabling, Control Tower creates:
# - AWSControlTowerAdminRole (assume from management account)
# - AWSControlTowerCloudTrailRole (for centralized CloudTrail)
# - AWSControlTowerConfigAggregatorRole (for centralized Config)
Step 3: Account Factory for Terraform (AFT)
AFT replaces the manual account setup process with a Git-based workflow. Engineers submit a PR to request a new account; AFT provisions it automatically.
# Deploy AFT (one-time setup in management account)
git clone https://github.com/aws-ia/terraform-aws-control_tower_account_factory
# aft-bootstrap/main.tf
module "aft" {
source = "github.com/aws-ia/terraform-aws-control_tower_account_factory"
ct_management_account_id = "123456789012"
log_archive_account_id = "234567890123"
audit_account_id = "345678901234"
aft_management_account_id = "456789012345"
ct_home_region = "us-east-1"
tf_backend_secondary_region = "us-west-2"
# Store AFT state in S3
terraform_s3_bucket_sse_algorithm = "aws:kms"
}
Account request — engineers submit a PR to this repo:
# accounts/prod-app-payments/account-request.tf
module "account_request" {
source = "github.com/aws-ia/terraform-aws-control_tower_account_factory//modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "[email protected]"
AccountName = "prod-app-payments"
ManagedOrganizationalUnit = "Workloads/Production"
SSOUserEmail = "[email protected]"
SSOUserFirstName = "Alice"
SSOUserLastName = "Smith"
}
account_tags = {
team = "payments"
environment = "production"
cost-center = "CC-PAYMENTS-001"
owner = "[email protected]"
}
change_management_parameters = {
change_requested_by = "[email protected]"
change_reason = "New payments service — Q2 2025 launch"
}
}
PR is approved → AFT runs:
- Creates the account in AWS Organizations
- Enrolls it in Control Tower (applies landing zone baseline)
- Runs account customizations (installs GuardDuty, sets budget alerts, configures VPC)
- Sets up SSO permissions for the requesting team
Total time: ~20 minutes vs. the previous 4-hour manual process.
Step 4: Account Customizations — Baseline Every Account
AFT supports customizations that run on every new account. Add your security baseline here:
# aft-account-customizations/terraform/main.tf (runs on every new account)
# Enable GuardDuty
resource "aws_guardduty_detector" "main" {
enable = true
datasources {
s3_logs { enable = true }
kubernetes { audit_logs { enable = true } }
malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } }
}
}
# Budget alert
resource "aws_budgets_budget" "account_budget" {
name = "account-monthly-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 = [var.account_owner_email]
}
}
# Default VPC removal (security best practice)
resource "aws_default_vpc" "default" {
force_destroy = true
}
Step 5: Self-Service Sandbox Accounts
Give engineers self-service sandbox accounts with automatic expiry and spending caps:
# accounts/eng-sandbox-alice/account-request.tf
module "account_request" {
source = "github.com/aws-ia/terraform-aws-control_tower_account_factory//modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "[email protected]"
AccountName = "eng-sandbox-alice"
ManagedOrganizationalUnit = "Sandbox"
SSOUserEmail = "[email protected]"
}
custom_fields = {
expiry_date = "2025-07-01" # Auto-suspend after 30 days
owner = "[email protected]"
}
}
# Lambda: check sandbox account expiry daily
import boto3
from datetime import datetime
def check_sandbox_expiry(event, context):
org = boto3.client('organizations')
accounts = org.list_accounts_for_parent(
ParentId=SANDBOX_OU_ID
)
for account in accounts['Accounts']:
tags = org.list_tags_for_resource(ResourceId=account['Id'])
expiry = next((t['Value'] for t in tags['Tags'] if t['Key'] == 'expiry_date'), None)
if expiry and datetime.strptime(expiry, '%Y-%m-%d') < datetime.now():
# Suspend account
org.close_account(AccountId=account['Id'])
Step 6: Migrate Existing Accounts
For the 20 accounts already in use, enroll them into Control Tower incrementally:
# Enroll existing account via Control Tower console
# OR use Account Factory with the existing account's email:
# In Account Factory, choose "Enroll account"
# Provide: existing account ID, email, target OU
# Control Tower will:
# - Add mandatory guardrails
# - Configure centralized CloudTrail
# - Set up AWS Config aggregation
# - Apply Security Hub standards
Risk mitigation: Test enrollment on your least-critical accounts first. Some existing SCPs or IAM configurations may conflict with Control Tower’s required roles.
Summary
| Before Control Tower | After Control Tower |
|---|---|
| 4+ hours to create account | ~20 minutes (automated) |
| 2-week wait for dev accounts | Self-service sandbox in 20 minutes |
| Inconsistent security baseline | Mandatory guardrails enforced on every account |
| Manual compliance evidence | Centralized Config + CloudTrail in Log Archive |
| Engineers share admin creds | SSO federation, individual attribution |
- How to structure OUs for compliance and developer agility
- How Account Factory for Terraform (AFT) automates account creation
- Which guardrails to make mandatory vs optional per OU
- How to give developers self-service sandbox accounts
- How to migrate existing accounts into Control Tower without disruption
Have a similar scenario to share?
Production incidents are the best teachers. Submit your real-world scenario and help others learn.
Open Google FormRelated Scenarios
Design an IAM Strategy for a 500-Engineer Organization With SOC2 & PCI-DSS Compliance
The Problem Shared admin credentials violate the most fundamental security principle: least privilege. With shared credentials, you …
Handling Terraform State Drift in Production
The Problem Drift is when your live AWS infrastructure no longer matches what Terraform’s state file describes. It happens when: …
Migrate a Java Monolith From On-Premises to AWS With Minimal Downtime
The Problem A 6-month deadline forces choices. You don’t have time to re-architect the monolith into microservices. You need to move …