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
-
Organization Management
- Use proper OU structure
- Implement SCPs effectively
- Configure proper policies
- Regular structure reviews
-
Security
- Enable security services
- Use proper access controls
- Implement compliance policies
- Regular security reviews
-
Cost Optimization
- Monitor account usage
- Implement cost allocation
- Use consolidated billing
- Regular cost reviews
-
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
- 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
}
}
}
]
})
}
- 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.