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:
| Benefit | Description | Example |
|---|---|---|
| π Security isolation | Separate AWS accounts create hard security boundaries | Compromised dev account cannot access production |
| π° Billing clarity | Costs naturally segregated by account | Each team/project has clear AWS spending |
| π Resource organization | Logical separation of workloads | Separate accounts per environment or business unit |
| βοΈ Compliance boundaries | Regulated workloads isolated from general systems | PCI environment in dedicated accounts |
| π― Service limit multiplication | Each account has independent quotas | 10,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:
| Feature | Benefit |
|---|---|
| Volume discounts | Combined usage reaches discount tiers faster (EC2 Reserved Instances, S3 tiered pricing) |
| Single payment method | One credit card/invoice for entire organization |
| Reserved Instance sharing | RIs purchased in any account apply to eligible usage organization-wide |
| Savings Plans sharing | Compute Savings Plans discount shared across accounts |
| Cost allocation tags | Standardized 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:
| Component | Purpose |
|---|---|
| Management Account | Organizations root, billing, Control Tower administration |
| Log Archive Account | Centralized repository for all audit logs (CloudTrail, Config, GuardDuty) |
| Audit Account | Cross-account security tooling (Security Hub, GuardDuty master, Config aggregator) |
| Account Factory | Automated provisioning of new accounts with baseline configuration |
Guardrails are high-level governance rules implemented via SCPs and AWS Config:
Three types of guardrails:
Mandatory: Automatically enforced on all accounts (cannot be disabled)
- Example: "Disallow public write access to S3 buckets"
Strongly Recommended: Best practices, enabled by default (can be disabled)
- Example: "Enable MFA for root user"
Elective: Optional controls for specific compliance needs
- Example: "Detect whether public read access to S3 is enabled"
Guardrail implementation methods:
| Type | Mechanism | Action | Example |
|---|---|---|---|
| Preventive | SCP | Blocks actions | Deny deletion of CloudTrail trails |
| Detective | AWS Config Rule | Reports violations | Alert 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:
| Application | Accounts | Rationale |
|---|---|---|
| E-commerce Platform | ecommerce-prod, ecommerce-staging, ecommerce-dev | PCI-DSS isolation for payment processing |
| Data Analytics | analytics-prod, analytics-sandbox | Large EMR clusters need dedicated quotas |
| Mobile Backend | mobile-api-prod, mobile-api-test | High-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
NotActionto exempt global services (CloudFront, IAM, Route53) that must operate in specific regions aws:RequestedRegioncondition 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
FullAWSAccessSCP - 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
| Concept | Key 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:
| Framework | Multi-Account Support |
|---|---|
| PCI-DSS | Dedicated account for cardholder data environment (CDE) |
| HIPAA | PHI workloads isolated in BAA-covered accounts |
| SOC 2 | Production environment boundary enforced at account level |
| FedRAMP | Authorized boundary = specific account(s) |
| GDPR | EU 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.