Managing AWS Organizations with Terraform

A comprehensive guide to setting up AWS Organizations for multi-account management using Terraform Infrastructure as Code

Managing AWS Organizations with Terraform

AWS Organizations helps you centrally manage and govern your environment as you grow and scale your AWS resources. This guide shows how to set up AWS Organizations using Terraform.

Prerequisites

  • AWS CLI configured
  • Terraform installed
  • Root or management account access
  • Understanding of organizational structure

Project Structure

aws-organizations-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

Basic Organizations Configuration

# main.tf
provider "aws" {
  region = var.aws_region
}

# Enable AWS Organizations
resource "aws_organizations_organization" "main" {
  aws_service_access_principals = [
    "cloudtrail.amazonaws.com",
    "config.amazonaws.com",
    "sso.amazonaws.com",
    "backup.amazonaws.com",
    "guardduty.amazonaws.com"
  ]

  feature_set = "ALL"

  enabled_policy_types = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY",
    "BACKUP_POLICY",
    "AISERVICES_OPT_OUT_POLICY"
  ]
}

# Root Organizational Unit
resource "aws_organizations_organizational_unit" "root" {
  name      = var.organization_name
  parent_id = aws_organizations_organization.main.roots[0].id
}

# Environment OUs
resource "aws_organizations_organizational_unit" "environments" {
  for_each  = toset(["Production", "Staging", "Development"])
  name      = each.key
  parent_id = aws_organizations_organizational_unit.root.id
}

# Workload OUs
resource "aws_organizations_organizational_unit" "workloads" {
  for_each  = toset(["Applications", "Data", "Security", "Shared"])
  name      = each.key
  parent_id = aws_organizations_organizational_unit.root.id
}

# Service Control Policy - Deny Root Access
resource "aws_organizations_policy" "deny_root" {
  name = "deny-root-access"
  type = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyRootAccess"
        Effect = "Deny"
        Action = "*"
        Resource = "*"
        Condition = {
          StringLike = {
            "aws:PrincipalArn": [
              "arn:aws:iam::*:root"
            ]
          }
        }
      }
    ]
  })
}

# Service Control Policy - Restrict Regions
resource "aws_organizations_policy" "restrict_regions" {
  name = "restrict-regions"
  type = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyNonApprovedRegions"
        Effect = "Deny"
        Action = "*"
        Resource = "*"
        Condition = {
          StringNotLike = {
            "aws:RequestedRegion": var.allowed_regions
          }
        }
      }
    ]
  })
}

# Tag Policy
resource "aws_organizations_policy" "tagging" {
  name = "mandatory-tags"
  type = "TAG_POLICY"

  content = jsonencode({
    tags = {
      Environment = {
        tag_key = {
          @@assign = "Environment"
        }
        tag_value = {
          @@assign = ["Production", "Staging", "Development"]
        }
        enforced_for = {
          @@assign = ["ec2:instance", "rds:db"]
        }
      }
      CostCenter = {
        tag_key = {
          @@assign = "CostCenter"
        }
        tag_value = {
          @@assign = var.cost_centers
        }
        enforced_for = {
          @@assign = ["*"]
        }
      }
    }
  })
}

# Backup Policy
resource "aws_organizations_policy" "backup" {
  name = "backup-policy"
  type = "BACKUP_POLICY"

  content = jsonencode({
    plans = {
      daily_backup = {
        regions = {
          @@assign = var.allowed_regions
        }
        rules = {
          daily_rule = {
            schedule_expression = {
              @@assign = "cron(0 5 ? * * *)"
            }
            start_backup_window_minutes = {
              @@assign = "60"
            }
            complete_backup_window_minutes = {
              @@assign = "120"
            }
            lifecycle = {
              delete_after_days = {
                @@assign = "30"
              }
            }
            target_backup_vault_name = {
              @@assign = "Default"
            }
            copy_actions = {
              secondary_region = {
                target_backup_vault_arn = {
                  @@assign = "arn:aws:backup:${var.secondary_region}:$account:backup-vault/Default"
                }
                lifecycle = {
                  delete_after_days = {
                    @@assign = "30"
                  }
                }
              }
            }
          }
        }
        selections = {
          tags = {
            backup = {
              iam_role_arn = {
                @@assign = "arn:aws:iam::$account:role/AWS-Backup-Service-Role"
              }
              tag_key = {
                @@assign = "Backup"
              }
              tag_value = {
                @@assign = ["true"]
              }
            }
          }
        }
      }
    }
  })
}

# AI Services Opt-out Policy
resource "aws_organizations_policy" "ai_optout" {
  name = "ai-services-optout"
  type = "AISERVICES_OPT_OUT_POLICY"

  content = jsonencode({
    services = {
      default = {
        opt_out_policy = {
          @@assign = "optOut"
        }
      }
      rekognition = {
        opt_out_policy = {
          @@assign = "optIn"
        }
      }
    }
  })
}

Variables Configuration

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "organization_name" {
  description = "Name of the organization"
  type        = string
}

variable "allowed_regions" {
  description = "List of allowed AWS regions"
  type        = list(string)
  default     = ["us-west-2", "us-east-1"]
}

variable "cost_centers" {
  description = "List of valid cost centers"
  type        = list(string)
  default     = ["IT", "Marketing", "Sales", "Engineering"]
}

variable "secondary_region" {
  description = "Secondary region for backup copies"
  type        = string
  default     = "us-east-1"
}

Best Practices

  1. Organization Management

    • Use proper OU structure
    • Implement SCPs effectively
    • Configure proper policies
    • Regular structure reviews
  2. Security

    • Enable security services
    • Use proper access controls
    • Implement compliance policies
    • Regular security reviews
  3. Cost Optimization

    • Monitor account usage
    • Implement cost allocation
    • Use consolidated billing
    • Regular cost reviews
  4. Governance

    • Implement tagging policies
    • Use service control policies
    • Configure backup policies
    • Regular governance reviews

