You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering AWS

Professional Mastery

Platform engineering, internal developer platforms, and architecture for different contexts

AWS Well-Architected Framework: Multi-Account Strategy

Master AWS multi-account architecture with free flashcards and spaced repetition practice. This lesson covers AWS Organizations, Control Tower implementation, and account isolation strategiesβ€”essential concepts for professional AWS architects designing enterprise-scale cloud infrastructures.

Welcome πŸ—οΈ

Welcome to the professional mastery level of AWS architecture! As organizations scale their cloud presence, managing resources within a single AWS account becomes increasingly complex and risky. Multi-account strategies represent one of the most critical architectural decisions for enterprise AWS deployments, directly impacting security posture, billing clarity, compliance boundaries, and operational efficiency.

In this lesson, you'll learn how to architect robust multi-account environments using AWS Organizations, implement governance at scale with AWS Control Tower, and design account structures that align with organizational boundaries. These skills distinguish senior architects from intermediate practitioners and are fundamental to achieving AWS certifications like Solutions Architect Professional and DevOps Engineer Professional.

Core Concepts πŸ’‘

Why Multi-Account Architecture?

Single-account limitations become apparent as organizations grow:

  • Security blast radius: A breach in one application can potentially compromise all workloads
  • Resource limits: Service quotas apply per account, creating scaling bottlenecks
  • Billing opacity: Cost allocation requires extensive tagging strategies
  • Compliance boundaries: Difficult to enforce regulatory isolation (PCI-DSS, HIPAA, SOC2)
  • Permission complexity: IAM policies become unwieldy with hundreds of principals
  • Environment separation: Production and development share the same security perimeter

Multi-account benefits address these challenges:

BenefitDescriptionExample
πŸ”’ Security isolationSeparate AWS accounts create hard security boundariesCompromised dev account cannot access production
πŸ’° Billing clarityCosts naturally segregated by accountEach team/project has clear AWS spending
πŸ“Š Resource organizationLogical separation of workloadsSeparate accounts per environment or business unit
βš–οΈ Compliance boundariesRegulated workloads isolated from general systemsPCI environment in dedicated accounts
🎯 Service limit multiplicationEach account has independent quotas10,000 Lambda concurrent executions per account

AWS Organizations: The Foundation πŸ›οΈ

AWS Organizations is the core service for managing multiple AWS accounts centrally. It provides:

Hierarchical account structure using Organizational Units (OUs):

Root Organization
    |
    β”œβ”€β”€ Security OU
    β”‚   β”œβ”€β”€ Log Archive Account
    β”‚   β”œβ”€β”€ Security Tooling Account
    β”‚   └── Audit Account
    |
    β”œβ”€β”€ Infrastructure OU
    β”‚   β”œβ”€β”€ Network Account
    β”‚   β”œβ”€β”€ Shared Services Account
    β”‚   └── DNS Account
    |
    β”œβ”€β”€ Workloads OU
    β”‚   β”œβ”€β”€ Production OU
    β”‚   β”‚   β”œβ”€β”€ App1-Prod Account
    β”‚   β”‚   └── App2-Prod Account
    β”‚   |
    β”‚   └── Non-Production OU
    β”‚       β”œβ”€β”€ App1-Dev Account
    β”‚       β”œβ”€β”€ App1-Test Account
    β”‚       └── Sandbox Accounts
    |
    └── Suspended OU
        └── Decommissioned accounts

Service Control Policies (SCPs) are the most powerful governance mechanism in AWS Organizations. They define maximum permissions that can be granted within accounts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "ec2:RunInstances"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "ec2:Region": [
            "us-east-1",
            "us-west-2"
          ]
        }
      }
    }
  ]
}

This SCP prevents EC2 instance launches in any region except us-east-1 and us-west-2, regardless of IAM permissions within the account.

Key SCP characteristics:

  • SCPs affect all principals in the account, including the root user
  • They filter permissions but never grant them (intersection with IAM policies)
  • Inherited down the OU hierarchy
  • Do not apply to the management account (security consideration)
  • Maximum size: 5,120 characters per policy

πŸ’‘ Pro tip: Always use explicit denies in SCPs rather than allow-based approaches. The deny-list model is more maintainable and secure as new services are automatically restricted.

Consolidated Billing and Cost Management πŸ’³

Consolidated billing aggregates usage across all member accounts:

