AWS Organizations
A cloud service designed to centralize & manage AWS accounts and to roll up billing from multiple AWS accounts into a single account. May be referred to as the “master” account because it can manage permissions of all its accounts that are “attached” to it. “Billing” is another name for this account because it’s the account that gets the invoice or the monthly charge. You can select any commercial AWS account to play this role. The account becomes this master account as soon as you join the master’s accounts organization. Next comes AWS’s Service Control Policies, this feature allows permission management for all your AWS accounts in your organization. In this post, I’ll share with you how to implement AWS’s service control policies with Terraform! Let’s break it down.
How to join an AWS Organization
Check out my previous post on the details.
AWS Organization Units
This has nothing to do with Microsoft’s Active Directory (AD) organization units. There’s no integration between MS AD and this, either. AWS’s organization units are a way of grouping AWS accounts so we can manage accounts permissions in groups or in a hierarchical format. There are dozens of ways to create this hierarchy and it all depends on your objective. You can group them by projects, departments, missions, environments, classifications, etc.
Simple OU structure by environments

Service Control Policies with Terraform
Service Control Policies (SCP) is a critical feature to learn and understand. Questions related to this feature is a topic on many, many AWS certifications. This feature is highly useful and it’s used a lot in the real world. It’s these policies that can allow or deny actions or services at a high level. What I mean by “high level” is outside of the AWS’s account. For example; let’s say you want to experiment with the most expensive EC2 instance type and let’s say you also have IAM permissions to allow those actions. Then you try to launch an EC2 with an instance type of p4d.24xlarge and bam you get the encoded authorized failure message! How?! You have given yourself full permissions! Bingo… it’s the service control policies. Even with a full administration policy on the account, you can still be denied by SCP.
It’s best practice to enable SCP and create the OUs, and policies and attach the policies even before building your systems! For information on SCP checkout AWS’s documents.
Show me the Terraform code!
I personally like to break down the main.tf terraform file into separate manageable terraform files. Here’s my file structure. Notice there’s only one environment aka account “master” here. Yes, I also use terragrunt.
org/
├── README.md
├── dev-ou.tf
├── main.tf
├── master
│ ├── inputs.yml
│ ├── terragrunt.hcl
│ └── vars.tf
├── prod-ou.tf
├── root-ou.tf
├── staging-ou.tf
└── terragrunt.hcl
main.tf this file usually contains common code. Enable SCP by adding the “SERVICE_CONTROL_POLICY” to the enabled_policy_types array.
provider "aws" {
region = var.aws_region
profile = var.aws_cli_profile
}
terraform {
backend "s3" {}
}
# Provides a resource to create an AWS organization.
resource "aws_organizations_organization" "this" {
# List of AWS service principal names for which
# you want to enable integration with your organization
aws_service_access_principals = [
"cloudtrail.amazonaws.com",
"config.amazonaws.com",
]
feature_set = "ALL"
enabled_policy_types = [
"TAG_POLICY",
"SERVICE_CONTROL_POLICY"
]
}
my root-out.tf contains the master account code and all the service control policies that I want to be applied to all accounts. Notice it’s attached to the root OU which then it’s inherited by all the accounts below the root OU!
resource "aws_organizations_account" "master" {
# A friendly name for the member account
name = "my-master"
email = "mymaster@email.com"
# Enables IAM users to access account billing information
# if they have the required permissions
# iam_user_access_to_billing = "ALLOW"
tags = {
Name = "my-master"
Owner = "Waleed"
Role = "billing"
}
parent_id = aws_organizations_organization.this.roots[0].id
}
# ---------------------------------------- #
# Service Control Policies for all accounts
# ---------------------------------------- #
# ---------------------------- #
# REGION RESTRICTION
# ---------------------------- #
data "aws_iam_policy_document" "restrict_regions" {
statement {
sid = "RegionRestriction"
effect = "Deny"
actions = ["*"]
resources = ["*"]
condition {
test = "StringNotEquals"
variable = "aws:RequestedRegion"
values = [
"us-east-1"
]
}
}
}
resource "aws_organizations_policy" "restrict_regions" {
name = "restrict_regions"
description = "Deny all regions except US East 1."
content = data.aws_iam_policy_document.restrict_regions.json
}
resource "aws_organizations_policy_attachment" "restrict_regions_on_root" {
policy_id = aws_organizations_policy.restrict_regions.id
target_id = aws_organizations_organization.this.roots[0].id
}
# ---------------------------- #
# EC2 INSTANCE TYPE RESTRICTION
# ---------------------------- #
data "aws_iam_policy_document" "restrict_ec2_types" {
statement {
sid = "RestrictEc2Types"
effect = "Deny"
actions = ["ec2:RunInstances"]
resources = ["arn:aws:ec2:*:*:instance/*"]
condition {
test = "StringNotEquals"
variable = "ec2:InstanceType"
values = [
"t3*",
"t4g*",
"a1.medium",
"a1.large"
]
}
}
}
resource "aws_organizations_policy" "restrict_ec2_types" {
name = "restrict_ec2_types"
description = "Allow certain EC2 instance types only."
content = data.aws_iam_policy_document.restrict_ec2_types.json
}
resource "aws_organizations_policy_attachment" "restrict_ec2_types_on_root" {
policy_id = aws_organizations_policy.restrict_ec2_types.id
target_id = aws_organizations_organization.this.roots[0].id
}
# ---------------------------- #
# REQUIRE EC2 TAGS
# ---------------------------- #
data "aws_iam_policy_document" "require_ec2_tags" {
statement {
sid = "RequireTag"
effect = "Deny"
actions = [
"ec2:RunInstances",
"ec2:CreateVolume"
]
resources = [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*"
]
condition {
test = "Null"
variable = "aws:RequestTag/Name"
values = ["true"]
}
}
}
resource "aws_organizations_policy" "require_ec2_tags" {
name = "require_ec2_tags"
description = "Name tag is required for EC2 instances and volumes."
content = data.aws_iam_policy_document.require_ec2_tags.json
}
resource "aws_organizations_policy_attachment" "require_ec2_tags_on_root" {
policy_id = aws_organizations_policy.require_ec2_tags.id
target_id = aws_organizations_organization.this.roots[0].id
}
Here’s an authorization failure message when I attempted to create an EC2 with an instance type that was not approved in the SCP defined above!




