AWSAWS OrganizationsIAMSCPTerraform

Don’t panic, organize (part 2 of 2)

By 04/28/2020 No Comments

This is a continuation of Don’t panic, organize (part 1 out of 2)

Creating an Organizational Unit with two accounts in it

Since we already laid the foundation in the form of an AWS Organization accompanied by basic IAM groups and users, our next step will be creating an Organizational Unit (OU) with two accounts in it. OU is basically a container for your accounts and an easy way to group them logically.

Moreover, as we’ll see later, we can attach policies at the OU level and all of the accounts that belong to it will inherit them as well. Wanna stop your admins from inadvertently shutting down your Direct Connect or perhaps from spinning up a certain kind of instances? You can easily enforce that with OU level policies, the so-called Service Control Policies or SCPs. At the OU level, you can also enable trusted access for certain AWS services, such as AWS Single Sign-On, which you can utilize if you decide to allow your users to log in using their corporate credentials.

Furthermore, you can enable tag policies via AWS Organizations. Basically, these policies dictate how resources must be tagged under given OU in order to be compliant.

Okay, let’s start by – you’ve guessed it – making a module for OUs:

+ aws-organizations-example/organizations-organizational_units/main.tf

resource "aws_organizations_organizational_unit" "ou" {
 name      = var.name
 parent_id = var.parent_id
}

+ aws-organizations-example/organizations-organizational_units/variables.tf

variable "name" {
 description = "After the Terraform docs: 'The name for the organizational unit'"
}

variable "parent_id" {
 description = "After the Terraform docs: 'ID of the parent organizational unit, which may be the root'"
}

+ aws-organizations-example/organizations-organizational_units/outputs.tf

output "id" {
 description = "After the Terraform docs: 'Identifier of the organization unit'"
 value       = aws_organizations_organizational_unit.ou.id
}

Next, a module for an AWS account:

+ aws-organizations-example/organizations-accounts/main.tf

resource "aws_organizations_account" "account" {
 name      = var.name
 email     = var.email
 parent_id = var.parent_id
 role_name = var.role_name

 # There is no AWS Organizations API for reading role_name
 lifecycle {
   ignore_changes = [role_name]
 }
}

+ aws-organizations-example/organizations-accounts/variables.tf

variable "name" {
 description = "After the Terraform docs: 'A friendly name for the member account. "
}

variable "email" {
 description = "After the Terraform docs: 'The email address of the owner to assign to the new member account. This email address must not already be associated with another AWS account.'"
}

variable "parent_id" {
 description = "After the Terraform docs: 'Parent Organizational Unit ID or Root ID for the account. Defaults to the Organization default Root ID. A configuration must be present for this argument to perform drift detection.'"
}

variable "role_name" {
 description = "After the Terraform docs: 'The name of an IAM role that Organizations automatically preconfigures in the new member account. This role trusts the master account, allowing users in the master account to assume the role, as permitted by the master account administrator. The role has administrator permissions in the new member account. (...)'"
 type        = string
 default     = null
}

Finally, let’s create an OU with two accounts in it by adding the following lines to our aws-organizations-example/main.tf, and then follow it with terraform init, terraform plan and terraform apply commands:

NB, an email address (email aliases are supported as shown in the example) can be associated with an AWS account only once, i.e. reusing the same email address for a creation of a different AWS account, even if the original one has long since been deleted, is impossible

NB2, to close an AWS account that is a part of an AWS Organization, you must assure first that it can be transformed into a standalone account

+ aws-organizations-example/main.tf

## ORGANIZATION UNITS - START
module "ou-1" {
 source    = "./organizations-organizational_units"
 name      = "ou-1"
 parent_id = module.root.roots.0.id
}
## ORGANIZATION UNITS - END

### ACCOUNTS - START
locals {
 role_name = "adminAssumeRole"
}

module "account-dev" {
 source    = "./organizations-accounts"
 name      = "account-dev"
 email     = "[email protected]_DOMAIN.TLD"
 parent_id = module.ou-1.id
 role_name = local.role_name
}

module "account-prod" {
 source    = "./organizations-accounts"
 name      = "account-prod"
 email     = "[email protected]_DOMAIN.TLD"
 parent_id = module.ou-1.id
 role_name = local.role_name
}
### ACCOUNTS - END
➜  aws-organizations-example$ terraform init
Initializing modules...
- account-dev in organizations-accounts
- account-prod in organizations-accounts
- ou-1 in organizations-organizational_units

# other output

Terraform has been successfully initialized!

