Multi-Account Strategies

AWS multi-account architecture is the gold standard for enterprise cloud deployments, providing isolation, security boundaries, and simplified billing. However, managing dozens or hundreds of AWS accounts manually becomes impossible. Terraform can automate account creation, organization setup, and cross-account access patterns, but it requires careful planning and understanding of AWS Organizations.

Here we’ll dive into patterns and practices for implementing multi-account AWS architectures with Terraform, from basic organization setup to complex cross-account workflows.

AWS Organizations Setup

AWS Organizations provides centralized management for multiple AWS accounts:

# Create the organization (run this in the master account)
resource "aws_organizations_organization" "main" {
  aws_service_access_principals = [
    "cloudtrail.amazonaws.com",
    "config.amazonaws.com",
    "guardduty.amazonaws.com",
    "securityhub.amazonaws.com",
    "sso.amazonaws.com"
  ]
  
  feature_set = "ALL"
  
  enabled_policy_types = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY",
    "BACKUP_POLICY"
  ]
}

# Organizational Units for different environments
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      = "Non-Production"
  parent_id = aws_organizations_organization.main.roots[0].id
}

resource "aws_organizations_organizational_unit" "security" {
  name      = "Security"
  parent_id = aws_organizations_organization.main.roots[0].id
}

resource "aws_organizations_organizational_unit" "shared_services" {
  name      = "Shared Services"
  parent_id = aws_organizations_organization.main.roots[0].id
}

Account Creation Automation

Automate the creation of new AWS accounts:

# Account creation with proper naming and email conventions
resource "aws_organizations_account" "accounts" {
  for_each = var.aws_accounts
  
  name      = each.value.name
  email     = each.value.email
  role_name = "OrganizationAccountAccessRole"
  
  # Move to appropriate OU after creation
  parent_id = each.value.parent_ou_id
  
  tags = {
    Environment = each.value.environment
    Purpose     = each.value.purpose
    Owner       = each.value.owner
  }
}

# Variable definition for accounts
variable "aws_accounts" {
  description = "AWS accounts to create"
  type = map(object({
    name         = string
    email        = string
    environment  = string
    purpose      = string
    owner        = string
    parent_ou_id = string
  }))
  
  default = {
    prod_web = {
      name         = "Production Web Services"
      email        = "[email protected]"
      environment  = "production"
      purpose      = "web-services"
      owner        = "web-team"
      parent_ou_id = aws_organizations_organizational_unit.production.id
    }
    prod_data = {
      name         = "Production Data Services"
      email        = "[email protected]"
      environment  = "production"
      purpose      = "data-services"
      owner        = "data-team"
      parent_ou_id = aws_organizations_organizational_unit.production.id
    }
    dev_sandbox = {
      name         = "Development Sandbox"
      email        = "[email protected]"
      environment  = "development"
      purpose      = "sandbox"
      owner        = "engineering"
      parent_ou_id = aws_organizations_organizational_unit.non_production.id
    }
  }
}

Service Control Policies

Implement governance through Service Control Policies:

# Prevent deletion of CloudTrail logs
resource "aws_organizations_policy" "prevent_cloudtrail_deletion" {
  name        = "PreventCloudTrailDeletion"
  description = "Prevent deletion of CloudTrail logs and configuration"
  type        = "SERVICE_CONTROL_POLICY"
  
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "PreventCloudTrailDeletion"
        Effect = "Deny"
        Action = [
          "cloudtrail:DeleteTrail",
          "cloudtrail:StopLogging",
          "cloudtrail:UpdateTrail"
        ]
        Resource = "*"
        Condition = {
          StringNotEquals = {
            "aws:PrincipalArn" = [
              "arn:aws:iam::*:role/OrganizationAccountAccessRole",
              "arn:aws:iam::*:role/SecurityAuditRole"
            ]
          }
        }
      }
    ]
  })
}

# Restrict regions for compliance
resource "aws_organizations_policy" "restrict_regions" {
  name        = "RestrictRegions"
  description = "Restrict operations to approved regions"
  type        = "SERVICE_CONTROL_POLICY"
  
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "RestrictRegions"
        Effect = "Deny"
        NotAction = [
          "iam:*",
          "sts:*",
          "cloudfront:*",
          "route53:*",
          "support:*",
          "trustedadvisor:*"
        ]
        Resource = "*"
        Condition = {
          StringNotEquals = {
            "aws:RequestedRegion" = [
              "us-east-1",
              "us-west-2",
              "eu-west-1"
            ]
          }
        }
      }
    ]
  })
}