FeatureBenefit
Volume discountsCombined usage reaches discount tiers faster (EC2 Reserved Instances, S3 tiered pricing)
Single payment methodOne credit card/invoice for entire organization
Reserved Instance sharingRIs purchased in any account apply to eligible usage organization-wide
Savings Plans sharingCompute Savings Plans discount shared across accounts
Cost allocation tagsStandardized tagging across accounts for chargeback

Example consolidated billing flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Management Account (Payer)             β”‚
β”‚  β€’ Receives consolidated bill                   β”‚
β”‚  β€’ Pays for all member accounts                 β”‚
β”‚  β€’ Cannot be restricted by SCPs                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚             β”‚             β”‚
    β–Ό             β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Account Aβ”‚  β”‚Account Bβ”‚  β”‚Account Cβ”‚
β”‚$1,200   β”‚  β”‚$3,400   β”‚  β”‚$800     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Total Bill: $5,400 (paid by Management Account)
Volume discounts applied to combined usage

AWS Control Tower: Automated Governance πŸŽ›οΈ

AWS Control Tower builds on Organizations to provide automated multi-account setup and governance:

Landing Zone: A pre-configured, secure multi-account environment:

ComponentPurpose
Management AccountOrganizations root, billing, Control Tower administration
Log Archive AccountCentralized repository for all audit logs (CloudTrail, Config, GuardDuty)
Audit AccountCross-account security tooling (Security Hub, GuardDuty master, Config aggregator)
Account FactoryAutomated provisioning of new accounts with baseline configuration

Guardrails are high-level governance rules implemented via SCPs and AWS Config:

Three types of guardrails:

  1. Mandatory: Automatically enforced on all accounts (cannot be disabled)

    • Example: "Disallow public write access to S3 buckets"
  2. Strongly Recommended: Best practices, enabled by default (can be disabled)

    • Example: "Enable MFA for root user"
  3. Elective: Optional controls for specific compliance needs

    • Example: "Detect whether public read access to S3 is enabled"

Guardrail implementation methods:

TypeMechanismActionExample
PreventiveSCPBlocks actionsDeny deletion of CloudTrail trails
DetectiveAWS Config RuleReports violationsAlert when RDS encryption is disabled

Account Factory workflow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. Request New Account                  β”‚
β”‚     β€’ Account name                       β”‚
β”‚     β€’ Email address                      β”‚
β”‚     β€’ OU placement                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  2. Control Tower Provisions             β”‚
β”‚     β€’ Creates account                    β”‚
β”‚     β€’ Applies baseline guardrails        β”‚
β”‚     β€’ Configures CloudTrail/Config       β”‚
β”‚     β€’ Sets up centralized logging        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  3. Custom Baselines Applied             β”‚
β”‚     β€’ Service Catalog portfolios         β”‚
β”‚     β€’ Standardized IAM roles             β”‚
β”‚     β€’ Network connectivity (Transit GW)  β”‚
β”‚     β€’ Security tooling agents            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  4. Account Ready                        β”‚
β”‚     β€’ Appears in target OU               β”‚
β”‚     β€’ Compliant with guardrails          β”‚
β”‚     β€’ Logged to central audit account    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
import boto3

service_catalog = boto3.client('servicecatalog')

## Provision new account via Account Factory product
response = service_catalog.provision_product(
    ProductId='prod-abcdefgh12345678',
    ProvisioningArtifactId='pa-xyz123456789',
    ProvisionedProductName='NewProductionAccount',
    ProvisioningParameters=[
        {'Key': 'AccountName', 'Value': 'Production-App-X'},
        {'Key': 'AccountEmail', 'Value': 'aws-prod-appx@company.com'},
        {'Key': 'ManagedOrganizationalUnit', 'Value': 'Production (ou-xyz-123)'},
        {'Key': 'SSOUserEmail', 'Value': 'admin@company.com'},
        {'Key': 'SSOUserFirstName', 'Value': 'Admin'},
        {'Key': 'SSOUserLastName', 'Value': 'User'}
    ]
)

print(f"Provisioning initiated: {response['RecordId']}")

Common Multi-Account Patterns πŸ—οΈ

