Skip to content

Landing Zones — Multi-Account Strategy

This is the foundation — every other topic (IAM, networking, Kubernetes, security, observability) builds on the organizational structure created here.

Landing Zone topic overview — foundation and dependent topics

As the central infrastructure team, you own the landing zone. You design the OU/folder hierarchy, write SCPs and org policies, build the account vending machine, and define the security baseline that every new account gets automatically. Tenant teams submit a request and receive a fully-configured, network-connected, security-baselined account within minutes.


A landing zone is a pre-configured, secure, multi-account/project cloud environment that provides:

  1. Account/project structure — logical separation by team, environment, and function
  2. Identity and access management — centralized SSO, role-based access
  3. Networking — hub-and-spoke VPCs, centralized egress/ingress, DNS
  4. Security baseline — logging, monitoring, encryption, guardrails
  5. Governance — preventive controls, detective controls, cost management
  6. Automation — account/project vending, baseline deployment, drift detection

Why enterprises need one BEFORE any workload migration:

  • Without a landing zone, teams create accounts/projects ad-hoc with no standards
  • No centralized logging = no audit trail = compliance failure
  • No network architecture = no private connectivity between workloads
  • No guardrails = shadow IT, cost overruns, security incidents
  • Retrofitting governance onto 100 unmanaged accounts is 10x harder than building it right from the start

Every landing zone enforces guardrails at three levels:

TypePurposeAWS ImplementationGCP Implementation
PreventiveBlock actions before they happenSCPs, IAM permission boundariesOrg policies, IAM deny policies
DetectiveAlert when violations occurAWS Config rules, Security HubSCC findings, Cloud Asset Inventory
CorrectiveAuto-remediate violationsConfig auto-remediation, LambdaCloud Functions triggered by SCC

Guardrails taxonomy — Preventive, Detective, Corrective


AWS uses account-based isolation managed through Control Tower and Account Factory for Terraform (AFT). GCP uses project-based isolation with a folder hierarchy managed through Project Factory. Both achieve multi-tenant isolation with guardrails, but the primitives differ.

AWS Landing Zone — Control Tower and Beyond

Section titled “AWS Landing Zone — Control Tower and Beyond”

AWS Control Tower — The Managed Solution

Section titled “AWS Control Tower — The Managed Solution”

AWS Control Tower provides a managed landing zone with:

AWS Control Tower account structure

ComponentPurpose
Landing ZoneThe overall multi-account environment (currently version 4.0)
Organizational Units (OUs)Logical grouping for accounts (Security, Sandbox, Workloads)
Controls (Guardrails)Preventive (SCP-based), detective (Config-based), proactive (CloudFormation hooks)
Account FactoryConsole-based or API-driven account provisioning with baselines
DashboardCompliance status across all accounts and controls
Landing Zone APIsProgrammatic management of baselines and controls

Service Control Policies (SCPs) — Deep Dive

Section titled “Service Control Policies (SCPs) — Deep Dive”

SCPs define the maximum permissions for accounts in an OU. They do not grant permissions — they act as a guardrail (similar to permission boundaries but at the account level).

Essential SCPs for an enterprise bank:

// SCP 1: Deny regions outside approved list
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnapprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"sts:*",
"organizations:*",
"support:*",
"budgets:*",
"cloudfront:*",
"route53:*",
"wafv2:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"ap-southeast-1",
"me-south-1",
"eu-west-1"
]
}
}
}
]
}
// SCP 2: Prevent disabling security services
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDisablingSecurityServices",
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"guardduty:UpdateDetector",
"securityhub:DisableSecurityHub",
"securityhub:DeleteMembers",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"access-analyzer:DeleteAnalyzer"
],
"Resource": "*"
}
]
}
// SCP 3: Deny creating IAM users and access keys (force SSO/roles)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIAMUsersAndKeys",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:CreateLoginProfile"
],
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": [
"arn:aws:iam::*:role/BreakGlassRole"
]
}
}
}
]
}
// SCP 4: Deny public S3 buckets
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicS3",
"Effect": "Deny",
"Action": [
"s3:PutBucketPublicAccessBlock"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:PublicAccessBlockConfiguration/BlockPublicAcls": "true",
"s3:PublicAccessBlockConfiguration/BlockPublicPolicy": "true",
"s3:PublicAccessBlockConfiguration/IgnorePublicAcls": "true",
"s3:PublicAccessBlockConfiguration/RestrictPublicBuckets": "true"
}
}
}
]
}