Account Management

# Create Member Account
resource "aws_organizations_account" "member" {
  name      = "member-account"
  email     = "member@example.com"
  role_name = "OrganizationAccountAccessRole"

  parent_id = aws_organizations_organizational_unit.environments["Production"].id

  tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}

# Delegate Administrator Account
resource "aws_organizations_delegated_administrator" "security" {
  account_id = aws_organizations_account.security.id
  service_principal = "securityhub.amazonaws.com"
}

# Move Account
resource "aws_organizations_organizational_unit_account_membership" "production" {
  account_id = aws_organizations_account.member.id
  parent_id  = aws_organizations_organizational_unit.environments["Production"].id
}

Policy Attachments

# Attach SCP to OU
resource "aws_organizations_policy_attachment" "deny_root" {
  policy_id = aws_organizations_policy.deny_root.id
  target_id = aws_organizations_organizational_unit.root.id
}

# Attach Tag Policy to OU
resource "aws_organizations_policy_attachment" "tagging" {
  policy_id = aws_organizations_policy.tagging.id
  target_id = aws_organizations_organizational_unit.root.id
}

# Attach Backup Policy to Production OU
resource "aws_organizations_policy_attachment" "backup_prod" {
  policy_id = aws_organizations_policy.backup.id
  target_id = aws_organizations_organizational_unit.environments["Production"].id
}

Service Integrations

# Enable AWS Config
resource "aws_organizations_organization_config_rule_status" "config" {
  depends_on = [aws_organizations_organization.main]
  status     = "ENABLED"
}

# Enable AWS Security Hub
resource "aws_organizations_organization_security_hub_status" "security_hub" {
  depends_on = [aws_organizations_organization.main]
  status     = "ENABLED"
}

# Enable AWS GuardDuty
resource "aws_organizations_organization_guardduty_status" "guardduty" {
  depends_on = [aws_organizations_organization.main]
  status     = "ENABLED"
}

Common Use Cases

  1. Multi-Environment Setup
# Create Environment Accounts
resource "aws_organizations_account" "environments" {
  for_each  = toset(["dev", "staging", "prod"])
  name      = "${var.organization_name}-${each.key}"
  email     = "aws-${each.key}@example.com"
  role_name = "OrganizationAccountAccessRole"

  parent_id = aws_organizations_organizational_unit.environments[
    each.key == "prod" ? "Production" : 
    each.key == "staging" ? "Staging" : "Development"
  ].id

  tags = {
    Environment = each.key
    ManagedBy   = "Terraform"
  }
}

# Environment-Specific SCPs
resource "aws_organizations_policy" "environment_restrictions" {
  for_each = {
    dev = {
      instance_types = ["t2.micro", "t2.small"]
      max_cost      = 100
    }
    staging = {
      instance_types = ["t2.medium", "t2.large"]
      max_cost      = 500
    }
    prod = {
      instance_types = ["m5.large", "m5.xlarge"]
      max_cost      = 1000
    }
  }

  name = "${each.key}-restrictions"
  type = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "RestrictInstanceTypes"
        Effect = "Deny"
        Action = "ec2:RunInstances"
        Resource = "arn:aws:ec2:*:*:instance/*"
        Condition = {
          StringNotLike = {
            "ec2:InstanceType": each.value.instance_types
          }
        }
      },
      {
        Sid    = "BudgetLimit"
        Effect = "Deny"
        Action = [
          "ec2:RunInstances",
          "rds:CreateDBInstance"
        ]
        Resource = "*"
        Condition = {
          NumericGreaterThan = {
            "aws:RequestTag/EstimatedCost": each.value.max_cost
          }
        }
      }
    ]
  })
}
  1. Shared Services Account
# Create Shared Services Account
resource "aws_organizations_account" "shared_services" {
  name      = "${var.organization_name}-shared-services"
  email     = "aws-shared-services@example.com"
  role_name = "OrganizationAccountAccessRole"

  parent_id = aws_organizations_organizational_unit.workloads["Shared"].id

  tags = {
    Type      = "SharedServices"
    ManagedBy = "Terraform"
  }
}

# Allow Access from Member Accounts
resource "aws_organizations_policy" "shared_services_access" {
  name = "shared-services-access"
  type = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowSharedServicesAccess"
        Effect = "Allow"
        Action = [
          "ec2:DescribeTransitGateways",
          "route53resolver:*",
          "directconnect:*"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:PrincipalOrgPaths": ["${aws_organizations_organizational_unit.workloads["Shared"].id}/*"]
          }
        }
      }
    ]
  })
}

Advanced Features

# Cross-Account Access Role
resource "aws_iam_role" "cross_account" {
  provider = aws.member
  name     = "CrossAccountAccess"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Condition = {
          StringEquals = {
            "aws:PrincipalOrgID": aws_organizations_organization.main.id
          }
        }
      }
    ]
  })
}

# Organization Trail
resource "aws_cloudtrail" "organization" {
  name                          = "${var.organization_name}-trail"
  s3_bucket_name               = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_organization_trail        = true
  enable_logging               = true
  
  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }

  tags = {
    Environment = "Organization"
    ManagedBy   = "Terraform"
  }
}

Conclusion

This setup provides a comprehensive foundation for deploying AWS Organizations using Terraform. Remember to:

  • Plan your organizational structure carefully
  • Implement proper security controls
  • Monitor account usage and costs
  • Keep your configurations versioned
  • Test thoroughly before production deployment

The complete code can be customized based on your specific requirements and use cases.