Account or OU specific SCP
What if you want to restrict certain actions or services on a single account. The code below shows how to block the internet in this environment. This says to create the prod account, create the SCP and only attach it directly to this OU; and this OU only has the prod account. To attach an SCP to an account only check the documentation.
prod-ou.tf
resource "aws_organizations_account" "prod" {
# A friendly name for the member account
name = "my-prod"
email = "my-prod@email.com"
# Enables IAM users to access account billing information
# if they have the required permissions
iam_user_access_to_billing = "ALLOW"
tags = {
Name = "my-prod"
Owner = "Waleed"
Role = "prod"
}
parent_id = aws_organizations_organizational_unit.prod.id
}
resource "aws_organizations_organizational_unit" "prod" {
name = "prod"
parent_id = aws_organizations_organization.this.roots[0].id
}
# ------------------------------- #
# PREVENT INTERNET ACCESS TO A VPC
# ------------------------------- #
data "aws_iam_policy_document" "block_internet" {
statement {
sid = "BlockInternet"
effect = "Deny"
actions = [
"ec2:AttachInternetGateway",
"ec2:CreateInternetGateway",
"ec2:CreateEgressOnlyInternetGateway",
"ec2:CreateVpcPeeringConnection",
"ec2:AcceptVpcPeeringConnection",
"globalaccelerator:Create*",
"globalaccelerator:Update*"
]
resources = ["*"]
}
}
resource "aws_organizations_policy" "block_internet" {
name = "block_internet"
description = "Block internet access to the production network."
content = data.aws_iam_policy_document.block_internet.json
}
resource "aws_organizations_policy_attachment" "block_internet_on_prod" {
policy_id = aws_organizations_policy.block_internet.id
target_id = aws_organizations_organizational_unit.prod.id
}
I think you get the idea, go forth and implement governance with AWS’s organizations and Service Control Policies! Subscribe for more cloud code!
Leave a Reply