# Attach policies to OUs
resource "aws_organizations_policy_attachment" "production_cloudtrail" {
  policy_id = aws_organizations_policy.prevent_cloudtrail_deletion.id
  target_id = aws_organizations_organizational_unit.production.id
}

resource "aws_organizations_policy_attachment" "all_regions" {
  policy_id = aws_organizations_policy.restrict_regions.id
  target_id = aws_organizations_organization.main.roots[0].id
}

Cross-Account Role Management

Set up roles for cross-account access:

# Cross-account role in each member account
resource "aws_iam_role" "cross_account_admin" {
  provider = aws.member_account
  
  name = "CrossAccountAdminRole"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = [
            "arn:aws:iam::${var.master_account_id}:root",
            "arn:aws:iam::${var.security_account_id}:root"
          ]
        }
        Condition = {
          StringEquals = {
            "sts:ExternalId" = var.external_id
          }
          IpAddress = {
            "aws:SourceIp" = var.allowed_ip_ranges
          }
        }
      }
    ]
  })
  
  max_session_duration = 3600
  
  tags = {
    Purpose = "Cross-account administration"
  }
}

# Attach appropriate policies
resource "aws_iam_role_policy_attachment" "cross_account_admin" {
  provider = aws.member_account
  
  role       = aws_iam_role.cross_account_admin.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

# Read-only role for auditing
resource "aws_iam_role" "cross_account_readonly" {
  provider = aws.member_account
  
  name = "CrossAccountReadOnlyRole"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.security_account_id}:root"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "cross_account_readonly" {
  provider = aws.member_account
  
  role       = aws_iam_role.cross_account_readonly.name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

Centralized Logging and Monitoring

Set up centralized logging across all accounts:

# Central logging bucket in security account
resource "aws_s3_bucket" "central_logs" {
  provider = aws.security_account
  
  bucket = "${var.organization_name}-central-logs"
}

resource "aws_s3_bucket_policy" "central_logs" {
  provider = aws.security_account
  
  bucket = aws_s3_bucket.central_logs.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AWSCloudTrailAclCheck"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.central_logs.arn
      },
      {
        Sid    = "AWSCloudTrailWrite"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.central_logs.arn}/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      }
    ]
  })
}

# CloudTrail in each member account
resource "aws_cloudtrail" "member_account" {
  for_each = var.member_accounts
  
  provider = aws.member_accounts[each.key]
  
  name           = "${each.key}-cloudtrail"
  s3_bucket_name = aws_s3_bucket.central_logs.bucket
  s3_key_prefix  = "cloudtrail/${each.key}"
  
  include_global_service_events = true
  is_multi_region_trail        = true
  enable_logging               = true
  
  tags = {
    Account = each.key
    Purpose = "Centralized audit logging"
  }
}

AWS SSO Integration

Integrate with AWS Single Sign-On for centralized access:

# SSO instance (created automatically when SSO is enabled)
data "aws_ssoadmin_instances" "main" {}

# Permission sets for different roles
resource "aws_ssoadmin_permission_set" "admin" {
  name             = "AdministratorAccess"
  description      = "Full administrative access"
  instance_arn     = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  session_duration = "PT2H"  # 2 hours
  
  tags = {
    Purpose = "Administrative access"
  }
}

resource "aws_ssoadmin_managed_policy_attachment" "admin" {
  instance_arn       = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  managed_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
  permission_set_arn = aws_ssoadmin_permission_set.admin.arn
}

# Developer permission set with limited access
resource "aws_ssoadmin_permission_set" "developer" {
  name             = "DeveloperAccess"
  description      = "Developer access with restrictions"
  instance_arn     = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  session_duration = "PT8H"  # 8 hours
}

resource "aws_ssoadmin_permission_set_inline_policy" "developer" {
  inline_policy      = data.aws_iam_policy_document.developer_policy.json
  instance_arn       = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn
}

data "aws_iam_policy_document" "developer_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ec2:Describe*",
      "s3:ListBucket",
      "s3:GetObject",
      "logs:*",
      "cloudwatch:*"
    ]
    resources = ["*"]
  }
  
  statement {
    effect = "Deny"
    actions = [
      "ec2:TerminateInstances",
      "rds:DeleteDBInstance",
      "s3:DeleteBucket"
    ]
    resources = ["*"]
  }
}

