Terraform and AWS Multi account Setup?


This article is focused on how to use AWS multiple account setup with Terraform. Terraform is a great tool for provisioning your cloud infrastructure but as you start using Terraform for managing your cloud infrastructure you will always feel a need for multiple AWS accounts that can cater to some specific environment such as - development, test, stage, and production.

If you are following DevOps practices for managing the AWS account then it is also recommended by AWS to use multiple accounts for managing your cloud infrastructure.

Benefits of Using Multiple AWS Accounts

  1. Granular Security - With multiple AWS accounts, you can create an AWS account for each environment (development, test, stage, production), organization level, or department.
  2. No need for one big AWS account - The other big benefit of using multiple AWS accounts is you do not need to maintain a single AWS account for managing your complete cloud infrastructure. Having a single AWS account poses more security risks for your overall cloud setup.
  3. Multiple AWS accounts are independent of each other - If you have a multiple AWS account setup then it is really easy for cloud practitioners to keep the AWS accounts separate from each other which eases the Creation and Deletion of AWS accounts.
  4. AWS Organizations - The other good reason for opting for a multi-account AWS setup is AWS Organization. It allows you to create AWS accounts with pre-sets of resources.
  5. Least Privileges - When you have multiple accounts then the administrator of one AWS account can not view or access another AWS account which by default implements the principle of least privileges.


But how to implement the multiple AWS Account concept using Terraform? -

There are a few ways to achieve this:

  1. By creating provider aliases
  2. Using Terraform workspaces
  3. AWS Organizations and Assume Role

1. By creating provider aliases

Let’s take an example to implement provider aliases for AWS multi-account setup using Terraform

1. First, define the provider -

1terraform {
2 required_providers {
3   aws = {
4     source  = "hashicorp/aws"
5     version = "~> 4.11.0"
6   }
7 }
8}

Each public cloud has a separate provider configuration which is used for API interactions and authentication. If you are using different public cloud providers such as Google Cloud or Azure then change the configuration by referring to their documentation.

2. Multiple provider configuration for multiple users -

Now after defining the required provider configuration we are going to write the terraform configuration for two accounts "dev" and "prod" using the same main.tf file.

 1# Development account
 2provider "aws" {
 3 profile = "dev"
 4 region  = var.region_dev
 5}
 6 
 7#Production account
 8provider "aws" {
 9 profile = "prod"
10 region  = var.region_prod
11 alias   = "prod"
12}


In the above configuration we are using two terraform variables “var.region_dev” and “var.region_prod” both the variables we are going to define separately into the “variables.tf” file.

Variables.tf

Here are variables.tf file which you need for this example -

1variable "region_dev" {
2 type    = string
3 default = "eu-central-1"
4}
5 
6variable "region_prod" {
7 type    = string
8 default = "eu-central-1"
9}

As you can see we have defined two string type terraform variables that contain the region where terraform is going to apply the configuration.

3. Mapping AWS resources to the respective AWS account

In the previous step we have created the aliases for DEV and PROD environments, let’s try to map resources that we want to provision in DEV as well as in PROD environments.

To make the example easy we will create two S3 Bucket one in DEV and another in the PROD environment.

 1resource "aws_s3_bucket" "dev_s3_bucket" {
 2   bucket = "S3-Dev-Bucket"
 3   acl    = "public-read"
 4   lifecycle {
 5       prevent_destroy = true
 6   }
 7}
 8 
 9resource "aws_s3_bucket" "prod_s3_bucket" {
10   provider = aws.prod
11   bucket   = "S3-Prod-Bucket"
12   acl      = "public-read"
13   lifecycle {
14       prevent_destroy = true
15   }
16}

The only difference between DEV and PROD S3 resource buckets is - In the second S3 bucket for PROD we are defining one more provider which refers to the alias aws.prod.

The same approach can be taken to provision other AWS resources in the multiple AWS accounts using the aliases.



2. Using Terraform workspaces

Terraform provides workspaces for managing the Development, Test, Stage, and production environment. Terraform workspaces is a command-line utility that can be used after installing the terraform.

