AWS Organizations and Multi-Account Setup with Terraform
Why use several AWS accounts #
An AWS account is the strongest isolation boundary: permissions, quotas, billing and blast radius are per account. The usual recommendation is one account per environment and workload (development, production, shared services, security, logging) instead of one big account. AWS Organizations groups them under one management account with consolidated billing and central policies. See AWS best practices.
A typical structure #
Root
├── Security OU (audit, log archive)
├── Infrastructure OU (network, shared services)
├── Workloads OU
│ ├── dev account
│ └── pro account
└── Sandbox OU (experiments with budget limits)
Create the organization #
Run this once, from the management account, and keep nothing else in it:
resource "aws_organizations_organization" "this" {
feature_set = "ALL"
aws_service_access_principals = [
"cloudtrail.amazonaws.com",
"config.amazonaws.com",
"sso.amazonaws.com",
"guardduty.amazonaws.com",
]
enabled_policy_types = ["SERVICE_CONTROL_POLICY", "TAG_POLICY"]
}
resource "aws_organizations_organizational_unit" "workloads" {
name = "Workloads"
parent_id = aws_organizations_organization.this.roots[0].id
}
resource "aws_organizations_organizational_unit" "security" {
name = "Security"
parent_id = aws_organizations_organization.this.roots[0].id
}If you already have an organization, import it instead of creating it.
Create accounts #
resource "aws_organizations_account" "pro" {
name = "ditwl-pro"
email = "aws-pro@example.com" # unique per account
parent_id = aws_organizations_organizational_unit.workloads.id
role_name = "OrganizationAccountAccessRole"
iam_user_access_to_billing = "DENY"
lifecycle {
ignore_changes = [role_name]
prevent_destroy = true
}
}Service control policies #
An SCP sets the maximum permissions for every identity in an account, including its administrators. It does not grant access: it limits it.
data "aws_iam_policy_document" "guardrails" {
statement {
sid = "DenyLeavingOrganization"
effect = "Deny"
actions = ["organizations:LeaveOrganization"]
resources = ["*"]
}
statement {
sid = "DenyDisablingCloudTrail"
effect = "Deny"
actions = ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail"]
resources = ["*"]
}
statement {
sid = "RestrictRegions"
effect = "Deny"
not_actions = ["iam:*", "organizations:*", "route53:*", "cloudfront:*", "support:*", "sts:*"]
resources = ["*"]
condition {
test = "StringNotEquals"
variable = "aws:RequestedRegion"
values = ["eu-west-1", "eu-central-1"]
}
}
}
resource "aws_organizations_policy" "guardrails" {
name = "guardrails"
type = "SERVICE_CONTROL_POLICY"
content = data.aws_iam_policy_document.guardrails.json
}
resource "aws_organizations_policy_attachment" "workloads" {
policy_id = aws_organizations_policy.guardrails.id
target_id = aws_organizations_organizational_unit.workloads.id
}Test SCPs first in a sandbox OU: a wrong deny can block your own pipeline. Global services in not_actions must be excluded from the region restriction, otherwise things such as IAM stop working. SCPs do not apply to the management account, which is another reason to keep it empty.
Access across accounts #
- People: use IAM Identity Center (SSO) with permission sets assigned to groups and accounts, with no IAM users.
- Automation: a role in each target account that the pipeline assumes from a central account or with OIDC.
- Terraform: one provider per account with
assume_role:
provider "aws" {
alias = "pro"
region = "eu-west-1"
assume_role {
role_arn = "arn:aws:iam::${aws_organizations_account.pro.id}:role/OrganizationAccountAccessRole"
}
}Providers cannot be created in a loop in Terraform (OpenTofu 1.9 and later can use for_each on providers), so large setups use a separate configuration per account, with Terragrunt or one directory per environment.
Centralized services #
Typical accounts and what runs in them:
- Log archive: the organization-wide CloudTrail bucket, with write-once policies.
- Security/audit: GuardDuty, Config and Security Hub delegated administrator.
- Network: Transit Gateway, shared with RAM.
- Shared services: CI/CD runners, DNS, container registry.
Governance #
Add budgets per account, mandatory tags with tag policies (resource tagging) and a quarterly review of the SCPs.