AFT is the Terraform-native way to provision accounts through Control Tower. It uses a GitOps workflow:

AFT pipeline — from PR to account ready

AFT Repository Structure:

AFT repository structure — account requests, global customizations, account customizations, provisioning hooks

AFT Account Request example:

aft-account-request/team-alpha-prod.tf
module "team_alpha_prod" {
source = "./modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "aws+team-alpha-prod@bank.com"
AccountName = "team-alpha-prod"
ManagedOrganizationalUnit = "Workloads/Production"
SSOUserEmail = "platform-admin@bank.com"
SSOUserFirstName = "Platform"
SSOUserLastName = "Admin"
}
account_tags = {
Team = "team-alpha"
Environment = "production"
CostCenter = "CC-1234"
DataClass = "confidential"
Compliance = "pci-dss"
}
account_customizations_name = "team-alpha-prod"
change_management_parameters = {
change_requested_by = "platform-team"
change_reason = "New production account for Team Alpha payments service"
}
}

Global Baseline — What Every Account Gets

Section titled “Global Baseline — What Every Account Gets”
aft-global-customizations/terraform/main.tf
# This runs in EVERY new account automatically
# 1. VPC from IPAM
module "vpc" {
source = "../../modules/baseline-vpc"
ipam_pool_id = data.aws_ssm_parameter.ipam_pool_id.value
netmask_length = 22 # /22 = 1024 IPs per account
az_count = 3
enable_flow_logs = true
flow_log_destination = data.aws_ssm_parameter.central_log_bucket_arn.value
}
# 2. Transit Gateway attachment (connect to hub)
resource "aws_ec2_transit_gateway_vpc_attachment" "hub" {
subnet_ids = module.vpc.private_subnet_ids
transit_gateway_id = data.aws_ssm_parameter.tgw_id.value
vpc_id = module.vpc.vpc_id
transit_gateway_default_route_table_association = false
transit_gateway_default_route_table_propagation = false
tags = { Name = "tgw-attach-${var.account_name}" }
}
# 3. IAM baseline roles
resource "aws_iam_role" "admin_role" {
name = "PlatformAdminRole"
assume_role_policy = data.aws_iam_policy_document.sso_trust.json
}
resource "aws_iam_role" "readonly_role" {
name = "DeveloperReadOnlyRole"
assume_role_policy = data.aws_iam_policy_document.sso_trust.json
}
# 4. AWS Config recorder
resource "aws_config_configuration_recorder" "main" {
name = "default"
role_arn = aws_iam_role.config_role.arn
recording_group {
all_supported = true
include_global_resource_types = true
}
}
resource "aws_config_delivery_channel" "main" {
name = "default"
s3_bucket_name = data.aws_ssm_parameter.config_bucket.value
depends_on = [aws_config_configuration_recorder.main]
}
# 5. GuardDuty member (auto-join delegated admin)
resource "aws_guardduty_member" "member" {
provider = aws.security_admin
account_id = var.account_id
detector_id = data.aws_guardduty_detector.admin.id
email = var.account_email
invite = true
}
# 6. Security Hub member
resource "aws_securityhub_member" "member" {
provider = aws.security_admin
account_id = var.account_id
email = var.account_email
}
# 7. Default EBS encryption
resource "aws_ebs_encryption_by_default" "enabled" {
enabled = true
}
# 8. S3 public access block (account-level)
resource "aws_s3_account_public_access_block" "block" {
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
AspectAWSGCP
Managed solutionControl TowerNo equivalent (use CFT or Fabric)
Account factoryAFT (Account Factory for Terraform)Custom Terraform project factory
GuardrailsSCPs (deny-list, full IAM language)Org policies (boolean constraints + custom)
Account groupingOUs (flat, accounts can only be in one OU)Folders (nestable up to 10 levels)
Network isolationVPC per account (default)Shared VPC across projects (common)
Centralized loggingOrg CloudTrail to Log Archive accountOrg-level log sinks to logging project
Security postureSecurity Hub + GuardDuty (delegated admin)SCC (org-level) + Assured Workloads
ComplianceAWS Artifact, Config conformance packsAssured Workloads, compliance reports
Cost managementAWS Budgets, Cost Explorer (consolidated)Billing budgets per project, BigQuery export
Policy languageJSON (same as IAM), conditions, NotActionBoolean constraints + CEL for custom
Drift detectionConfig rules, Control Tower drift alertsCloud Asset Inventory, SCC findings
Break-glassIAM Identity Center emergency access + SCP exemptionEmergency access via dedicated SA + org policy exception

Account/Project Vending Machine — The Full Workflow

Section titled “Account/Project Vending Machine — The Full Workflow”

Regardless of cloud, the vending pattern is the same:

Account/Project vending workflow — request to ready

When vending accounts at scale, CIDR conflicts are a real risk. Use centralized IP Address Management:

# AWS VPC IPAM — central pool allocates CIDRs automatically
resource "aws_vpc_ipam" "main" {
operating_regions {
region_name = "ap-southeast-1"
}
operating_regions {
region_name = "me-south-1"
}
}
resource "aws_vpc_ipam_pool" "workloads" {
ipam_scope_id = aws_vpc_ipam.main.private_default_scope_id
address_family = "ipv4"
locale = "ap-southeast-1"
}
resource "aws_vpc_ipam_pool_cidr" "workloads" {
ipam_pool_id = aws_vpc_ipam_pool.workloads.id
cidr = "10.0.0.0/12" # 10.0.0.0 - 10.15.255.255
}
# In account factory baseline — VPC gets CIDR from IPAM pool
resource "aws_vpc" "main" {
ipv4_ipam_pool_id = data.aws_ssm_parameter.ipam_pool_id.value
ipv4_netmask_length = 22 # Each account gets a /22 (1024 IPs)
}

Scenario 1: Create 50 AWS Accounts for 10 Teams

Section titled “Scenario 1: Create 50 AWS Accounts for 10 Teams”

Q: “Create 50 AWS accounts for 10 teams across dev/staging/prod — walk through the architecture and automation.”

Model Answer:

OU Structure:

AWS OU structure for 50 accounts across 10 teams

Automation with AFT:

  1. AFT deployment: Deploy AFT in a dedicated AFT management account within the Infrastructure OU. Use AFT version 1.15.0+ with Terraform 1.6+.

  2. Account requests via Git: Each account is defined in a .tf file in the aft-account-request repo. For 50 accounts across 10 teams with 5 environments each:

# Generate all accounts programmatically
locals {
teams = ["alpha", "beta", "gamma", "delta", "epsilon",
"zeta", "eta", "theta", "iota", "kappa"]
environments = ["dev", "staging", "prod"]
accounts = flatten([
for team in local.teams : [
for env in local.environments : {
name = "team-${team}-${env}"
email = "aws+team-${team}-${env}@bank.com"
ou = "Workloads/${title(env)}"
team = team
env = env
}
]
])
}
  1. Global baseline (applied to every account automatically):

    • VPC with /22 CIDR from IPAM (no overlap possible)
    • Transit Gateway attachment to Network Hub
    • AWS Config recorder → central Log Archive bucket
    • GuardDuty member → Security Tooling Account
    • Security Hub member → Security Tooling Account
    • IAM roles: PlatformAdmin, DeveloperReadOnly, AppDeployer
    • EBS default encryption enabled
    • S3 public access block (account-level)
    • CloudWatch log group for application logs
  2. SCPs per OU:

    • Production OU: Deny region outside approved list, deny termination without tag, deny disabling security services, deny IAM user creation
    • Staging OU: Same as prod but allow wider instance types
    • Development OU: Relaxed — allow more services, still deny IAM users and public access
    • Sandbox OU: Most relaxed — auto-nuke resources after 7 days, $100/month budget alarm
  3. SSO (IAM Identity Center):

    • Federate from corporate Okta
    • Permission sets: PlatformAdmin, TeamDeveloper, ReadOnly, BreakGlass
    • Assign team-alpha-devs Okta group → TeamDeveloper on team-alpha-dev, team-alpha-staging
    • Assign team-alpha-devs Okta group → ReadOnly on team-alpha-prod (they can view but not change prod directly — CI/CD deploys)
  4. Timeline: Initial setup of Control Tower + AFT takes 2-3 days. Vending 50 accounts takes ~2 hours (AFT processes them sequentially, ~2-3 minutes per account). Global baseline applies automatically.


Scenario 2: Create 50 GCP Projects for 10 Teams

Section titled “Scenario 2: Create 50 GCP Projects for 10 Teams”

Q: “Do the same on GCP — create 50 projects for 10 teams.”

Model Answer:

Folder Structure:

GCP folder structure for 50 projects across 10 teams

Automation — Terraform Project Factory:

# Vend all 50 projects in a loop
locals {
teams = ["alpha", "beta", "gamma", "delta", "epsilon",
"zeta", "eta", "theta", "iota", "kappa"]
environments = {
dev = { folder_id = google_folder.development.id, budget = 1000 }
staging = { folder_id = google_folder.staging.id, budget = 3000 }
prod = { folder_id = google_folder.production.id, budget = 10000 }
}
}
module "team_projects" {
source = "./modules/project-factory"
for_each = { for item in flatten([
for team in local.teams : [
for env, config in local.environments : {
key = "team-${team}-${env}"
project_id = "bank-${team}-${env}"
project_name = "Team ${title(team)} ${title(env)}"
folder_id = config.folder_id
team_name = team
environment = env
budget = config.budget
}
]
]) : item.key => item }
project_name = each.value.project_name
project_id = each.value.project_id
folder_id = each.value.folder_id
org_id = var.org_id
billing_account_id = var.billing_account_id
team_name = each.value.team_name
environment = each.value.environment
shared_vpc_host_project = var.shared_vpc_host_project
logging_project = var.logging_project
monthly_budget_usd = each.value.budget
notification_channel_id = var.finops_channel
}

Key differences from AWS approach:

  1. Shared VPC instead of per-project VPCs: All projects are service projects attached to the network-hub host project. Subnets are centrally managed — teams cannot create their own networks.

  2. Org policies instead of SCPs: Applied at folder level, inherited by all projects. Production folder gets strict policies (no external IPs, no SA keys, restricted regions). Dev folder gets relaxed policies.

  3. Google Groups for IAM: Map team-alpha@bank.com group to roles/viewer on the production project and roles/editor on the dev project. No per-user IAM assignments.

  4. No “Account Factory” equivalent: GCP does not have a Control Tower equivalent. You build your own project factory with Terraform modules. The Google Cloud Foundation Fabric and CFT (Cloud Foundation Toolkit) provide reference modules.

  5. Billing budgets per project: Each project gets a billing budget with alerts at 50%, 80%, and 100% forecasted spend.


Scenario 3: Ensuring Security Baseline on Every New Account

Section titled “Scenario 3: Ensuring Security Baseline on Every New Account”

Q: “How do you ensure every new account/project gets the same security baseline automatically?”

Model Answer:

This is a pipeline problem. The key is: no human touches the new account/project directly — everything is automated.

Security baseline pipeline — request, validation, baseline, drift detection

Defense in depth — three layers:

  1. Preventive (before it happens):

    • SCPs/org policies PREVENT creating public resources, disabling security tools, using unapproved regions
    • Permission boundaries CAP what tenant roles can do
  2. Detective (catch violations):

    • AWS Config rules or GCP SCC findings detect non-compliant resources
    • Daily Terraform plan detects baseline drift
    • Cloud Asset Inventory queries find resources without required tags
  3. Corrective (auto-fix):

    • Config auto-remediation closes open security groups within 60 seconds
    • Lambda/Cloud Function removes public access from S3/GCS buckets
    • Auto-tagging function adds mandatory tags to untagged resources

Testing the baseline:

  • Every baseline change goes through the same PR → review → test pipeline
  • Test in a dedicated test account/project first
  • Use OPA/Conftest to policy-check the Terraform plan before apply
  • Monthly: run a “baseline compliance audit” across all accounts

Scenario 4: New Account Request — End-to-End Flow

Section titled “Scenario 4: New Account Request — End-to-End Flow”

Q: “A team requests a new AWS account. What happens from request to ready?”

Model Answer:

Here is the complete flow for Team Kappa requesting a production account:

Step 1 — Request (Day 0, 10 minutes)

Team lead submits a Jira ticket or opens a PR to the aft-account-request repo:

module "team_kappa_prod" {
source = "./modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "aws+team-kappa-prod@bank.com"
AccountName = "team-kappa-prod"
ManagedOrganizationalUnit = "Workloads/Production"
SSOUserEmail = "platform-admin@bank.com"
SSOUserFirstName = "Platform"
SSOUserLastName = "Admin"
}
account_tags = {
Team = "kappa"
Environment = "production"
CostCenter = "CC-5678"
DataClass = "confidential"
}
account_customizations_name = "standard-workload"
}