Use Case

To get a better understanding of Terraform workspace for managing the multiple accounts we will again take the same environments -

  1. DEV
  2. PROD

And in both environments, we are going to create an EC2 instance.

1. Let’s create DEV and PROD workspaces

Before we start writing the terraform configuration let’s first create terraform workspaces for DEV as well as PROD.

Run the following terraform command for DEV workspace creation

1$ terraform workspace new dev

Run the following terraform command for PROD workspace creation

1$ terraform workspace new prod

2. Select and Use workspace After creating the workspaces the next challenge would be how to use the workspaces. The most important rule over here is - “You can only use one active workspace at a time”. To do that use the following commands -

For using DEV workspace -

1$ terraform workspace select dev

For using the PROD workspace -

1$ terraform workspace select prod

3. Create EC2 instance in DEV as well as PROD using workspace Here is our basic Terraform configuration for a provisioning EC2 instances in AWS using terraform workspace -

 1provider "aws" {
 2  region     = "eu-central-1"
 3}
 4 
 5locals {
 6 instance_name = "${terraform.workspace}-instance"
 7}
 8 
 9resource "aws_instance" "ec2_example" {
10   ami = "ami-0767046d1677be5a0"
11   instance_type = var.instance_type
12   tags = {
13     Name = local.instance_name
14   }
15}

The important part of the above terraform configuration is the locals block -

1locals {
2 instance_name = "${terraform.workspace}-instance"
3}

When you create and use terraform workspace then you can use the name of the workspace inside your terraform configuration using the interpolation - ${terraform.workspace}.

The above-mentioned terraform interpolation will assign the workspace name to the EC2 instance name so that it will be easy for you to identify the DEV and PROD resources.

(*Note - Always keep the AWS credentials for DEV and PROD accounts separate from each other)



3. AWS Organizations and Assume Role

1. Create AWS organization

AWS has a really good feature known as AWS organization which can be used for multi-account setup using terraform.

Let's create an "AWS Organization" -

1provider "aws" {
2   region = "eu-central-1"
3}
4 
5resource "aws_organizations_organization" "organization" {
6}

2. Create DEV and PROD account

Now we will create two accounts DEV(Development environment) and PROD(Production environment) but you can create as many as you can be based on your needs. Apart from that, we are also going to create a USERS account for managing the users.

 1resource "aws_organizations_account" "USERS" {
 2 name      = "spacelift-users"
 3 email     = "users@test.com"
 4 role_name = "admin"
 5}
 6 
 7resource "aws_organizations_account" "DEV" {
 8 name      = "spacelift-dev"
 9 email     = "dev@test.com"
10 role_name = "admin"
11}
12 
13resource "aws_organizations_account" "PROD" {
14 name      = "spacelift-production"
15 email     = "prod@test.com"
16 role_name = "admin"
17}

(*Note - You need to use separate email ids for each account because AWS Organization does not allow you have the same email id associated with other accounts)

3. Create provider alias for USERS, DEV and PROD

As we aim to implement a multi-account AWS setup using terraform so we need to define provider blocks for each account using an alias.

 1provider "aws" {
 2 assume_role {
 3   role_arn = "arn:aws:iam::${aws_organizations_account.USERS.id}:role/admin"
 4 }
 5 
 6 alias  = "USERS"
 7 region = "eu-central-1"
 8}
 9 
10provider "aws" {
11 assume_role {
12   role_arn = "arn:aws:iam::${aws_organizations_account.DEV.id}:role/admin"
13 }
14 
15 alias  = "DEV"
16 region = "eu-central-1"
17}
18 
19provider "aws" {
20 assume_role {
21   role_arn = "arn:aws:iam::${aws_organizations_account.PROD.id}:role/admin"
22 }
23 
24 alias  = "PROD"
25 region = "eu-central-1"
26}


4. IAM Groups, Roles, and Policy for the Accounts

As we have created USERS, DEV, and PROD accounts we are going to use USERS account for managing the users, and for that, we need to create some IAM Roles, groups, and Policies.