Pattern 1: Environment-Based Segregation

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Production OU                          β”‚
β”‚  β€’ High availability requirements       β”‚
β”‚  β€’ Strict change control                β”‚
β”‚  β€’ Enhanced monitoring                  β”‚
β”‚  β€’ Multi-AZ mandated via SCP            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Non-Production OU                      β”‚
β”‚  β€’ Lower-cost instance types            β”‚
β”‚  β€’ Relaxed controls for experimentation β”‚
β”‚  β€’ Auto-shutdown during off-hours       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Sandbox OU                             β”‚
β”‚  β€’ Time-limited accounts (90 days)      β”‚
β”‚  β€’ Spending limits enforced             β”‚
β”‚  β€’ Isolated from corporate network      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pattern 2: Business Unit Segregation

Each business unit has complete autonomy within their OU:

{
  "Organization": {
    "Root": {
      "Marketing-BU-OU": {
        "accounts": ["Marketing-Prod", "Marketing-Dev"],
        "budget": "$50,000/month",
        "scp": "marketing-restrictions.json"
      },
      "Engineering-BU-OU": {
        "accounts": ["Eng-Prod-1", "Eng-Prod-2", "Eng-Shared-Services"],
        "budget": "$200,000/month",
        "scp": "engineering-restrictions.json"
      },
      "Finance-BU-OU": {
        "accounts": ["Finance-ERP", "Finance-Analytics"],
        "budget": "$30,000/month",
        "scp": "finance-compliance.json"
      }
    }
  }
}

Pattern 3: Application-Based Segregation

Each major application gets its own set of accounts:

ApplicationAccountsRationale
E-commerce Platformecommerce-prod, ecommerce-staging, ecommerce-devPCI-DSS isolation for payment processing
Data Analyticsanalytics-prod, analytics-sandboxLarge EMR clusters need dedicated quotas
Mobile Backendmobile-api-prod, mobile-api-testHigh-traffic API requires separate rate limits

Cross-Account Access Patterns πŸ”

Cross-account IAM roles are the recommended method for accessing resources across accounts:

import boto3

## In Account A (123456789012) - assume role in Account B
sts_client = boto3.client('sts')

assumed_role = sts_client.assume_role(
    RoleArn='arn:aws:iam::987654321098:role/CrossAccountS3Access',
    RoleSessionName='Account-A-Session',
    DurationSeconds=3600
)

credentials = assumed_role['Credentials']

## Use temporary credentials to access Account B resources
s3_client_account_b = boto3.client(
    's3',
    aws_access_key_id=credentials['AccessKeyId'],
    aws_secret_access_key=credentials['SecretAccessKey'],
    aws_session_token=credentials['SessionToken']
)

buckets = s3_client_account_b.list_buckets()
print(f"Buckets in Account B: {[b['Name'] for b in buckets['Buckets']]}")

Trust policy in Account B (987654321098):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-external-id-12345"
        },
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"
        }
      }
    }
  ]
}

Permission policy attached to the role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::account-b-data-bucket",
        "arn:aws:s3:::account-b-data-bucket/*"
      ]
    }
  ]
}

Resource Access Manager (RAM) 🀝

AWS RAM enables sharing of specific AWS resources across accounts without using cross-account roles:

Shareable resources include:

  • VPC subnets (for centralized networking)
  • Transit Gateway attachments
  • Route 53 Resolver rules
  • License Manager configurations
  • Aurora DB clusters
  • CodeBuild projects
import boto3

ram = boto3.client('ram')

## Share VPC subnet with multiple accounts
response = ram.create_resource_share(
    name='SharedSubnetForApplications',
    resourceArns=[
        'arn:aws:ec2:us-east-1:123456789012:subnet/subnet-abc123'
    ],
    principals=[
        '987654321098',  # Account ID
        '567890123456'   # Another account ID
    ],
    allowExternalPrincipals=False,
    tags=[
        {'key': 'Environment', 'value': 'Production'},
        {'key': 'Purpose', 'value': 'Centralized-Networking'}
    ]
)

print(f"Resource share created: {response['resourceShare']['resourceShareArn']}")

Network Account Pattern with RAM:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Network Account (Hub)                      β”‚
β”‚  β€’ VPC with centralized resources           β”‚
β”‚  β€’ Transit Gateway                          β”‚
β”‚  β€’ Route 53 Private Hosted Zones            β”‚
β”‚  β€’ Network Firewall                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ (RAM shares subnets)
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚         β”‚         β”‚
     β–Ό         β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚App      β”‚β”‚App      β”‚β”‚App      β”‚
