Multi-Account AWS Strategy with AWS Organizations | Forrict Skip to main content
Cloud Architecture AWS Best Practices

Multi-Account AWS Strategy with AWS Organizations

Forrict Team
Multi-Account AWS Strategy with AWS Organizations
A comprehensive guide to designing and implementing a scalable multi-account AWS architecture using AWS Organizations, Control Tower, and Infrastructure as Code

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

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:

  1. Create via Account Factory (automated baseline)
  2. Assign to appropriate OU
  3. Apply required SCPs
  4. Enable security services
  5. Create baseline VPC
  6. Configure cross-account roles
  7. Set up billing alerts
  8. Tag account appropriately
  9. Document in CMDB
  10. Grant access to teams

Account Decommissioning:

  1. Move to “Suspended” OU
  2. Apply restrictive SCP (deny all except billing)
  3. Delete resources
  4. Cancel subscriptions
  5. Archive logs to Glacier
  6. 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:

  1. Assess your current account structure
  2. Design your target OU hierarchy
  3. Define essential SCPs for your compliance requirements
  4. Deploy AWS Control Tower or custom IaC solution
  5. Migrate existing accounts to new structure
  6. Implement automated account vending
  7. 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

F

Forrict Team

AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.

Tags

AWS AWS Organizations Control Tower Multi-Account Security Cloud Architecture Infrastructure as Code CDK

Related Articles

Ready to Transform Your AWS Infrastructure?

Let's discuss how we can help optimize your cloud journey