# Account assignments
resource "aws_ssoadmin_account_assignment" "admin_prod" {
  instance_arn       = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.admin.arn
  
  principal_id   = var.admin_group_id
  principal_type = "GROUP"
  
  target_id   = aws_organizations_account.accounts["prod_web"].id
  target_type = "AWS_ACCOUNT"
}

Cost Management and Billing

Implement cost controls across accounts:

# Billing alerts for each account
resource "aws_budgets_budget" "account_budget" {
  for_each = var.aws_accounts
  
  provider = aws.member_accounts[each.key]
  
  name         = "${each.key}-monthly-budget"
  budget_type  = "COST"
  limit_amount = each.value.monthly_budget
  limit_unit   = "USD"
  time_unit    = "MONTHLY"
  
  cost_filters = {
    Service = ["Amazon Elastic Compute Cloud - Compute"]
  }
  
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                 = 80
    threshold_type            = "PERCENTAGE"
    notification_type         = "ACTUAL"
    subscriber_email_addresses = [each.value.billing_email]
  }
  
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                 = 100
    threshold_type            = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = [each.value.billing_email]
  }
}

# Cost anomaly detection
resource "aws_ce_anomaly_detector" "account_anomaly" {
  for_each = var.aws_accounts
  
  provider = aws.member_accounts[each.key]
  
  name         = "${each.key}-cost-anomaly-detector"
  monitor_type = "DIMENSIONAL"
  
  specification = jsonencode({
    Dimension = "SERVICE"
    MatchOptions = ["EQUALS"]
    Values = ["EC2-Instance", "RDS"]
  })
}

resource "aws_ce_anomaly_subscription" "account_anomaly" {
  for_each = var.aws_accounts
  
  provider = aws.member_accounts[each.key]
  
  name      = "${each.key}-anomaly-subscription"
  frequency = "DAILY"
  
  monitor_arn_list = [
    aws_ce_anomaly_detector.account_anomaly[each.key].arn
  ]
  
  subscriber {
    type    = "EMAIL"
    address = each.value.billing_email
  }
  
  threshold_expression {
    and {
      dimension {
        key           = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
        values        = ["100"]
        match_options = ["GREATER_THAN_OR_EQUAL"]
      }
    }
  }
}

Account Baseline Configuration

Apply consistent baseline configuration to all accounts:

# Module for account baseline
module "account_baseline" {
  source = "./modules/account-baseline"
  
  for_each = var.aws_accounts
  
  providers = {
    aws = aws.member_accounts[each.key]
  }
  
  account_name        = each.key
  environment         = each.value.environment
  security_account_id = var.security_account_id
  log_bucket_name     = aws_s3_bucket.central_logs.bucket
  
  # Enable services based on account type
  enable_guardduty    = true
  enable_config       = true
  enable_securityhub  = each.value.environment == "production"
  enable_cloudtrail   = true
  
  # Tagging strategy
  default_tags = {
    Account     = each.key
    Environment = each.value.environment
    Owner       = each.value.owner
    ManagedBy   = "terraform"
  }
}

Cross-Account Resource Sharing

Share resources across accounts using Resource Access Manager:

# Share VPC subnets across accounts
resource "aws_ram_resource_share" "shared_subnets" {
  provider = aws.shared_services
  
  name                      = "shared-subnets"
  allow_external_principals = false
  
  tags = {
    Purpose = "Share networking resources"
  }
}

resource "aws_ram_resource_association" "shared_subnets" {
  provider = aws.shared_services
  
  for_each = toset(var.shared_subnet_ids)
  
  resource_arn       = "arn:aws:ec2:${var.aws_region}:${var.shared_services_account_id}:subnet/${each.value}"
  resource_share_arn = aws_ram_resource_share.shared_subnets.arn
}

resource "aws_ram_principal_association" "shared_subnets" {
  provider = aws.shared_services
  
  for_each = var.member_account_ids
  
  principal          = each.value
  resource_share_arn = aws_ram_resource_share.shared_subnets.arn
}

What’s Next

Multi-account strategies provide the organizational foundation for enterprise AWS deployments, but managing costs and implementing proper tagging strategies becomes critical as your infrastructure scales.

In the next part, we’ll explore cost optimization techniques, including resource lifecycle management, automated cost controls, and tagging strategies that enable accurate cost allocation and optimization across your AWS infrastructure.