β”‚Account Aβ”‚β”‚Account Bβ”‚β”‚Account Cβ”‚
β”‚         β”‚β”‚         β”‚β”‚         β”‚
β”‚EC2      β”‚β”‚Lambda   β”‚β”‚ECS      β”‚
β”‚instancesβ”‚β”‚functionsβ”‚β”‚tasks    β”‚
β”‚in sharedβ”‚β”‚in sharedβ”‚β”‚in sharedβ”‚
β”‚subnet   β”‚β”‚subnet   β”‚β”‚subnet   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Examples πŸ”

Example 1: Implementing Region Restrictions with SCPs

Scenario: Your compliance team requires all resources to remain in US regions only. Implement this at the organization level.

Solution:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonUSRegions",
      "Effect": "Deny",
      "NotAction": [
        "cloudfront:*",
        "iam:*",
        "route53:*",
        "support:*",
        "budgets:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "us-east-1",
            "us-east-2",
            "us-west-1",
            "us-west-2"
          ]
        }
      }
    }
  ]
}

Key points:

  • Uses NotAction to exempt global services (CloudFront, IAM, Route53) that must operate in specific regions
  • aws:RequestedRegion condition key checks where API calls are directed
  • This SCP prevents resource creation but doesn't delete existing resources in other regions
  • Apply to root OU to affect all accounts (except management account)

Validation test:

import boto3
from botocore.exceptions import ClientError

## Attempt to launch EC2 in prohibited region
try:
    ec2_eu = boto3.client('ec2', region_name='eu-west-1')
    response = ec2_eu.run_instances(
        ImageId='ami-0abcdef1234567890',
        InstanceType='t3.micro',
        MinCount=1,
        MaxCount=1
    )
    print("ERROR: SCP not working, instance launched!")
except ClientError as e:
    if e.response['Error']['Code'] == 'UnauthorizedOperation':
        print("βœ… SCP working: Region restriction enforced")
    else:
        print(f"Unexpected error: {e}")

Example 2: Centralized Logging Architecture

Scenario: Implement centralized CloudTrail and AWS Config logging for audit and compliance.

Architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Log Archive Account                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  S3 Bucket: org-audit-logs        β”‚     β”‚
β”‚  β”‚  β€’ Versioning enabled             β”‚     β”‚
β”‚  β”‚  β€’ MFA Delete enabled             β”‚     β”‚
β”‚  β”‚  β€’ Encryption: KMS                β”‚     β”‚
β”‚  β”‚  β€’ Lifecycle: Glacier after 90d   β”‚     β”‚
β”‚  β”‚  β€’ Object Lock: Compliance mode   β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ (logs delivered here)
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚         β”‚         β”‚         β”‚
     β–Ό         β–Ό         β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Mgmt    β”‚β”‚ App     β”‚β”‚ App     β”‚β”‚ Network β”‚
β”‚ Account β”‚β”‚Account 1β”‚β”‚Account 2β”‚β”‚ Account β”‚
β”‚         β”‚β”‚         β”‚β”‚         β”‚β”‚         β”‚
β”‚CloudTrl β”‚β”‚CloudTrl β”‚β”‚CloudTrl β”‚β”‚CloudTrl β”‚
β”‚Config   β”‚β”‚Config   β”‚β”‚Config   β”‚β”‚Config   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation:

import boto3
import json

## In Log Archive Account - create centralized bucket
s3 = boto3.client('s3')
kms = boto3.client('kms')

bucket_name = 'org-audit-logs-123456789012'
org_id = 'o-abc1234567'

## Create bucket
s3.create_bucket(Bucket=bucket_name)

## Enable versioning
s3.put_bucket_versioning(
    Bucket=bucket_name,
    VersioningConfiguration={'Status': 'Enabled'}
)

