DevOpsil
AWS
92%
Needs Review

Cloud Landing Zone Architecture: Account Structure Done Right

Asif MuzammilAsif Muzammil6 min read

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:

  1. AWS Organizations — Account hierarchy and policy inheritance
  2. Service Control Policies (SCPs) — Guardrails that apply at the OU level
  3. Account Baseline — Config, CloudTrail, Security Hub, GuardDuty, every account
  4. 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:

ServicePurposeConfig
CloudTrailAPI audit logOrg-level trail → central S3 bucket
AWS ConfigResource complianceAll resources, central aggregator
GuardDutyThreat detectionOrg-level enrollment from Security account
Security HubFindings aggregationFSBP + CIS standards
IAM Access AnalyzerExternal access alertsZone of trust = organization
Budget alertsCost guardrailEmail + 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.

Share:

Was this article helpful?

Asif Muzammil
Asif Muzammil

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

AWSTutorialBeginnerNeeds Review

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.

Aareez Asif·
6 min read
AWSQuick RefBeginnerNeeds Review

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.

Sarah Chen·
3 min read

More in AWS

View all →
AWSQuick RefBeginnerNeeds Review

AWS CLI: Cheat Sheet

AWS CLI cheat sheet with copy-paste commands for EC2, S3, IAM, Lambda, ECS, CloudFormation, SSM, and Secrets Manager operations.

Dev Patel·
3 min read

Discussion