Cloud Landing Zone Architecture: Account Structure Done Right
The Problem With "We'll Figure Out Accounts Later"
Every company I've worked with that didn't design their AWS account structure upfront eventually paid for it. Usually around the 18-month mark, when they have 40 accounts with inconsistent naming, no guardrails, three different VPC CIDR ranges colliding, and a security team asking uncomfortable questions about who has admin access to what.
An AWS Landing Zone is the answer to "how do we structure this from the start?" — or more often in practice, "how do we clean up this mess?" Either way, the components are the same.
The Core Components
A well-designed Landing Zone has four layers:
- AWS Organizations — Account hierarchy and policy inheritance
- Service Control Policies (SCPs) — Guardrails that apply at the OU level
- Account Baseline — Config, CloudTrail, Security Hub, GuardDuty, every account
- Account Vending — Automated provisioning via Control Tower or custom tooling
Account Hierarchy Design
Resist the urge to create OUs per team. You'll end up with 30 OUs and contradictory SCPs. The pattern that works at scale:
Root
├── Security OU
│ ├── Log Archive Account
│ └── Security Tooling Account
├── Infrastructure OU
│ ├── Network Account (Transit Gateway, Route53 Resolvers)
│ └── Shared Services Account (Artifact Registry, CI/CD)
├── Workloads OU
│ ├── Production OU
│ │ └── [app-prod accounts]
│ ├── Non-Production OU
│ │ └── [app-dev, app-staging accounts]
│ └── Sandbox OU
│ └── [individual developer sandbox accounts]
├── Exceptions OU
│ └── [accounts that need SCP exclusions — audit carefully]
└── Suspended OU
└── [accounts pending deletion]
The Suspended OU matters. Don't delete accounts immediately — AWS holds them for 90 days. Move them here and apply a denyAll SCP so they can't be used but you haven't lost the account history.
Service Control Policies That Actually Matter
SCPs are not IAM policies. They define the maximum permissions available in an account — they don't grant anything. Write them to deny what you never want, not to allow what you do want.
Essential SCP 1: Prevent leaving the organization
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyLeavingOrg",
"Effect": "Deny",
"Action": [
"organizations:LeaveOrganization"
],
"Resource": "*"
}
]
}
Essential SCP 2: Require MFA for sensitive actions
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyWithoutMFA",
"Effect": "Deny",
"Action": [
"iam:CreateVirtualMFADevice",
"iam:DeleteVirtualMFADevice",
"iam:DeactivateMFADevice",
"iam:CreateUser",
"iam:AttachUserPolicy",
"organizations:*"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Essential SCP 3: Restrict to approved regions
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"support:*",
"sts:*",
"cloudfront:*",
"route53:*",
"waf:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2", "eu-west-1"]
}
}
}
]
}
Note the NotAction — global services like IAM, Route53, and CloudFront operate from us-east-1 regardless of where you're working. If you deny those, you'll have a very bad day.
Account Baseline with Terraform
Every account that gets vended needs a consistent baseline deployed. Structure it as a Terraform module that gets applied via your CI/CD pipeline:
module "account_baseline" {
source = "git::https://github.com/your-org/tf-modules//account-baseline?ref=v1.4.0"
account_id = var.account_id
account_name = var.account_name
environment = var.environment
cloudtrail_bucket_arn = "arn:aws:s3:::org-cloudtrail-logs-central"
config_bucket_arn = "arn:aws:s3:::org-config-logs-central"
security_hub_standards = ["aws-foundational-security-best-practices/v/1.0.0"]
guardduty_detector_enabled = true
config_recorder_enabled = true
budget_limit_usd = var.budget_limit
budget_alert_email = var.billing_contact
}
The baseline module should enable at minimum:
| Service | Purpose | Config |
|---|---|---|
| CloudTrail | API audit log | Org-level trail → central S3 bucket |
| AWS Config | Resource compliance | All resources, central aggregator |
| GuardDuty | Threat detection | Org-level enrollment from Security account |
| Security Hub | Findings aggregation | FSBP + CIS standards |
| IAM Access Analyzer | External access alerts | Zone of trust = organization |
| Budget alerts | Cost guardrail | Email + SNS on 80%, 100% |
Account Vending: Control Tower vs. Custom
AWS Control Tower is the managed option. It handles the organizational scaffolding, deploys baseline via "Account Factory," and gives you a dashboard. The trade-off: it's opinionated, upgrades can be painful, and customization requires Account Factory for Terraform (AFT) — which is excellent but adds complexity.
Custom vending with Service Catalog + Terraform gives you full control. The flow:
Developer submits request (YAML in Git)
↓
GitHub Actions pipeline triggers
↓
aws organizations create-account \
--email "[email protected]" \
--account-name "company-appname-env"
↓
Assume OrganizationAccountAccessRole in new account
↓
Apply account_baseline Terraform module
↓
Create VPC via Network account's TGW attachment
↓
Notify requester via Slack
# Assume the cross-account role that every new account gets automatically
NEW_ACCOUNT_ID="123456789012"
CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::${NEW_ACCOUNT_ID}:role/OrganizationAccountAccessRole" \
--role-session-name "account-vending" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text)
export AWS_ACCESS_KEY_ID=$(echo $CREDS | awk '{print $1}')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | awk '{print $2}')
export AWS_SESSION_TOKEN=$(echo $CREDS | awk '{print $3}')
# Now run baseline Terraform in the new account context
terraform -chdir=account-baseline init && terraform apply -auto-approve \
-var="account_id=${NEW_ACCOUNT_ID}" \
-var="account_name=company-appname-prod"
Common Mistakes
Mistake 1: Putting everything in one OU. You lose the ability to apply different SCPs per workload classification. Production should have tighter guardrails than sandboxes.
Mistake 2: Using root credentials for anything. Create an IAM Identity Center (SSO) configuration on day one. Root credentials should be locked in a vault and used only for account recovery scenarios.
Mistake 3: Inconsistent account naming. Pick a convention and enforce it via the vending pipeline: {company}-{workload}-{environment}. Consistent names make billing, tagging, and access policies dramatically simpler.
Mistake 4: Forgetting the Exceptions OU. Every org eventually has a legacy account that can't comply with a specific SCP. Create the Exceptions OU, document why each account is there, and review it quarterly.
Validating Your Landing Zone
# Check all accounts have CloudTrail enabled
aws organizations list-accounts --query 'Accounts[*].Id' --output text | \
tr '\t' '\n' | while read ACCOUNT_ID; do
TRAIL_STATUS=$(aws cloudtrail get-trail-status \
--name org-trail \
--query 'IsLogging' \
--output text 2>/dev/null || echo "NOT_FOUND")
echo "${ACCOUNT_ID}: ${TRAIL_STATUS}"
done
# List all SCPs and their attachment points
aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
--query 'Policies[*].{Name:Name,Id:Id}' --output table
A well-built landing zone is not a one-time project. It's the foundation everything else runs on. Invest the time upfront — you'll spend far less time fighting your own infrastructure later.
Was this article helpful?
Senior Cloud Architect
AWS, GCP, and Azure — I've built production workloads on all three. From landing zone design to multi-region failover, I architect cloud infrastructure that scales without surprises. Well-Architected Framework isn't a checklist, it's a mindset.
Related Articles
AWS Lambda Cold Start Optimization Using Provisioned Concurrency And SnapStart
Cold starts are the silent SLO killer in serverless architectures. You've built a beautiful Lambda function, deployed it, and everything looks great in you...
AWS ECS Fargate: Serverless Container Deployment Without Managing Nodes
Deploy containers on AWS ECS Fargate — no EC2 instances to manage. Covers task definitions, services, load balancers, environment variables, secrets from SSM/Secrets Manager, auto-scaling, and CI/CD integration.
AWS EKS: Production Kubernetes Cluster Setup from Scratch
Step-by-step guide to launching a production-ready EKS cluster on AWS — node groups, IAM roles, VPC configuration, managed add-ons, kubeconfig setup, and cost optimization. Both eksctl and Terraform approaches covered.
Fix AWS EC2 Instance 'Status Check Failed' Errors
Diagnose and recover AWS EC2 instances with failed system or instance status checks using stop/start, volume rescue, and auto-recovery techniques.
Fix AWS S3 'Access Denied' Errors
Systematically troubleshoot and fix AWS S3 Access Denied errors caused by IAM policies, bucket policies, ACLs, and encryption settings.
AWS IAM Least Privilege: Policies That Won't Lock You Out
Build AWS IAM policies using least privilege without locking yourself out — practical patterns for roles, service accounts, and permission boundaries.
More in AWS
View all →AWS CLI: Cheat Sheet
AWS CLI cheat sheet with copy-paste commands for EC2, S3, IAM, Lambda, ECS, CloudFormation, SSM, and Secrets Manager operations.
AWS Core Services: The DevOps Engineer's Essential Guide
Navigate the essential AWS building blocks — EC2, S3, VPC, IAM, RDS, Lambda, and EKS explained for DevOps engineers with practical examples.