## Bucket policy allowing CloudTrail from all org accounts
bucket_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSCloudTrailAclCheck",
            "Effect": "Allow",
            "Principal": {"Service": "cloudtrail.amazonaws.com"},
            "Action": "s3:GetBucketAcl",
            "Resource": f"arn:aws:s3:::{bucket_name}"
        },
        {
            "Sid": "AWSCloudTrailWrite",
            "Effect": "Allow",
            "Principal": {"Service": "cloudtrail.amazonaws.com"},
            "Action": "s3:PutObject",
            "Resource": f"arn:aws:s3:::{bucket_name}/AWSLogs/{org_id}/*",
            "Condition": {
                "StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}
            }
        },
        {
            "Sid": "AWSConfigAclCheck",
            "Effect": "Allow",
            "Principal": {"Service": "config.amazonaws.com"},
            "Action": "s3:GetBucketAcl",
            "Resource": f"arn:aws:s3:::{bucket_name}"
        },
        {
            "Sid": "AWSConfigWrite",
            "Effect": "Allow",
            "Principal": {"Service": "config.amazonaws.com"},
            "Action": "s3:PutObject",
            "Resource": f"arn:aws:s3:::{bucket_name}/AWSLogs/{org_id}/*",
            "Condition": {
                "StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}
            }
        }
    ]
}

s3.put_bucket_policy(
    Bucket=bucket_name,
    Policy=json.dumps(bucket_policy)
)

print(f"βœ… Centralized logging bucket configured: {bucket_name}")

In each member account:

## Enable CloudTrail pointing to centralized bucket
cloudtrail = boto3.client('cloudtrail')

response = cloudtrail.create_trail(
    Name='OrganizationTrail',
    S3BucketName='org-audit-logs-123456789012',
    IsMultiRegionTrail=True,
    EnableLogFileValidation=True,
    KmsKeyId='arn:aws:kms:us-east-1:999999999999:key/12345678-1234-1234-1234-123456789012'
)

cloudtrail.start_logging(Name='OrganizationTrail')
print("βœ… CloudTrail enabled and logging to central account")

Example 3: Account Factory Customization

Scenario: Customize Account Factory to automatically configure new accounts with company standards.

Solution using AWS Service Catalog and Lambda:

import boto3
import json

def lambda_handler(event, context):
    """
    Triggered by Account Factory provisioning.
    Applies baseline configuration to new accounts.
    """
    
    # Extract new account details
    account_id = event['detail']['responseElements']['createAccountStatus']['accountId']
    account_name = event['detail']['responseElements']['createAccountStatus']['accountName']
    
    print(f"Configuring new account: {account_name} ({account_id})")
    
    # Assume role in new account
    sts = boto3.client('sts')
    assumed_role = sts.assume_role(
        RoleArn=f'arn:aws:iam::{account_id}:role/AWSControlTowerExecution',
        RoleSessionName='AccountBaselineConfig'
    )
    
    credentials = assumed_role['Credentials']
    
    # Create clients with assumed credentials
    session = boto3.Session(
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )
    
    # 1. Enable AWS Security Hub
    securityhub = session.client('securityhub', region_name='us-east-1')
    try:
        securityhub.enable_security_hub(
            EnableDefaultStandards=True
        )
        print("βœ… Security Hub enabled")
    except securityhub.exceptions.ResourceConflictException:
        print("Security Hub already enabled")
    
    # 2. Enable GuardDuty
    guardduty = session.client('guardduty', region_name='us-east-1')
    detector = guardduty.create_detector(
        Enable=True,
        FindingPublishingFrequency='FIFTEEN_MINUTES'
    )
    print(f"βœ… GuardDuty enabled: {detector['DetectorId']}")
    
    # 3. Create standard IAM roles
    iam = session.client('iam')
    
    # Developer role
    developer_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:*",
                    "s3:*",
                    "lambda:*",
                    "dynamodb:*",
                    "cloudformation:*"
                ],
                "Resource": "*",
                "Condition": {
                    "StringEquals": {
                        "aws:RequestedRegion": ["us-east-1", "us-west-2"]
                    }
                }
            },
            {
                "Effect": "Deny",
                "Action": [
                    "iam:*",
                    "organizations:*",
                    "account:*"
                ],
                "Resource": "*"
            }
        ]
    }
    
    iam.create_role(
        RoleName='DeveloperRole',
        AssumeRolePolicyDocument=json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Allow",
                "Principal": {"AWS": f"arn:aws:iam::{account_id}:root"},
                "Action": "sts:AssumeRole"
            }]
        }),
        Description='Standard developer access role'
    )
    
    iam.put_role_policy(
        RoleName='DeveloperRole',
        PolicyName='DeveloperPermissions',
        PolicyDocument=json.dumps(developer_policy)
    )
    print("βœ… Developer role created")
    
    # 4. Set up budget alerts
    budgets = session.client('budgets', region_name='us-east-1')
    
    budgets.create_budget(
        AccountId=account_id,
        Budget={
            'BudgetName': 'MonthlySpendingLimit',
            'BudgetLimit': {'Amount': '5000', 'Unit': 'USD'},
            'TimeUnit': 'MONTHLY',
            'BudgetType': 'COST'
        },
        NotificationsWithSubscribers=[
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 80,
                    'ThresholdType': 'PERCENTAGE'
                },
                'Subscribers': [{
                    'SubscriptionType': 'EMAIL',
                    'Address': 'finance@company.com'
                }]
            }
        ]
    )
    print("βœ… Budget alerts configured")
    
    return {
        'statusCode': 200,
        'body': json.dumps(f'Account {account_id} baseline applied')
    }