Step 2 — Approval (Day 0-1)

  • For non-prod: auto-approved if tags are valid and CIDR is available
  • For prod: platform team lead reviews and approves the PR
  • CI pipeline validates: naming convention, tag completeness, CIDR availability in IPAM, budget is set

Step 3 — Provisioning (Day 1, ~15 minutes automated)

AFT pipeline triggers on merge:

  1. CreateManagedAccount API → Control Tower creates account in the Production OU
  2. Global customizations run:
    • VPC created with /22 from IPAM pool (e.g., 10.3.4.0/22)
    • Transit Gateway attachment created → route propagation to hub
    • Route added: 0.0.0.0/0 → TGW (all traffic goes through Network Firewall in hub)
    • AWS Config recorder started → logs to central S3 in Log Archive
    • GuardDuty enabled → findings sent to Security Tooling admin
    • Security Hub enabled → findings aggregated
    • EBS default encryption → on
    • S3 public access block → on
    • IAM roles created: PlatformAdmin, TeamDeveloper, ReadOnly
  3. Account customizations run (for standard-workload):
    • EKS-ready VPC subnets tagged for ALB controller
    • ECR pull-through cache rule pointing to Shared Services ECR
    • Secrets Manager VPC endpoint created
  4. Provisioning customizations run:
    • IAM Identity Center assignment: team-kappa group → TeamDeveloper permission set
    • Route53 private hosted zone: kappa.internal.bank.com
    • ArgoCD ApplicationSet in Shared Services creates namespace team-kappa in the management cluster

Step 4 — Notification (Day 1, automated)

Slack message to #team-kappa:

New AWS account ready for Team Kappa (Production)
- Account ID: 444455556666
- Console: https://bank.awsapps.com/start
- VPC CIDR: 10.3.4.0/22
- Region: ap-southeast-1
- Guardrails: Production OU SCPs active (region lock, no public access, no IAM users)
- ArgoCD namespace: team-kappa (deploy via GitOps)
- Support: #platform-support

Step 5 — Day 2 Operations

  • Daily Terraform plan detects if anyone manually changed the baseline
  • Config rules evaluate all new resources against compliance standards
  • Billing budget alerts at 50%, 80%, 100% of forecast
  • GuardDuty and Security Hub findings auto-routed to the security team

Total time from request to ready: ~4-6 hours (mostly waiting for approval; provisioning itself takes ~15 minutes).