Let's first create IAM Group, Policy, and user for self-managing user so that after onboarding users should be able to manage their security credentials, password, and MFA(Multi-Factor Authentication) Secrets

 1resource "aws_iam_group" "manage_user_group" {
 2 name = "ManageUsers"
 3 
 4 provider = aws.USERS
 5}
 6 
 7resource "aws_iam_group_policy_attachment" "iam_read_only_access_policy" {
 8 group      = aws_iam_group.manage_user_group.name
 9 policy_arn = "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
10 
11 provider = aws.USERS
12}
13 
14resource "aws_iam_group_policy_attachment" "iam_self_manage_service_specific_credentials" {
15 group      = aws_iam_group.manage_user_group.name
16 policy_arn = "arn:aws:iam::aws:policy/IAMSelfManageServiceSpecificCredentials"
17 
18 provider = aws.USERS
19}
20 
21resource "aws_iam_group_policy_attachment" "iam_user_change_password" {
22 group      = aws_iam_group.manage_user_group.name
23 policy_arn = "arn:aws:iam::aws:policy/IAMUserChangePassword"
24 
25 provider = aws.USERS
26}
27 
28resource "aws_iam_policy" "self_manage_vmfa" {
29 name   = "SelfManageVMFA"
30 policy = file("${path.module}/data/mfa_policy.json")
31 
32 provider = aws.USERS
33}
34 
35resource "aws_iam_group_policy_attachment" "self_manage_vmfa" {
36 group      = aws_iam_group.manage_user_group.name
37 policy_arn = aws_iam_policy.self_manage_vmfa.arn
38 
39 provider = aws.USERS
40}

Here is a policy file used in the above terraform block. i.e. mfa_policy.json

 1{
 2 "Version": "2020-12-05",
 3 "Statement": [
 4   {
 5     "Effect": "Allow",
 6     "Action": [
 7       "iam:CreateVirtualMFADevice",
 8       "iam:EnableMFADevice",
 9       "iam:ResyncMFADevice",
10       "iam:DeleteVirtualMFADevice"
11     ],
12     "Resource": [
13       "arn:aws:iam::*:mfa/${aws:username}",
14       "arn:aws:iam::*:user/${aws:username}"
15     ]
16   },
17   {
18     "Sid": "AllowUsersToDeactivateTheirOwnVirtualMFADevice",
19     "Effect": "Allow",
20     "Action": [
21       "iam:DeactivateMFADevice"
22     ],
23     "Resource": [
24       "arn:aws:iam::*:mfa/${aws:username}",
25       "arn:aws:iam::*:user/${aws:username}"
26     ],
27     "Condition": {
28       "Bool": {
29         "aws:MultiFactorAuthPresent": "true"
30       }
31     }
32   },
33   {
34     "Effect": "Allow",
35     "Action": [
36       "iam:ListMFADevices",
37       "iam:ListVirtualMFADevices",
38       "iam:ListUsers"
39     ],
40     "Resource": "*"
41   }
42 ]
43}

5. Setup Administrator Role for production environment

In the previous step, we have set up the self-managed IAM policy, Group, and Role. Now we need to set up some PRODUCTION level access and it should be catered to developers who are working on the development and maintenance of the application.

First, create a developer role and for that, we are gonna put the following terraform configuration inside a [module] 11 names developer

Path - modules/developer-role/main.tf

 1variable "trusted_entity" {
 2 type = string
 3}
 4 
 5resource "aws_iam_role" "this" {
 6 name = "Developer"
 7 
 8 assume_role_policy = data.template_file.trust_relationship.rendered
 9}
10 
11data "template_file" "trust_relationship" {
12 template = <<TEMPLATE
13{
14 "Version": "2012-10-17",
15 "Statement": [
16   {
17     "Effect": "Allow",
18     "Principal": {
19       "AWS": "${trusted_entity}"
20     },
21     "Action": "sts:AssumeRole",
22     "Condition": {
23       "Bool": {
24         "aws:MultiFactorAuthPresent": "true"
25       }
26     }
27   }
28 ]
29}
30TEMPLATE
31 
32 vars = {
33   trusted_entity = var.trusted_entity
34 }
35}
36 
37resource "aws_iam_role_policy_attachment" "administrator_access" {
38 policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
39 role       = aws_iam_role.this.name
40}
41 
42output "role_arn" {
43 value = aws_iam_role.this.arn
44}