Example 4: Cross-Account CI/CD Pipeline

Scenario: Deploy applications from a central DevOps account to multiple environment accounts.

Architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  DevOps Account (123456789012)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  CodePipeline: AppX-Pipeline      β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     β”‚
β”‚  β”‚  β”‚ 1. Source (CodeCommit)      β”‚  β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     β”‚
β”‚  β”‚             β”‚                     β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     β”‚
β”‚  β”‚  β”‚ 2. Build (CodeBuild)        β”‚  β”‚     β”‚
β”‚  β”‚  β”‚    β€’ Run tests              β”‚  β”‚     β”‚
β”‚  β”‚  β”‚    β€’ Create artifacts       β”‚  β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     β”‚
β”‚  β”‚             β”‚                     β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     β”‚
β”‚  β”‚  β”‚ 3. Deploy to Dev            β”‚  β”‚     β”‚
β”‚  β”‚  β”‚    (Assume role in Dev)     β”‚  β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     β”‚
β”‚  β”‚             β”‚                     β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     β”‚
β”‚  β”‚  β”‚ 4. Manual Approval          β”‚  β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     β”‚
β”‚  β”‚             β”‚                     β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚     β”‚
β”‚  β”‚  β”‚ 5. Deploy to Prod           β”‚  β”‚     β”‚
β”‚  β”‚  β”‚    (Assume role in Prod)    β”‚  β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚                  β”‚
            β–Ό                  β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Dev Account  β”‚   β”‚ Prod Account β”‚
    β”‚ (9876543...)β”‚   β”‚ (5678901...) β”‚
    β”‚              β”‚   β”‚              β”‚
    β”‚ IAM Role:    β”‚   β”‚ IAM Role:    β”‚
    β”‚ CodePipeline β”‚   β”‚ CodePipeline β”‚
    β”‚ Deployer     β”‚   β”‚ Deployer     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Trust policy in target accounts (Dev/Prod):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "pipeline-deployment-2024"
        }
      }
    }
  ]
}

CodePipeline deployment action:

import boto3

codepipeline = boto3.client('codepipeline')

pipeline_definition = {
    'name': 'CrossAccountDeployment',
    'roleArn': 'arn:aws:iam::123456789012:role/CodePipelineServiceRole',
    'artifactStore': {
        'type': 'S3',
        'location': 'codepipeline-artifacts-123456789012'
    },
    'stages': [
        {
            'name': 'Source',
            'actions': [{
                'name': 'SourceAction',
                'actionTypeId': {
                    'category': 'Source',
                    'owner': 'AWS',
                    'provider': 'CodeCommit',
                    'version': '1'
                },
                'configuration': {
                    'RepositoryName': 'my-application',
                    'BranchName': 'main'
                },
                'outputArtifacts': [{'name': 'SourceOutput'}]
            }]
        },
        {
            'name': 'DeployToDev',
            'actions': [{
                'name': 'DeployAction',
                'actionTypeId': {
                    'category': 'Deploy',
                    'owner': 'AWS',
                    'provider': 'CloudFormation',
                    'version': '1'
                },
                'configuration': {
                    'ActionMode': 'CREATE_UPDATE',
                    'StackName': 'application-stack',
                    'TemplatePath': 'SourceOutput::template.yaml',
                    'RoleArn': 'arn:aws:iam::987654321098:role/CloudFormationDeploymentRole'
                },
                'inputArtifacts': [{'name': 'SourceOutput'}],
                'roleArn': 'arn:aws:iam::987654321098:role/CodePipelineDeployerRole'
            }]
        },
        {
            'name': 'Approval',
            'actions': [{
                'name': 'ManualApproval',
                'actionTypeId': {
                    'category': 'Approval',
                    'owner': 'AWS',
                    'provider': 'Manual',
                    'version': '1'
                },
                'configuration': {
                    'CustomData': 'Please review deployment in Dev before promoting to Production'
                }
            }]
        },
        {
            'name': 'DeployToProd',
            'actions': [{
                'name': 'DeployAction',
                'actionTypeId': {
                    'category': 'Deploy',
                    'owner': 'AWS',
                    'provider': 'CloudFormation',
                    'version': '1'
                },
                'configuration': {
                    'ActionMode': 'CREATE_UPDATE',
                    'StackName': 'application-stack',
                    'TemplatePath': 'SourceOutput::template.yaml',
                    'RoleArn': 'arn:aws:iam::567890123456:role/CloudFormationDeploymentRole'
                },
                'inputArtifacts': [{'name': 'SourceOutput'}],
                'roleArn': 'arn:aws:iam::567890123456:role/CodePipelineDeployerRole'
            }]
        }
    ]
}

