Multi-Account AWS Strategy with AWS Organizations
Building a secure, scalable, and governable AWS foundation through strategic multi-account architecture
Introduction
As organizations grow their AWS footprint, managing resources in a single AWS account quickly becomes unmanageable and risky. A well-designed multi-account strategy using AWS Organizations is fundamental to achieving security, compliance, and operational excellence at scale.
This comprehensive guide walks you through designing and implementing a robust multi-account architecture, covering organizational unit (OU) design, Service Control Policies (SCPs), cross-account access patterns, and Infrastructure as Code (IaC) automation using AWS CDK and Terraform.
In this guide, you’ll learn:
- Why multi-account strategies are essential for enterprise AWS deployments
- How to design an effective organizational unit (OU) hierarchy
- Implementing guardrails using Service Control Policies (SCPs)
- Automating account provisioning with AWS Control Tower and IaC
- Best practices for cross-account access and resource sharing
- Real-world implementation patterns with code examples
Why Multi-Account Architecture Matters
The Single Account Anti-Pattern
Running all workloads in a single AWS account creates significant challenges:
Security Risks:
- Blast radius: A security incident affects all resources
- Difficult to implement least privilege across diverse workloads
- Challenging to isolate sensitive data and compliance boundaries
- No clear separation between environments (dev, test, prod)
Operational Complexity:
- Resource limits apply at account level (VPCs, Elastic IPs, etc.)
- Difficult to track costs per business unit or project
- Naming conflicts and resource organization challenges
- Complex IAM policies trying to segregate access
Compliance Issues:
- Hard to demonstrate clear data boundaries for auditors
- Difficult to apply different compliance controls per workload
- Challenging to maintain audit trails per business unit
Benefits of Multi-Account Strategy
1. Security Isolation
- Each account provides a hard security boundary
- Contain blast radius of security incidents
- Easier implementation of least privilege
- Clear separation of duties
2. Billing and Cost Management
- Clear cost attribution per account/business unit
- Separate budgets and cost centers
- Better chargeback mechanisms
- Easier to identify cost optimization opportunities
3. Compliance and Governance
- Isolated compliance boundaries per workload
- Different security controls per account type
- Clear audit trails per business unit
- Easier to meet regulatory requirements
4. Operational Benefits
- Avoid service limits in single account
- Better resource organization
- Independent deployment cycles
- Reduced change risk across environments
AWS Organizations Core Concepts
Organizational Units (OUs)
Organizational Units are logical groupings of AWS accounts that share common governance requirements. OUs enable hierarchical policy application and account organization.
Example OU Hierarchy:
Root
├── Security OU
│ ├── Log Archive Account
│ ├── Security Audit Account
│ └── Security Tooling Account
├── Infrastructure OU
│ ├── Network Account
│ ├── Shared Services Account
│ └── DNS Account
├── Workloads OU
│ ├── Production OU
│ │ ├── Prod-App-A Account
│ │ └── Prod-App-B Account
│ ├── Staging OU
│ │ └── Staging-App Accounts
│ └── Development OU
│ └── Dev-App Accounts
├── Sandbox OU
│ └── Individual Developer Accounts
└── Suspended OU
└── Decommissioned Accounts
Service Control Policies (SCPs)
SCPs are a type of organization policy that you use to manage permissions across your organization. SCPs offer central control over the maximum available permissions for all accounts.
Key Characteristics:
- SCPs don’t grant permissions; they set permission guardrails
- They define the maximum permissions for accounts
- SCPs affect all users and roles in attached accounts, including root
- Never affect the management account (formerly master account)
Account Structure Patterns
By Environment:
Production OU → Staging OU → Development OU → Sandbox OU
Best for: Organizations with clear environment separation needs
By Business Unit:
Sales OU → Marketing OU → Engineering OU → Finance OU
Best for: Large enterprises with independent business units
By Compliance Boundary:
PCI-DSS OU → HIPAA OU → General OU
Best for: Organizations with different compliance requirements
Hybrid Approach (Recommended):
Root
├── Security OU (function-based)
├── Infrastructure OU (function-based)
├── Production OU
│ ├── Finance Workloads (business-unit)
│ └── Customer-Facing Apps (business-unit)
└── Non-Production OU
├── Development (environment)
└── Staging (environment)
Designing Your OU Structure
Best Practices for OU Design
1. Keep It Simple
- Start with 3-5 top-level OUs
- Avoid deep nesting (3 levels maximum recommended)
- Easy to understand and explain to stakeholders
- Can evolve over time as needs change
2. Plan for Scale
Current: 10 accounts → Future: 100+ accounts
Design your structure to accommodate growth without major reorganization.
3. Align with Business Structure
- Mirror organizational reporting lines where applicable
- Consider cost center alignment
- Enable clear ownership and accountability
4. Security-First Approach
- Separate security and infrastructure accounts
- Isolate sensitive workloads
- Clear production vs. non-production boundaries
Example OU Structure for Mid-Sized Company
# OU Design: Mid-Sized SaaS Company
Root:
Security_OU:
purpose: "Centralized security and audit functions"
accounts:
- Log-Archive (all CloudTrail, Config, Flow Logs)
- Security-Audit (read-only access for auditing)
- Security-Tooling (GuardDuty, Security Hub, etc.)
scps:
- deny-region-outside-eu
- require-encryption
Infrastructure_OU:
purpose: "Shared infrastructure services"
accounts:
- Network (Transit Gateway, VPN, Direct Connect)
- Shared-Services (CI/CD, Artifact Registry)
- DNS (Route53 hosted zones)
scps:
- deny-region-outside-eu
- restrict-instance-types
Production_OU:
purpose: "Production workloads"
accounts:
- Prod-API-Backend
- Prod-Frontend
- Prod-Data-Platform
scps:
- deny-region-outside-eu
- require-encryption
- require-mfa
- deny-root-user
- require-backup-tags
Non_Production_OU:
purpose: "Development and testing"
staging_ou:
accounts:
- Staging-Full-Stack
development_ou:
accounts:
- Dev-Engineering
scps:
- deny-region-outside-eu
- cost-control
Sandbox_OU:
purpose: "Individual experimentation"
accounts:
- Sandbox-Engineer-1
- Sandbox-Engineer-2
scps:
- cost-limits-strict
- auto-terminate-resources
Service Control Policies (SCPs)
SCP Evaluation Logic
Effective Permissions = Identity Policy ∩ SCP ∩ Resource Policy
Even if an IAM policy grants access, an SCP can deny it. SCPs act as guardrails.
Essential SCPs for Enterprise
1. Deny Operations Outside Approved Regions
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllOutsideEU",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"eu-west-1",
"eu-central-1"
]
},
"ArnNotLike": {
"aws:PrincipalARN": [
"arn:aws:iam::*:role/AllowGlobalServices"
]
}
}
}
]
}
Why this matters: Data residency compliance (GDPR, local regulations)
2. Require Encryption for S3 and EBS
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedS3Uploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": [
"AES256",
"aws:kms"
]
}
}
},
{
"Sid": "DenyUnencryptedEBS",
"Effect": "Deny",
"Action": [
"ec2:RunInstances",
"ec2:CreateVolume"
],
"Resource": "*",
"Condition": {
"Bool": {
"ec2:Encrypted": "false"
}
}
}
]
}
Why this matters: Security baseline, compliance requirements
3. Require MFA for Sensitive Operations
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyActionsWithoutMFA",
"Effect": "Deny",
"Action": [
"ec2:StopInstances",
"ec2:TerminateInstances",
"rds:DeleteDBInstance",
"s3:DeleteBucket"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Why this matters: Prevent accidental deletions, add security layer
4. Prevent Root User Usage
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootUser",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
}
]
}
Why this matters: Root user should never be used for daily operations
5. Cost Control for Sandbox Accounts
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyExpensiveInstanceTypes",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotLike": {
"ec2:InstanceType": [
"t3.*",
"t3a.*",
"t4g.*"
]
}
}
},
{
"Sid": "DenyExpensiveRDS",
"Effect": "Deny",
"Action": "rds:CreateDBInstance",
"Resource": "*",
"Condition": {
"StringNotLike": {
"rds:DatabaseClass": "db.t3.*"
}
}
}
]
}
Why this matters: Prevent runaway costs in experimental accounts
Implementing with AWS Control Tower
What is AWS Control Tower?
AWS Control Tower automates the setup of a well-architected multi-account environment based on AWS best practices. It builds on AWS Organizations, adding:
- Landing Zone: Automated baseline environment
- Guardrails: Pre-configured SCPs and Config rules
- Account Factory: Self-service account provisioning
- Dashboard: Centralized compliance view
Control Tower Setup
Prerequisites:
- New or existing AWS Organization
- Management account with appropriate permissions
- Email addresses for Log Archive and Audit accounts
Deployment Steps:
# 1. Ensure you're in the management account
aws sts get-caller-identity
# 2. Deploy Control Tower via AWS Console
# Navigate to: AWS Control Tower → Set up landing zone
# 3. Configure regions (select your allowed regions)
# Home Region: eu-west-1
# Additional Regions: eu-central-1
# 4. Configure foundational OU structure
# Default OUs created:
# - Security (Log Archive + Audit)
# - Sandbox
# 5. Enable guardrails
Control Tower Guardrails
Mandatory Guardrails (Always Enforced):
- Disallow public write access to S3 buckets
- Disallow public read access to S3 buckets
- Enable MFA for root user
- Disallow changes to encryption configuration for S3 buckets
Strongly Recommended Guardrails:
- Enable encryption for EBS volumes
- Enable encryption at rest for RDS instances
- Detect whether MFA is enabled for IAM users
- Detect whether public routes exist in route tables
Elective Guardrails:
- Disallow internet connection through RDP
- Disallow VPCs with internet gateways in specific OUs
- Disallow specific instance types
Account Factory Automation
Account Factory enables self-service account creation with standardized baselines.
Customize Account Factory with Service Catalog:
# lambda/account-vending.py
import boto3
import json
def lambda_handler(event, context):
"""
Custom account vending machine
Triggered after Account Factory creates new account
"""
account_id = event['account_id']
account_email = event['account_email']
ou_name = event['organizational_unit']
# Initialize clients
organizations = boto3.client('organizations')
# Apply tags
organizations.tag_resource(
ResourceId=account_id,
Tags=[
{'Key': 'ManagedBy', 'Value': 'AccountFactory'},
{'Key': 'Environment', 'Value': ou_name},
{'Key': 'CostCenter', 'Value': event.get('cost_center', 'Default')}
]
)
# Enable security services
enable_security_services(account_id)
# Create baseline VPC
create_baseline_vpc(account_id, event.get('vpc_config', {}))
# Setup cross-account roles
create_cross_account_roles(account_id)
return {
'statusCode': 200,
'body': json.dumps({
'message': f'Account {account_id} configured successfully',
'account_email': account_email
})
}
def enable_security_services(account_id):
"""Enable GuardDuty, Security Hub, Config"""
# Assume role in new account
sts = boto3.client('sts')
assumed_role = sts.assume_role(
RoleArn=f'arn:aws:iam::{account_id}:role/AWSControlTowerExecution',
RoleSessionName='SecuritySetup'
)
credentials = assumed_role['Credentials']
# Enable GuardDuty
guardduty = boto3.client(
'guardduty',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
detector = guardduty.create_detector(Enable=True)
# Enable Security Hub
securityhub = boto3.client(
'securityhub',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
securityhub.enable_security_hub()
# Enable AWS Config
config = boto3.client(
'config',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
# Config recorder setup (simplified)
config.put_configuration_recorder(
ConfigurationRecorder={
'name': 'default',
'roleARN': f'arn:aws:iam::{account_id}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig',
'recordingGroup': {
'allSupported': True,
'includeGlobalResourceTypes': True
}
}
)
def create_baseline_vpc(account_id, vpc_config):
"""Create standardized VPC using CDK/CloudFormation"""
# Implementation would deploy VPC stack
pass
def create_cross_account_roles(account_id):
"""Create roles for cross-account access"""
# Implementation would create IAM roles
pass
Infrastructure as Code: AWS Organizations with CDK
Complete CDK Example: Organization Setup
// lib/organization-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as organizations from 'aws-cdk-lib/aws-organizations';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class OrganizationStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create Organizational Units
const securityOU = new organizations.CfnOrganizationalUnit(this, 'SecurityOU', {
name: 'Security',
parentId: 'r-xxxx', // Root ID from AWS Organizations
});
const infrastructureOU = new organizations.CfnOrganizationalUnit(this, 'InfrastructureOU', {
name: 'Infrastructure',
parentId: 'r-xxxx',
});
const productionOU = new organizations.CfnOrganizationalUnit(this, 'ProductionOU', {
name: 'Production',
parentId: 'r-xxxx',
});
const nonProductionOU = new organizations.CfnOrganizationalUnit(this, 'NonProductionOU', {
name: 'NonProduction',
parentId: 'r-xxxx',
});
// Create Service Control Policies
const denyOutsideEuSCP = new organizations.CfnPolicy(this, 'DenyOutsideEU', {
name: 'DenyRegionsOutsideEU',
description: 'Prevent resource creation outside EU regions',
type: 'SERVICE_CONTROL_POLICY',
content: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyAllOutsideEU',
Effect: 'Deny',
Action: '*',
Resource: '*',
Condition: {
StringNotEquals: {
'aws:RequestedRegion': ['eu-west-1', 'eu-central-1'],
},
},
},
],
}),
});
const requireEncryptionSCP = new organizations.CfnPolicy(this, 'RequireEncryption', {
name: 'RequireEncryption',
description: 'Require encryption for S3 and EBS',
type: 'SERVICE_CONTROL_POLICY',
content: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyUnencryptedS3',
Effect: 'Deny',
Action: 's3:PutObject',
Resource: '*',
Condition: {
StringNotEquals: {
's3:x-amz-server-side-encryption': ['AES256', 'aws:kms'],
},
},
},
{
Sid: 'DenyUnencryptedEBS',
Effect: 'Deny',
Action: ['ec2:RunInstances', 'ec2:CreateVolume'],
Resource: '*',
Condition: {
Bool: {
'ec2:Encrypted': 'false',
},
},
},
],
}),
});
const denyRootUserSCP = new organizations.CfnPolicy(this, 'DenyRootUser', {
name: 'DenyRootUser',
description: 'Prevent root user from performing actions',
type: 'SERVICE_CONTROL_POLICY',
content: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Sid: 'DenyRootUser',
Effect: 'Deny',
Action: '*',
Resource: '*',
Condition: {
StringLike: {
'aws:PrincipalArn': 'arn:aws:iam::*:root',
},
},
},
],
}),
});
// Attach SCPs to OUs
new organizations.CfnPolicyAttachment(this, 'SecurityOURegionPolicy', {
policyId: denyOutsideEuSCP.ref,
targetId: securityOU.ref,
});
new organizations.CfnPolicyAttachment(this, 'ProductionOURegionPolicy', {
policyId: denyOutsideEuSCP.ref,
targetId: productionOU.ref,
});
new organizations.CfnPolicyAttachment(this, 'ProductionOUEncryptionPolicy', {
policyId: requireEncryptionSCP.ref,
targetId: productionOU.ref,
});
new organizations.CfnPolicyAttachment(this, 'ProductionOURootPolicy', {
policyId: denyRootUserSCP.ref,
targetId: productionOU.ref,
});
// Outputs
new cdk.CfnOutput(this, 'SecurityOUId', {
value: securityOU.ref,
description: 'Security OU ID',
});
new cdk.CfnOutput(this, 'ProductionOUId', {
value: productionOU.ref,
description: 'Production OU ID',
});
}
}
// bin/organization-app.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { OrganizationStack } from '../lib/organization-stack';
const app = new cdk.App();
new OrganizationStack(app, 'OrganizationStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'us-east-1', // Organizations API is in us-east-1
},
description: 'AWS Organizations multi-account structure',
});
app.synth();
Terraform Alternative
# terraform/organizations.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Create Organizational Units
resource "aws_organizations_organizational_unit" "security" {
name = "Security"
parent_id = aws_organizations_organization.main.roots[0].id
}
resource "aws_organizations_organizational_unit" "infrastructure" {
name = "Infrastructure"
parent_id = aws_organizations_organization.main.roots[0].id
}
resource "aws_organizations_organizational_unit" "production" {
name = "Production"
parent_id = aws_organizations_organization.main.roots[0].id
}
resource "aws_organizations_organizational_unit" "non_production" {
name = "NonProduction"
parent_id = aws_organizations_organization.main.roots[0].id
}
# Service Control Policy: Deny outside EU
resource "aws_organizations_policy" "deny_outside_eu" {
name = "DenyRegionsOutsideEU"
description = "Prevent resource creation outside EU regions"
type = "SERVICE_CONTROL_POLICY"
content = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyAllOutsideEU"
Effect = "Deny"
Action = "*"
Resource = "*"
Condition = {
StringNotEquals = {
"aws:RequestedRegion" = [
"eu-west-1",
"eu-central-1"
]
}
}
}
]
})
}
# Service Control Policy: Require Encryption
resource "aws_organizations_policy" "require_encryption" {
name = "RequireEncryption"
description = "Require encryption for S3 and EBS"
type = "SERVICE_CONTROL_POLICY"
content = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyUnencryptedS3"
Effect = "Deny"
Action = "s3:PutObject"
Resource = "*"
Condition = {
StringNotEquals = {
"s3:x-amz-server-side-encryption" = ["AES256", "aws:kms"]
}
}
},
{
Sid = "DenyUnencryptedEBS"
Effect = "Deny"
Action = [
"ec2:RunInstances",
"ec2:CreateVolume"
]
Resource = "*"
Condition = {
Bool = {
"ec2:Encrypted" = "false"
}
}
}
]
})
}
# Attach policies to OUs
resource "aws_organizations_policy_attachment" "security_region_policy" {
policy_id = aws_organizations_policy.deny_outside_eu.id
target_id = aws_organizations_organizational_unit.security.id
}
resource "aws_organizations_policy_attachment" "production_region_policy" {
policy_id = aws_organizations_policy.deny_outside_eu.id
target_id = aws_organizations_organizational_unit.production.id
}
resource "aws_organizations_policy_attachment" "production_encryption_policy" {
policy_id = aws_organizations_policy.require_encryption.id
target_id = aws_organizations_organizational_unit.production.id
}
# Outputs
output "security_ou_id" {
description = "Security OU ID"
value = aws_organizations_organizational_unit.security.id
}
output "production_ou_id" {
description = "Production OU ID"
value = aws_organizations_organizational_unit.production.id
}
Cross-Account Access Patterns
Pattern 1: Cross-Account IAM Roles (Recommended)
Scenario: Application in Account A needs to access S3 bucket in Account B
Account B (Resource Account):
// lib/resource-account-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class ResourceAccountStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 bucket in Account B
const dataBucket = new s3.Bucket(this, 'DataBucket', {
bucketName: 'account-b-data-bucket',
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
// Create cross-account role
const crossAccountRole = new iam.Role(this, 'CrossAccountS3AccessRole', {
roleName: 'CrossAccountS3Access',
assumedBy: new iam.AccountPrincipal('111122223333'), // Account A ID
description: 'Role for Account A to access S3 bucket in Account B',
});
// Grant permissions
dataBucket.grantRead(crossAccountRole);
// Output role ARN
new cdk.CfnOutput(this, 'CrossAccountRoleArn', {
value: crossAccountRole.roleArn,
description: 'Cross-account role ARN',
exportName: 'CrossAccountS3RoleArn',
});
}
}
Account A (Requesting Account):
// lib/application-account-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class ApplicationAccountStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lambda function that needs access to Account B
const dataProcessorFunction = new lambda.Function(this, 'DataProcessor', {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'index.handler',
code: lambda.Code.fromInline(`
import boto3
import json
def handler(event, context):
# Assume role in Account B
sts = boto3.client('sts')
assumed_role = sts.assume_role(
RoleArn='arn:aws:iam::444455556666:role/CrossAccountS3Access',
RoleSessionName='DataProcessorSession'
)
credentials = assumed_role['Credentials']
# Access S3 in Account B using assumed role
s3 = boto3.client(
's3',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
# List objects in Account B bucket
response = s3.list_objects_v2(Bucket='account-b-data-bucket')
return {
'statusCode': 200,
'body': json.dumps({
'objects': response.get('Contents', [])
})
}
`),
});
// Grant permission to assume the cross-account role
dataProcessorFunction.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['sts:AssumeRole'],
resources: ['arn:aws:iam::444455556666:role/CrossAccountS3Access'],
}));
}
}
Pattern 2: Resource-Based Policies
Scenario: Lambda in Account A needs to be invoked by EventBridge in Account B
// Account A - Lambda with resource policy
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class CrossAccountLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const targetFunction = new lambda.Function(this, 'TargetFunction', {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'index.handler',
code: lambda.Code.fromInline(`
def handler(event, context):
print(f"Received event from Account B: {event}")
return {'statusCode': 200, 'body': 'Processed'}
`),
});
// Add resource-based policy to allow cross-account invocation
targetFunction.addPermission('AllowAccountBInvoke', {
principal: new iam.AccountPrincipal('444455556666'), // Account B
action: 'lambda:InvokeFunction',
sourceAccount: '444455556666',
});
new cdk.CfnOutput(this, 'FunctionArn', {
value: targetFunction.functionArn,
description: 'Lambda function ARN for cross-account invocation',
});
}
}
Pattern 3: AWS Resource Access Manager (RAM)
Share resources like Transit Gateways, Subnets, or Route53 Resolver rules across accounts.
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ram from 'aws-cdk-lib/aws-ram';
import { Construct } from 'constructs';
export class NetworkAccountStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create Transit Gateway in Network account
const transitGateway = new ec2.CfnTransitGateway(this, 'TransitGateway', {
description: 'Shared Transit Gateway for all accounts',
defaultRouteTableAssociation: 'enable',
defaultRouteTablePropagation: 'enable',
});
// Share Transit Gateway with entire organization
const tgwShare = new ram.CfnResourceShare(this, 'TGWShare', {
name: 'TransitGatewayShare',
resourceArns: [
`arn:aws:ec2:${this.region}:${this.account}:transit-gateway/${transitGateway.ref}`,
],
principals: [
'arn:aws:organizations::111122223333:organization/o-xxxxxxxxxx', // Organization ARN
],
allowExternalPrincipals: false,
});
new cdk.CfnOutput(this, 'TransitGatewayId', {
value: transitGateway.ref,
description: 'Shared Transit Gateway ID',
exportName: 'SharedTransitGatewayId',
});
}
}
Best Practices
1. Account Naming and Tagging
Consistent Naming Convention:
{environment}-{businessunit}-{purpose}-{region}
Examples:
- prod-finance-api-euw1
- dev-engineering-sandbox-euw1
- shared-network-tgw-euw1
Mandatory Tags:
CostCenter: "CC-12345"
Environment: "Production" | "Staging" | "Development" | "Sandbox"
Owner: "team-name@company.com"
Project: "project-alpha"
Compliance: "PCI-DSS" | "HIPAA" | "None"
DataClassification: "Public" | "Internal" | "Confidential" | "Restricted"
BackupPolicy: "Daily" | "Weekly" | "None"
2. Security Baseline Automation
Deploy security baseline automatically to every new account:
# baseline-security.yaml
SecurityBaseline:
- Enable CloudTrail (organization trail)
- Enable AWS Config
- Enable GuardDuty
- Enable Security Hub
- Enable IAM Access Analyzer
- Block S3 public access (account-level)
- Enable EBS encryption by default
- Enforce IMDSv2 for EC2
- Deploy AWS Systems Manager Session Manager
- Create CloudWatch Log Groups for VPC Flow Logs
3. Cost Management
Implement Cost Allocation:
- Tag enforcement via SCPs
- Cost and Usage Reports to S3
- AWS Cost Explorer access per account
- Budget alerts per account/OU
- Reserved Instance sharing across organization
Example Budget Alarm:
import * as budgets from 'aws-cdk-lib/aws-budgets';
new budgets.CfnBudget(this, 'MonthlyBudget', {
budget: {
budgetName: 'MonthlyAccountBudget',
budgetType: 'COST',
timeUnit: 'MONTHLY',
budgetLimit: {
amount: 1000,
unit: 'USD',
},
},
notificationsWithSubscribers: [
{
notification: {
notificationType: 'ACTUAL',
comparisonOperator: 'GREATER_THAN',
threshold: 80,
},
subscribers: [
{
subscriptionType: 'EMAIL',
address: 'finance-alerts@company.com',
},
],
},
],
});
4. Network Connectivity
Hub-and-Spoke with Transit Gateway:
- Network account: Transit Gateway hub
- Spoke accounts: VPCs attached to TGW
- Centralized egress/ingress
- Shared services access
Avoid:
- VPC peering at scale (doesn’t scale, complex routing)
- Public internet for inter-account communication
- Overly permissive security groups
5. Compliance and Auditing
Centralized Logging:
All accounts → CloudTrail → Log Archive Account S3
All accounts → Config → Log Archive Account S3
All accounts → VPC Flow Logs → Log Archive Account S3
Read-Only Audit Access:
// Security Audit account can read all accounts
const auditRole = new iam.Role(this, 'SecurityAuditRole', {
roleName: 'SecurityAuditor',
assumedBy: new iam.AccountPrincipal('999988887777'), // Security Audit account
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('SecurityAudit'),
iam.ManagedPolicy.fromAwsManagedPolicyName('ViewOnlyAccess'),
],
});
6. Account Lifecycle Management
New Account Checklist:
- Create via Account Factory (automated baseline)
- Assign to appropriate OU
- Apply required SCPs
- Enable security services
- Create baseline VPC
- Configure cross-account roles
- Set up billing alerts
- Tag account appropriately
- Document in CMDB
- Grant access to teams
Account Decommissioning:
- Move to “Suspended” OU
- Apply restrictive SCP (deny all except billing)
- Delete resources
- Cancel subscriptions
- Archive logs to Glacier
- Close account after retention period
Conclusion
A well-designed multi-account AWS strategy using AWS Organizations is foundational to secure, scalable, and compliant cloud operations. By implementing the patterns and practices outlined in this guide, you’ll establish a robust cloud environment that grows with your organization.
Key Takeaways:
- Start with a clear OU structure aligned with your business
- Use SCPs to establish guardrails, not to manage permissions
- Automate account provisioning and baseline security
- Implement cross-account access using IAM roles
- Tag everything for cost allocation and compliance
- Centralize logging and security monitoring
- Plan for scale from day one
Next Steps:
- Assess your current account structure
- Design your target OU hierarchy
- Define essential SCPs for your compliance requirements
- Deploy AWS Control Tower or custom IaC solution
- Migrate existing accounts to new structure
- Implement automated account vending
- Train teams on multi-account best practices
Ready to implement a robust multi-account strategy? Forrict can help you design and deploy an AWS Organizations structure tailored to your business needs.
Resources
- AWS Organizations Documentation
- AWS Control Tower Guide
- Service Control Policies Examples
- AWS Multi-Account Security Strategy
- AWS Well-Architected Framework - Security Pillar
Forrict Team
AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.