- Published on
Cross Account Terraform Assume Roles in AWS
- Authors
- Name
- Ruan Bekker
- @ruanbekker
In this tutorial we will go through a scenario where we use Terraform to write our remote state to a s3 bucket in our Tools Account, our main user is configured in our Management Account and we will deploy a SNS Topic in our Dev Account. We will accomplish this with assume roles.
Overview
This is how the overview will look like:
We will work with these example accounts:
- Management Acccount: 000000000001
- Tools Account: 000000000002
- Dev Account: 000000000003
Given the following terraform provider and backend configuration:
provider "aws" {
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
}
provider "aws" {
alias = "dev"
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
assume_role {
role_arn = "arn:aws:iam::000000000003:role/terraform-role"
}
}
terraform {
backend "s3" {
encrypt = true
bucket = "terraform-state-tools-account"
key = "example/terraform.tfstate"
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
assume_role = {
role_arn = "arn:aws:iam::000000000002:role/terraform-role"
}
}
}
We have the following:
- Default aws provider which uses the management account credentials.
- A dev aws provider, which uses the management account to authenticate and assumes a role from the dev account.
- To access the remote state, we use the management account to authenticate and assume a role from the tools account.
AWS IAM Roles
This is how the IAM User and IAM Roles are setup in order for the assume role calls to work.
Management Account
On the Master Account (000000000001) create a IAM User called terraform-user
, create a access key and secret key, then configure it in your aws cli credential provider:
aws --profile management configure
Assign a policy for the user that can used to allow assumes on target accounts: (dev and tools).
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAssumesToTerraformRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::000000000002:role/terraform-role",
"arn:aws:iam::000000000003:role/terraform-role"
]
}
]
}
Tools Account
On the Tools Account (000000000002) create a IAM Role called terraform-role
and define the trust relationship so that only the user from the management account can assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000001:user/terraform-user"
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}
And assign the arn:aws:iam::aws:policy/AdministratorAccess
policy to the role as we want this role to have full access. If we only wanted to assign s3 access for remote state, then we can remove the AdministratorAccess policy as it will still have access to the bucket due to the bucket policy.
We have a s3 bucket called terraform-state-tools-account
, which we need to assign bucket policy to allow the terraform role within this account to assume it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000001:role/terraform-role"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::terraform-state-tools-account"
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000001:role/terraform-role"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::terraform-state-tools-account/*"
}
]
}
Dev Account
On the dev account create a role terraform-role
and allow the terraform-user from the master account to assume from it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000001:user/terraform-user"
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}
And also define the arn:aws:iam::aws:policy/AdministratorAccess
policy as we want full access to this account.
Now your user should be able to assume the roles from the target account.
Terraform Example
In terraform we want to do the following:
- Define the default provider to use the management credentials (terraform-user).
- Access the bucket by referencing the management credentials, and assume the role in the tools account.
- Create the SNS topic in the dev account by referencing the management credentials and assume the role in the dev account.
In our providers.tf
, we are defining our default provider which will use the management account credentials, and we define a alias provider called dev
which will use the management account credentials, and then assume the role from the dev account to be able to make changes in the dev aws account.
terraform {
required_providers {
aws = {
version = ">= 5.66.0"
source = "hashicorp/aws"
}
}
}
provider "aws" {
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
}
provider "aws" {
alias = "dev"
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
assume_role {
role_arn = "arn:aws:iam::000000000003:role/terraform-role"
}
}
In our remote-state.tf
, we are defining the remote state aws s3 bucket and the key that it will write the terraform.tfstate
to as well as how it will authenticate, which will be gaining its credentials from the management profile and then assumes the role from the tools account.
terraform {
backend "s3" {
encrypt = true
bucket = "terraform-state-tools-account"
key = "dev/sns-example/terraform.tfstate"
region = "eu-west-1"
profile = "management"
shared_credentials_files = ["~/.aws/credentials"]
assume_role = {
role_arn = "arn:aws:iam::000000000002:role/terraform-role"
}
}
}
And in main.tf
, is where we will create the aws sns topic in the dev account, and we will also be outputting the aws account id of the management account just as demonstration that we can see its the management account.
data "aws_caller_identity" "management" {}
data "aws_caller_identity" "dev" {
provider = aws.dev
}
resource "aws_sns_topic" "dev_topic" {
provider = aws.dev
name = "dev-topic"
tags = {
account-id = data.aws_caller_identity.dev.account_id
}
}
output "account_id_management" {
value = data.aws_caller_identity.management.account_id
}
Then we can initialise and run a terraform plan:
terraform init
terraform plan
The output should look more or less like this:
Terraform will perform the following actions:
# aws_sns_topic.dev_topic will be created
+ resource "aws_sns_topic" "dev_topic" {
+ arn = (known after apply)
+ beginning_archive_time = (known after apply)
+ content_based_deduplication = false
+ fifo_topic = false
+ id = (known after apply)
+ name = "dev-topic"
+ name_prefix = (known after apply)
+ owner = (known after apply)
+ policy = (known after apply)
+ signature_version = (known after apply)
+ tags = {
+ "account-id" = "000000000003"
}
+ tags_all = {
+ "account-id" = "000000000003"
}
+ tracing_config = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ account_id_master = "000000000001"
Thank You
Thanks for reading, if you like my content, feel free to check out my website, and subscribe to my newsletter or follow me at @ruanbekker on Twitter.
- Linktree: https://go.ruan.dev/links
- Patreon: https://go.ruan.dev/patreon