codepipeline.create_pipeline(pipeline=pipeline_definition)
print("βœ… Cross-account pipeline created")

Common Mistakes ⚠️

Mistake 1: Using the Management Account for Workloads

❌ Wrong approach:

## Deploying production application in management account
ec2 = boto3.client('ec2')  # In management account (111111111111)
ec2.run_instances(
    ImageId='ami-abc123',
    InstanceType='t3.large',
    MinCount=10,
    MaxCount=10
)

Why it's wrong:

  • Management account cannot be restricted by SCPs
  • Compromised workload = entire organization at risk
  • Billing becomes complicated with mixed infrastructure and organizational charges
  • Limits ability to enforce least privilege

βœ… Correct approach:

  • Never deploy workloads in the management account
  • Use management account only for:
    • AWS Organizations administration
    • Consolidated billing
    • Organization-wide CloudTrail
    • Control Tower setup
  • Deploy all workloads in dedicated member accounts

Mistake 2: Overly Permissive SCPs

❌ Wrong approach:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}

Why it's wrong:

  • Defeats the entire purpose of SCPs
  • No guardrails against accidental or malicious actions
  • Compliance violations possible

βœ… Correct approach:

  • Use deny-list strategy (implicit allow + explicit denies)
  • Start with AWS Organizations default FullAWSAccess SCP
  • Add specific deny statements for prohibited actions:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeavingOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    },
    {
      "Sid": "DenyRootAccountUsage",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "DenyDisablingSecurityServices",
      "Effect": "Deny",
      "Action": [
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromMasterAccount",
        "securityhub:DisableSecurityHub",
        "config:DeleteConfigurationRecorder",
        "config:DeleteDeliveryChannel",
        "config:StopConfigurationRecorder"
      ],
      "Resource": "*"
    }
  ]
}

Mistake 3: Hardcoding Account IDs

❌ Wrong approach:

## Hardcoded account IDs in application code
if account_id == '123456789012':
    bucket_name = 'prod-data-bucket'
elif account_id == '987654321098':
    bucket_name = 'dev-data-bucket'
else:
    raise ValueError("Unknown account")

Why it's wrong:

  • Brittle code that breaks when accounts change
  • Difficult to maintain across many services
  • Security risk (account structure exposed in code)

βœ… Correct approach:

  • Use AWS Organizations tags on accounts:
import boto3

sts = boto3.client('sts')
organizations = boto3.client('organizations')

## Get current account
account_id = sts.get_caller_identity()['Account']

## Look up account metadata
account = organizations.describe_account(AccountId=account_id)['Account']
tags = organizations.list_tags_for_resource(
    ResourceId=account_id
)['Tags']

## Use tags to determine configuration
environment = next(tag['Value'] for tag in tags if tag['Key'] == 'Environment')
bucket_name = f"{environment}-data-bucket"
print(f"Using bucket: {bucket_name}")

Mistake 4: Not Planning for Account Limits

❌ Wrong approach:

  • Creating accounts without considering organizational limits
  • Default limit: 10 accounts (can be increased to thousands)

βœ… Correct approach:

  • Request limit increases before you need them:
import boto3

support = boto3.client('support', region_name='us-east-1')

