Introduction
Maintaining accounts with Terraform ensures a single source of truth about what changes we made for our principals and recommended policies based on organization guidelines. We can also structure and manage the changes for our IAM Identity Center Multi-account Permission declaratively with the help of Hashicorp Configuration Language (HCL). Either a long-time or newly hired operations engineer can change quickly.
Quick Demo
Recorded with asciinema asciinema
Requirements
AWS Organization
Ensure we have at least two AWS Organization Accounts in a different environment.
AWS Organization Account | Description |
---|
Operations Management | Our main AWS Account where we manage our AWS Organization Accounts. |
Development | The Account for Development, QA, and Testing. |
Production | The Account for Production Workloads. |
IAM Identity Center Users
A user with <NO GROUP>
could be independently set or under other groups, such as qualityassurance
, or a combination of any existing groups in our user’s table.
Instead of using Vanilla Terraform, we will use a Terraform Framework called Terraspace. Terraspace helps us organize the resources by stacks, keeping them organized and manageable at the same time.
- Terraform
- Terraspace
- Git
AWS IAM Account
Create an AWS IAM Account with Programmatic Access having the following managed permissions policies attached.
AmazonS3FullAccess
- Provides full access to all buckets.AmazonDynamoDBFullAccess
- Provides full access to Amazon DynamoDB.IAMFullAccess
- Provides full access to IAM.AWSSSOMasterAccountAdministrator
- Provides access within AWS SSO to manage AWS Organizations master and member accounts and cloud application.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| aws iam create-user --user-name provisioner
# {
# "User": {
# "Path": "/",
# "UserName": "provisioner",
# "UserId": "XXXXXXXXXXXXXXXXXXXX",
# "Arn": "arn:aws:iam::< AWS ACCOUNT ID >:user/provisioner",
# "CreateDate": "2023-02-23T11:25:55+00:00"
# }
# }
aws iam create-access-key --user-name provisioner
# {
# "AccessKey": {
# "UserName": "provisioner",
# "AccessKeyId": "< AWS_ACCESS_KEY_ID >",
# "Status": "Active",
# "SecretAccessKey": "< AWS_SECRET_ACCESS_KEY >",
# "CreateDate": "2023-02-23T11:31:51+00:00"
# }
# }
aws iam attach-user-policy --user-name provisioner \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
aws iam attach-user-policy --user-name provisioner \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam attach-user-policy --user-name provisioner \
--policy-arn arn:aws:iam::aws:policy/AWSSSODirectoryAdministrator
aws iam attach-user-policy --user-name provisioner \
--policy-arn arn:aws:iam::aws:policy/IAMFullAccess
aws iam list-attached-user-policies --user-name provisioner
# {
# "AttachedPolicies": [
# {
# "PolicyName": "IAMFullAccess",
# "PolicyArn": "arn:aws:iam::aws:policy/IAMFullAccess"
# },
# {
# "PolicyName": "AWSSSOMasterAccountAdministrator",
# "PolicyArn": "arn:aws:iam::aws:policy/AWSSSOMasterAccountAdministrator"
# },
# {
# "PolicyName": "AmazonDynamoDBFullAccess",
# "PolicyArn": "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
# },
# {
# "PolicyName": "AmazonS3FullAccess",
# "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3FullAccess"
# }
# ]
# }
|
Load the AWS IAM Credentials to our System Profile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| aws configure --profile provisioner
# AWS Access Key ID [None]: < AWS_ACCESS_KEY_ID >
# AWS Secret Access Key [None]: < AWS_SECRET_ACCESS_KEY >
# Default region name [None]: us-east-1
# Default output format [None]: json
export AWS_PROFILE=provisioner
aws sts get-caller-identity
# {
# "UserId": "XXXXXXXXXXXXXXXXXXXX",
# "Account": "< AWS ACCOUNT ID >",
# "Arn": "arn:aws:iam::< AWS ACCOUNT ID >:user/provisioner"
# }
|
Our AWS IAM User for Terraform provisioner
is now ready to use.
Multi-Account Permissions Project
Please create a new project where our Infrastructure is a Code definition for Terraform.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| terraspace new project aws-iam-ic-multi-account-permissions-example --plugin aws
cd aws-iam-ic-multi-account-permissions-example
# Makes sure we have the pregenerated files listed.
tree -L 2
# .
# ├── Gemfile
# ├── Gemfile.lock
# ├── README.md
# ├── Terrafile
# ├── app
# │ ├── modules
# │ └── stacks
# └── config
# ├── app.rb
# └── terraform
|
Prepare the Modules
To reduce the IaC bloating and maintains the simplicity, we will be using a reusable module from CloudPosse.
Install the SSO Module
1
2
| echo -e 'mod "sso", source: "cloudposse/sso/aws", version: "0.7.1"' >> Terrafile
terraspace bundle
|
Terraspace Stack for the Permission Sets
1
| terraspace new stack permission-sets
|
Terraspace Stacks for our AWS Accounts
1
2
3
| terraspace new stack operations
terraspace new stack development
terraspace new stack production
|
Download the Cloudposse Modules Context File
1
2
3
| ls ./app/stacks/ | xargs -I {} \
curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf \
-o ./app/stacks/{}/context.tf
|
Cloudposse context files use these variables and form proper resource naming conventions.
1
2
3
4
5
6
7
| ls ./app/stacks/ | xargs -I {} \
sh -c 'mkdir -p ./app/stacks/{}/tfvars && tee ./app/stacks/{}/tfvars/base.tfvars <<EOF
enabled = true
namespace = "aws-sso"
name = "aws-sso"
stage = "<%= expansion(':ENV') %>"
EOF'
|
These Terraform outputs will be variables for our AWS accounts and principal assignment.
1
2
3
4
5
6
| tee ./app/stacks/permission-sets/outputs.tf <<EOF
output "permission_sets" {
description = "Show All Permission Sets"
value = module.permission_sets.permission_sets
}
EOF
|
AWS accounts and principal assignments require permission set via outputs.
1
2
3
4
| ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee -a ./app/stacks/{}/tfvars/base.tfvars <<EOF
permission_sets = <%= output("permission-sets.permission_sets") %>
EOF'
|
Applying any Terraform action shows the output details, so we can review or validate that everything applies correctly.
1
2
3
4
5
6
7
| ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee -a ./app/stacks/{}/outputs.tf <<EOF
output "assignments" {
description = "Show All Account Assignments for a Permission Set"
value = module.sso_account_assignments.assignments
}
EOF'
|
Create a an variables for our Permission Sets and AWS Accounts
1
2
3
4
5
6
7
| ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee ./app/stacks/{}/variables.tf <<EOF
variable "permission_sets" {
}
variable "aws_account_{}" {
}
EOF'
|
Principals and Accounts Management
Create a Permission Sets Resource
First, create an example of an inline policy for one of our Permission Sets
1
2
3
4
5
6
7
8
9
10
11
12
13
| tee ./app/stacks/permission-sets/inline_policy_S3Access.tf <<EOF
data "aws_iam_policy_document" "inline_policy_S3Acccess" {
statement {
sid = "1"
actions = ["*"]
resources = [
"arn:aws:s3:::*",
]
}
}
EOF
|
Permission Sets Resource Definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| tee ./app/stacks/permission-sets/main.tf <<EOF
module "permission_sets" {
source = "../../modules/sso/modules/permission-sets"
context = module.this.context
permission_sets = [
{
name = "AdministratorAccess",
description = "Provides full access to AWS services and resources.",
relay_state = "",
session_duration = "PT12H",
tags = {},
inline_policy = "",
policy_attachments = [
"arn:aws:iam::aws:policy/AdministratorAccess",
]
customer_managed_policy_attachments = []
},
{
name = "PowerUserAccess",
description = "Provides full access to AWS services and resources, but does not allow management of Users and groups.",
relay_state = "",
session_duration = "PT12H",
tags = {},
inline_policy = "",
policy_attachments = [
"arn:aws:iam::aws:policy/PowerUserAccess",
]
customer_managed_policy_attachments = []
},
{
name = "ReadOnlyAccess"
description = "Provides read-only access to AWS services and resources."
relay_state = ""
session_duration = "PT12H"
tags = {}
inline_policy = data.aws_iam_policy_document.inline_policy_S3Acccess.json
policy_attachments = [
"arn:aws:iam::aws:policy/ReadOnlyAccess",
]
customer_managed_policy_attachments = []
},
]
}
EOF
|
At this point, we completed the Permission Sets stack creation and can initially apply it if we want.
1
2
| terraspace plan permission-sets
terraspace up permission-sets
|
Create an Accounts Assignments Resources
Operations Account Resource Definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| tee ./app/stacks/operations/main.tf <<EOF
module "sso_account_assignments" {
source = "../../modules/sso/modules/account-assignments"
context = module.this.context
account_assignments = [
############
## GROUPS ##
############
{
principal_name = "administrator"
principal_type = "GROUP"
permission_set_name = "AdministratorAccess"
permission_set_arn = var.permission_sets["AdministratorAccess"].arn
account = var.aws_account_operations
},
###########
## USERS ##
###########
{
principal_name = "[email protected]"
principal_type = "USER"
permission_set_name = "AdministratorAccess"
permission_set_arn = var.permission_sets["AdministratorAccess"].arn
account = var.aws_account_operations
},
]
}
EOF
|
Plan and apply for Operations Account.
1
2
| terraspace plan operations
terraspace up operations
|
Development Account Resource Definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| tee ./app/stacks/development/main.tf <<EOF
module "sso_account_assignments" {
source = "../../modules/sso/modules/account-assignments"
context = module.this.context
account_assignments = [
############
## GROUPS ##
############
{
principal_name = "administrator"
principal_type = "GROUP"
permission_set_name = "AdministratorAccess"
permission_set_arn = var.permission_sets["AdministratorAccess"].arn
account = var.aws_account_development
},
{
principal_name = "developer"
principal_type = "GROUP"
permission_set_name = "PowerUserAccess"
permission_set_arn = var.permission_sets["PowerUserAccess"].arn
account = var.aws_account_development
},
###########
## USERS ##
###########
{
principal_name = "[email protected]"
principal_type = "USER"
permission_set_name = "PowerUserAccess"
permission_set_arn = var.permission_sets["PowerUserAccess"].arn
account = var.aws_account_development
},
{
principal_name = "[email protected]"
principal_type = "USER"
permission_set_name = "ReadOnlyAccess"
permission_set_arn = var.permission_sets["ReadOnlyAccess"].arn
account = var.aws_account_development
},
]
}
EOF
|
Plan and apply for Development Account.
1
2
| terraspace plan development
terraspace up development
|
Production Account Resource Definition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| tee ./app/stacks/production/main.tf <<EOF
module "sso_account_assignments" {
source = "../../modules/sso/modules/account-assignments"
context = module.this.context
account_assignments = [
############
## GROUPS ##
############
{
principal_name = "administrator"
principal_type = "GROUP"
permission_set_name = "AdministratorAccess"
permission_set_arn = var.permission_sets["AdministratorAccess"].arn
account = var.aws_account_production
},
{
principal_name = "developer"
principal_type = "GROUP"
permission_set_name = "ReadOnlyAccess"
permission_set_arn = var.permission_sets["ReadOnlyAccess"].arn
account = var.aws_account_production
},
###########
## USERS ##
###########
{
principal_name = "[email protected]"
principal_type = "USER"
permission_set_name = "AdministratorAccess"
permission_set_arn = var.permission_sets["AdministratorAccess"].arn
account = var.aws_account_production
},
]
}
EOF
|
Plan and apply for Production Account.
1
2
| terraspace plan production
terraspace up production
|
Stacks Final Directory Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| tree -L 3 ./app/stacks
# ./app/stacks
# ├── development
# │ ├── context.tf
# │ ├── main.tf
# │ ├── outputs.tf
# │ ├── tfvars
# │ │ └── base.tfvars
# │ └── variables.tf
# ├── operations
# │ ├── context.tf
# │ ├── main.tf
# │ ├── outputs.tf
# │ ├── tfvars
# │ │ └── base.tfvars
# │ └── variables.tf
# ├── permission-sets
# │ ├── context.tf
# │ ├── inline_policy_S3Access.tf
# │ ├── main.tf
# │ ├── outputs.tf
# │ ├── tfvars
# │ │ └── base.tfvars
# │ └── variables.tf
# └── production
# ├── context.tf
# ├── main.tf
# ├── outputs.tf
# ├── tfvars
# │ └── base.tfvars
# └── variables.tf
|
Plan and Apply All Stacks
We can also plan and apply at once the entire stacks, as you have seen in the Multi-Account Terraform Quick Action Demo.
1
2
| terraspace all plan
terraspace all up
|
Access Portal Example Scenarios
Based on our Terraform resource accounts assignments.
User John Doe
under the administrator
group.
The administrator
group is assigned to all accounts
User Sarah Jane
under the operations
group.
The operations
group is not assigned, however we specifically assigned the user.
User Marcello Kelley
and Miliana Smith
under the developer
group.
The developer
group members are assigned to specific accounts with different access levels.
User Antonia Morris
is under no group.
The user is assigned to specific account with its own access level.
Automate using Github Actions
GitHub Actions Workflow
Create a GitHub Actions Workflow File
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| mkdir -p ./.github/workflows && tee ./.github/workflows/apply-assignments.yml <<EOF
name: "Apply Assignments for Accounts and Permission Sets"
on:
push:
branches:
- main
env:
AWS_REGION: "us-east-1"
AWS_ACCESS_KEY_ID: \${{ secrets.PROVISIONER_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: \${{ secrets.PROVISIONER_AWS_SECRET_ACCESS_KEY }}
AWS_ACCOUNT_ID: \${{ secrets.OPERATIONS_AWS_ACCOUNT_ID }}
TS_ENV: "dev"
TF_VAR_aws_account_operations: \${{ secrets.OPERATIONS_AWS_ACCOUNT_ID }}
TF_VAR_aws_account_development: \${{ secrets.DEVELOPMENT_AWS_ACCOUNT_ID }}
TF_VAR_aws_account_production: \${{ secrets.PRODUCTION_AWS_ACCOUNT_ID }}
jobs:
apply-assignments:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
- name: Install Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_wrapper: false
terraform_version: 1.3.9
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1.2'
- name: Install Terraspace
run: |
bundle install
- name: Download Terraform Modules
run: |
terraspace bundle
- name: Terraspace All Init
run: |
terraspace all init -y
- name: Terraspace All Up
run: |
terraspace all up -y
- name: Upload Logs to Artifacts
uses: actions/upload-artifact@v3
if: always()
with:
name: terraspace-logs
path: ./log/*
EOF
|
Create a Secrets in our GitHub Repository
1
2
3
4
5
| PROVISIONER_AWS_ACCESS_KEY_ID
PROVISIONER_AWS_SECRET_ACCESS_KEY
OPERATIONS_AWS_ACCOUNT_ID
DEVELOPMENT_AWS_ACCOUNT_ID
PRODUCTION_AWS_ACCOUNT_ID
|
GitHub Actions Workflow Page
At this point, we can also automate the process using GitHub Actions. Without the locally installed Operations Tools, we maximized the cloud resource.
Final GitHub Project Repository
You can download the final and working example project repository at this link https://github.com/nathanielvarona/aws-iam-ic-multi-account-permissions-example.