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.