Now create one more module named developer-group and users of this group are allowed to assume the roles.

Path - modules/developer-group/main.tf

 1variable "group_name" {
 2 type = string
 3}
 4 
 5variable "assume_role_arns" {
 6 type = list(string)
 7}
 8 
 9resource "aws_iam_group" "this" {
10 name = var.group_name
11}
12 
13resource "aws_iam_policy" "assume_role" {
14 name   = "${var.group_name}AssumeRole"
15 policy = data.template_file.assume_role.rendered
16}
17 
18data "template_file" "assume_role" {
19 template = <<TEMPLATE
20{
21 "Version": "2012-10-17",
22 "Statement": {
23   "Effect": "Allow",
24   "Action": "sts:AssumeRole",
25   "Resource": ${roles_arns_json}
26 }
27}
28TEMPLATE
29 
30 vars = {
31   roles_arns_json = jsonencode(var.assume_role_arns)
32 }
33}
34 
35resource "aws_iam_group_policy_attachment" "assume_role" {
36 group      = aws_iam_group.this.name
37 policy_arn = aws_iam_policy.assume_role.arn
38}
39 
40output "group_name" {
41 value = aws_iam_group.this.name
42}

6. IAM user account

So till now, we have set up aws_organizations_account, aws_provider aliases, IAM group, IAM policy, and IAM roles. Now we will create actual users with the help of the terraforming module. Here is a terraform configuration for the user module -

Path - modules/user/main.tf

 1variable "name" {
 2 type = string
 3}
 4 
 5variable "pgp_key" {
 6 type = string
 7}
 8 
 9variable "groups" {
10 type = list(string)
11}
12 
13resource "aws_iam_user" "this" {
14 name = var.name
15}
16 
17resource "aws_iam_user_login_profile" "this" {
18 user    = aws_iam_user.this.name
19 pgp_key = var.pgp_key
20}
21 
22resource "aws_iam_access_key" "this" {
23 user    = aws_iam_user.this.name
24 pgp_key = var.pgp_key
25}
26 
27resource "aws_iam_user_group_membership" "this" {
28 user = aws_iam_user.this.name
29 
30 groups = var.groups
31}
32 
33output "summary" {
34 value = {
35   name              = var.name
36   password          = aws_iam_user_login_profile.this.encrypted_password
37   access_key_id     = aws_iam_access_key.this.id
38   secret_access_key = aws_iam_access_key.this.encrypted_secret
39 }
40}

7. Adding a User In the previous step we have defined the user module, let’s use the same module to create a user with production access-

 1module "test_user" {
 2 source  = "modules/user"
 3 name    = "test.user"
 4 pgp_key = "lkjsdfuLKASDEGBDEOPNBSG..." # public key
 5 
 6 groups = [
 7   aws_iam_group.self_managing.name,
 8   module.developer_group_production.group_name
 9 ]
10 
11 providers = {
12   aws = aws.users
13 }
14}
15 
16output "user_details" {
17 value = [
18   module.test_user.summary
19 ]
20}

Once you apply the above user configuration you should see the following information of the generated user -

1user_details = [
2 {
3   "access_key_id" = "AKIDLSJDFGLKANS324"
4   "name" = "test.user"
5   "password" = "asldjfOIUASDHF7823HJDG..."
6   "secret_access_key" = "KLSDFJO84GNVBSU..."
7 },
8]

Note - Click here to read more on How to manage AWS IAM user, Roles and Policies with Terraform

Conclusion

I hope the above steps would help you to set up multiple AWS accounts using terraform. Also you might have some different steps and procedures for implementing the multiple AWS accounts because there is no strict rules on handling the AWS account with terraform.


Read More - Terragrunt -

  1. How to use Terragrunt?

Posts in this Series