case = support.create_case(
    subject='Request to increase AWS Organizations account limit',
    serviceCode='organizations',
    categoryCode='general-guidance',
    severityCode='low',
    communicationBody='''
    We are requesting an increase to our AWS Organizations account limit.
    
    Current limit: 50 accounts
    Requested limit: 200 accounts
    
    Justification:
    - Multi-account strategy for security isolation
    - Separate accounts per business unit and environment
    - Planned growth over next 12 months
    
    Use case: Enterprise multi-account architecture
    ''',
    ccEmailAddresses=['cloud-team@company.com'],
    language='en'
)

print(f"Support case created: {case['caseId']}")

Mistake 5: Circular Trust Relationships

❌ Wrong approach:

## In Account A - trusts Account B
{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::AccountB:root"},
  "Action": "sts:AssumeRole"
}

## In Account B - trusts Account A
{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::AccountA:root"},
  "Action": "sts:AssumeRole"
}

Why it's wrong:

  • Creates security confusion
  • Difficult to audit
  • Can lead to privilege escalation

βœ… Correct approach:

  • Establish unidirectional trust with clear purpose
  • Use a hub-and-spoke model:
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  Security Hub   β”‚
        β”‚    Account      β”‚
        β”‚  (Read-only     β”‚
        β”‚   access to     β”‚
        β”‚   all accounts) β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚        β”‚        β”‚
        β–Ό        β–Ό        β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”
    β”‚Accountβ”‚β”‚Accountβ”‚β”‚Accountβ”‚
    β”‚   A   β”‚β”‚   B   β”‚β”‚   C   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”˜
    
    (Accounts trust Security Hub,
     but not each other)

Key Takeaways 🎯

πŸ“‹ Quick Reference Card

ConceptKey Points
AWS Organizationsβ€’ Central management of multiple accounts
β€’ Hierarchical OU structure
β€’ SCPs define maximum permissions
β€’ Consolidated billing with volume discounts
Service Control Policiesβ€’ Filter permissions, never grant them
β€’ Use deny-list strategy (explicit denies)
β€’ Don't affect management account
β€’ Inherited down OU tree
β€’ Max 5,120 characters per policy
AWS Control Towerβ€’ Automated landing zone setup
β€’ Guardrails: mandatory, strongly recommended, elective
β€’ Account Factory for standardized provisioning
β€’ Centralized logging to Log Archive account
β€’ Detective (Config) and preventive (SCP) controls
Multi-Account Patternsβ€’ Environment-based: Dev/Test/Prod accounts
β€’ Business unit: One OU per department
β€’ Application-based: Separate accounts per app
β€’ Hub-and-spoke: Centralized networking/security
Cross-Account Accessβ€’ IAM roles with AssumeRole (preferred method)
β€’ AWS RAM for resource sharing
β€’ External ID for third-party security
β€’ Unidirectional trusts for clarity
Security Best Practicesβ€’ Never use management account for workloads
β€’ Enable CloudTrail in all accounts
β€’ Centralize logs in dedicated account
β€’ Tag accounts for configuration management
β€’ Automate baseline with Account Factory

Architecture decision checklist:

βœ… Have you planned your OU structure to reflect organizational boundaries?
βœ… Are SCPs enforcing company-wide security policies (region restrictions, required encryption)?
βœ… Is the management account protected and used only for administration?
βœ… Are logs centralized in a separate, locked-down account?
βœ… Have you automated account provisioning with baseline security?
βœ… Is cross-account access implemented with least-privilege IAM roles?
βœ… Are accounts tagged for dynamic configuration lookup?
βœ… Have you requested limit increases for anticipated growth?

Cost optimization considerations:

  • Reserved Instances and Savings Plans share across organization
  • Use consolidated billing volume discounts
  • Implement budget alerts per account
  • Tag resources consistently for cost allocation
  • Consider separate billing account for financial isolation

Compliance mapping:

FrameworkMulti-Account Support
PCI-DSSDedicated account for cardholder data environment (CDE)
HIPAAPHI workloads isolated in BAA-covered accounts
SOC 2Production environment boundary enforced at account level
FedRAMPAuthorized boundary = specific account(s)
GDPREU data residency enforced via SCPs on regional OUs

πŸ’‘ Final thought: Multi-account architecture is not just a technical decisionβ€”it's a strategic organizational capability that enables teams to move faster while maintaining security, compliance, and cost control. Start simple with a few accounts, but design the OU structure to accommodate future growth.

Further Study πŸ“š