# other output
➜  aws-organizations-example$ terraform plan
# other output

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 + create

Terraform will perform the following actions:

 # module.account-dev.aws_organizations_account.account will be created
 + resource "aws_organizations_account" "account" {
     + arn              = (known after apply)
     + email            = "[email protected]"
     + id               = (known after apply)
     + joined_method    = (known after apply)
     + joined_timestamp = (known after apply)
     + name             = "account-dev"
     + parent_id        = (known after apply)
     + role_name        = "adminAssumeRole"
     + status           = (known after apply)
   }

 # module.account-prod.aws_organizations_account.account will be created
 + resource "aws_organizations_account" "account" {
     + arn              = (known after apply)
     + email            = "[email protected]"
     + id               = (known after apply)
     + joined_method    = (known after apply)
     + joined_timestamp = (known after apply)
     + name             = "account-prod"
     + parent_id        = (known after apply)
     + role_name        = "adminAssumeRole"
     + status           = (known after apply)
   }

 # module.ou-1.aws_organizations_organizational_unit.ou will be created
 + resource "aws_organizations_organizational_unit" "ou" {
     + accounts  = (known after apply)
     + arn       = (known after apply)
     + id        = (known after apply)
     + name      = "ou-1"
     + parent_id = "r-deu2"
   }

Plan: 3 to add, 0 to change, 0 to destroy.

# other output
➜  aws-organizations-example$ terraform apply
# other output

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

# other output

To access either of the two newly created accounts you need to log in to your master account as a user with permissions allowing the sts:AssumeRole action on the adminAssumeRole role in your destination account.

(The admin_user_1, which we created earlier, being a member of the administrators group has such permissions.)

Once logged in, grab the number of the account to which you want to switch from AWS Organizations. Next, click on your username located in the upper-right corner of the screen to reveal a dropdown list similar to the one shown below:

You’ll be taken to the screen shown below. Click on the _switch role_ button:

Finally, enter the account number you’ve copied earlier and the name of the role which you want to assume (adminAssumeRole in our case). Hit the blue button:

To log in to the other account you’ll need to go back to the master account, grab the second account’s number and repeat the steps given above.

As far as programmatic access via AWS CLI is concerned, all you need to do is configure your admin_user_1 in the ~/.aws/credentials file:

[admin_user_1]
aws_access_key_id = AKIA57MNK6ZKUWMBKF5D
aws_secret_access_key = YOUR_DECRYPTED_SECRET_ACCESS_KEY

And then create profiles referencing it in the ~/.aws/config file:

[profile account-prod]
role_arn = arn:aws:iam::YOUR_ACCOUNT-PROD_NUMBER:role/adminAssumeRole
source_profile = admin_user_1

[profile account-dev]
role_arn = arn:aws:iam::YOUR_ACCOUNT-DEV_NUMBER:role/adminAssumeRole
source_profile = admin_user_1

With the above set, you can use the CLI as follows:

➜  aws-organizations-example$ AWS_PROFILE=admin_user_1 aws sts get-caller-identity
{
   "UserId": "AIDA57MNK6ZKR6OL2BKS7",
   "Account": "YOUR_MASTER_ACCOUNT_NUMBER",
   "Arn": "arn:aws:iam::YOUR_MASTER_ACCOUNT_NUMBER:user/users/admin_user_1"
}
➜  aws-organizations-example$ AWS_PROFILE=account-prod aws sts get-caller-identity
{
   "UserId": "AROAUWT3K2MNKK3KIHBFA:botocore-session-1586498878",
   "Account": "YOUR_ACCOUNT-PROD_NUMBER",
   "Arn": "arn:aws:sts::YOUR_ACCOUNT-PROD_NUMBER:assumed-role/adminAssumeRole/botocore-session-1586498878"
}
➜  aws-organizations-example$ AWS_PROFILE=account-dev aws sts get-caller-identity
{
   "UserId": "AROA27JRAFMNKW5S4W3MN:botocore-session-1586498917",
   "Account": "YOUR_ACCOUNT-DEV_NUMBER",
   "Arn": "arn:aws:sts::YOUR_ACCOUNT-DEV_NUMBER:assumed-role/adminAssumeRole/botocore-session-1586498917"

Applying a Service Control Policy (SCP)

Lastly, we’ll compose and attach an SCP blocking the root users of ou-1 OU’s accounts, both current and future ones, from taking any (or rather the majority as certain exceptions apply) actions, either via the console or programmatically. (For inspirations on writing other SCPs, check out this webpage.)

So, as usual, we’ll commence with a module:

+ aws-organizations-example/organizations-policies/main.tf

resource "aws_organizations_policy" "policy" {
 name        = var.name
 description = var.description
 content     = var.content
 type        = var.type
}

resource "aws_organizations_policy_attachment" "attachment" {
 count = length(var.target_id)
  policy_id = aws_organizations_policy.policy.id
 target_id = var.target_id[count.index]
}

+ aws-organizations-example/organizations-policies/variables.tf

variable "content" {
 description = "The content of the policy"
 type        = string
}

variable "name" {
 description = "The name of the policy."
 type        = string
}

variable "description" {
 description = "The description of the policy"
 type        = string
}
variable "type" {
 description = "The type of the policy; either SERVICE_CONTROL_POLICY or TAG_POLICY"
 type        = string
}

variable "target_id" {
 description = "The list of IDs of the targets to which the policy should be attached to; can be: the root account, an organizational unit or a non-root account"
 type        = list(string)
}

And we’ll invoke it as follows:

+ aws-organizations-example/main.tf

### SERVICE LEVEL (AND/OR TAG) POLICIES - START
module "lock-down-root-user" {
 source      = "./organizations-policies"
 name        = "lock-down-root-user"
 description = "An SCP blocking the root user from taking any action, either via the console or programmatically"
 content     = <<POLICY
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Action": "*",
     "Resource": "*",
     "Effect": "Deny",
     "Condition": {
       "StringLike": {
         "aws:PrincipalArn": [
           "arn:aws:iam::*:root"
         ]
       }
     }
   }
 ]
}
POLICY
 type        = "SERVICE_CONTROL_POLICY"
 target_id   = [module.ou-1.id]
}
### SERVICE LEVEL (AND/OR TAG) POLICIES - END

Now, on to the last dance:

➜  aws-organizations-example$ terraform init
Initializing modules...
- lock-down-root-user in organizations-policies

# other output

Terraform has been successfully initialized!

# other output
➜  aws-organizations-example$ terraform plan
# other output

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 + create

Terraform will perform the following actions:

 # module.lock-down-root-user.aws_organizations_policy.policy will be created
 + resource "aws_organizations_policy" "policy" {
     + arn         = (known after apply)
     + content     = jsonencode(
           {
             + Statement = [
                 + {
                     + Action    = "*"
                     + Condition = {
                         + StringLike = {
                             + aws:PrincipalArn = [
                                 + "arn:aws:iam::*:root",
                               ]
                           }
                       }
                     + Effect    = "Deny"
                     + Resource  = "*"
                   },
               ]
             + Version   = "2012-10-17"
           }
       )
     + description = "An SCP blocking the root user from taking any action, either via the console or programmatically"
     + id          = (known after apply)
     + name        = "lock-down-root-user"
     + type        = "SERVICE_CONTROL_POLICY"
   }

 # module.lock-down-root-user.aws_organizations_policy_attachment.attachment[0] will be created
 + resource "aws_organizations_policy_attachment" "attachment" {
     + id        = (known after apply)
     + policy_id = (known after apply)
     + target_id = "ou-deu2-jx8q5x1c"
   }

Plan: 2 to add, 0 to change, 0 to destroy.

# other output
➜  aws-organizations-example$ terraform apply
# other output

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

# other output

Conclusion

In this article we have shown how AWS Organizations service can be utilized to: a) automate accounts creation, b) group accounts together into Organizational Units, and c) apply policies to these groups for additional governance.

One other important feature of AWS Organizations is Consolidated Billing, which is described in AWS documentation as follows:

You can use AWS Organizations to set up a single payment method for all the AWS accounts in your organization through consolidated billing.

With consolidated billing, you can see a combined view of charges incurred by all your accounts, as well as take advantage of pricing benefits from aggregated usage, such as volume discounts for Amazon EC2 and Amazon S3.

…and just to recap, here’s what we’ve built:

The above diagram was crafted in Cloudcraft

And here’s the final file structure:

aws-organizations-example
├── main.tf
├── provider.tf
├── terraform.tfstate
├── terraform.tfstate.backup
├── .terraform
│   └── ... # Terraform's modules and plugins
├── iam-groups
│   ├── main.tf
│   └── variables.tf
├── iam-users
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── organizations
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── organizations-accounts
│   ├── main.tf
│   └── variables.tf
├── organizations-organizational_units
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── organizations-policies
│   ├── main.tf
│   └── variables.tf

“That’s all Folks!” Hope you enjoyed the read and until next time!