Published on

Cross Account Terraform Assume Roles in AWS

Authors

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:

image

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:

  1. Default aws provider which uses the management account credentials.
  2. A dev aws provider, which uses the management account to authenticate and assumes a role from the dev account.
  3. 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.

Join my Newsletter?
Buy Me A Coffee