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
Enabling trusted access via aws_service_access_principals above lets these services see your organization, but turning on organization-wide management for each one is a separate step, done through that service’s own resources (not a generic aws_organizations_organization_* resource - there isn’t one). For example, GuardDuty:
# Delegate a GuardDuty administrator account, then enable org-wide auto-enrollment
resource "aws_guardduty_organization_admin_account" "main" {
admin_account_id = var.security_account_id
}
resource "aws_guardduty_organization_configuration" "main" {
detector_id = aws_guardduty_detector.main.id
auto_enable = true
}
Security Hub and AWS Config follow the same pattern but with their own resources (aws_securityhub_organization_admin_account, and AWS Config’s organization aggregator/conformance pack resources respectively) - check the current provider docs for each, since the exact arguments differ per service.
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.