Mastering Cross-Account Terraform Modules

Unlocking Efficiency: Mastering Cross-Account Terraform Modules

In the world of Infrastructure as Code (IAC), Terraform has emerged as a powerful tool for provisioning and managing cloud resources across various cloud providers. Often, we encounter scenarios where we need to manage resources spanning multiple AWS accounts. In this blog post, we’ll explore how to create a versatile Terraform module that enables cross-account resource management by allowing you to pass credentials for multiple AWS accounts.

We have seen a lot of examples of how to create a terraform module but sometimes we need to interact with more than one account at once in order to fulfill the requirement. below are a few examples where you need a cross-account terraform module.

  • VPC peering between two VPC in different AWS accounts.
  • Creating an LB and its DNS record where LB and Route53 hosted zone are in different AWS accounts.
  • Transit gateway connections between VPC in different AWS accounts.
  • IAM role and SQS are in two separate AWS accounts.

The use cases mentioned above are only a few examples, cross account modules are not only limited to this. It can be implemented in any custom module as well where we need interaction between more than one account.

For this article, I will take an example of VPC peering. you can take reference from it and use it anywhere.

First of all, I will show you what a VPC peering module for a single account looks like.

Single account VPC peering module

Below is my provider.tf

    # provider.tf
    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 4.16"
        }
      }
      required_version = ">= 1.2.0"
    }
    
    provider "aws" {
      region  = "ap-south-1"
      profile = "my-first-account"
    }

In my provider.tf I have just provided information for one of my AWS account with the profile name my-first-account

Now Let’s see how I am calling the module:


    # vpc-peering.tf
    module "vpc-peering-same-account-1-account2" {
      source           = "../../modules/vpc-peering/same-account"
      requestor_vpc_id = "ENTER VPC ID OF THE FIRST VPC HERE"
      acceptor_vpc_id  = "ENTER VPC ID OF THE SECOND VPC HERE"
      peer_region      = "YOUR AWS REGION"
    }

In the above file I am just calling the VPC peering module and providing the information like VPC ID and aws region.

Now let’s see how things are looking inside the module. below is my variable.tf of the module.


    # variable.tf inside module
    variable "peer_region" {}
    variable "requestor_vpc_id" {}
    variable "acceptor_vpc_id" {}
    
    
    data "aws_vpc" "requestor" {
      id = var.requestor_vpc_id
    }
    
    data "aws_route_tables" "requestor" {
      vpc_id = var.requestor_vpc_id
    }
    
    data "aws_vpc" "acceptor" {
      id = var.acceptor_vpc_id
    }
    
    data "aws_route_tables" "acceptor" {
      vpc_id = var.acceptor_vpc_id
    }

In the above file, I am just declaring the variables that are required inside the module and I have also declared two data variables that will provide me the information of route tables.

Now Let’s see how my actual module file looks like. which is creating the VPC peering.


    # main.tf inside module
    # Requester's side of the connection.
    resource "aws_vpc_peering_connection" "peer" {
      peer_owner_id = data.aws_caller_identity.peer.account_id
      peer_region   = var.peer_region
      vpc_id        = var.requestor_vpc_id
      peer_vpc_id   = var.acceptor_vpc_id
      auto_accept   = false
    }
    
    # Accepter's side of the connection.
    resource "aws_vpc_peering_connection_accepter" "peer" {
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
      auto_accept               = true
    }
    
    #Module      : ROUTE REQUESTOR
    #Description : Create routes from requestor to acceptor.
    resource "aws_route" "requestor_vpc_peering" {
      count                     = length(data.aws_route_tables.requestor.ids)
      route_table_id            = tolist(data.aws_route_tables.requestor.ids)[count.index]
      destination_cidr_block    = data.aws_vpc.acceptor.cidr_block
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
    }
    
    #Module      : ROUTE ACCEPTOR
    #Description : Create routes from acceptor to requestor.
    resource "aws_route" "acceptor_vpc_peering" {
      count                     = length(data.aws_route_tables.acceptor.ids)
      route_table_id            = tolist(data.aws_route_tables.acceptor.ids)[count.index]
      destination_cidr_block    = data.aws_vpc.requestor.cidr_block
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
    }

The above module is doing 4 operations.

  • Initiating the VPC peering from one end.
  • Accepting the VPC peering from the other end.
  • Propagating the routes in the route table of the first VPC.
  • Propagating the routes in the route table of the second VPC.

Changes needed to convert this into a Cross-Account

#1 Update provider.tf with credentials of another account.

Earlier my provider.tf had credentials for only one AWS account. Now I have to update the same with the credentials of my second AWS account as well by adding the below snippet additionally into my provider.tf file


    provider "aws" {
        region  = "ap-south-1"
        profile = "my-second-account"
        alias   = "my-second-account"
      }

After adding the above snippet my final file will look something like this


    terraform {
        required_providers {
          aws = {
            source  = "hashicorp/aws"
            version = "~> 4.16"
          }
        }
        required_version = ">= 1.2.0"
      }
      
      provider "aws" {
        region  = "ap-south-1"
        profile = "my-first-account"
      }
      
      provider "aws" {
        region  = "ap-south-1"
        profile = "my-second-account"
        alias   = "my-second-account"
      }
#2 Provide credentials while calling the module

Now I will have to both credentials while calling my module by adding an additional providers block. See the updated file below.


    module "vpc-peering-cross-account-1-account2" {
        source           = "../../modules/vpc-peering/cross-account"
        requestor_vpc_id = "ENTER VPC ID OF THE FIRST ACCOUNT HERE"
        acceptor_vpc_id  = "ENTER VPC ID OF THE SECOND ACCOUNT HERE"
        peer_region      = "YOUR REGION"
        providers        = {
          aws.requestor = aws
          aws.acceptor  = aws.my-second-account
        }
      }

In the providers block I am passing two credentials with the name aws.requestor and aws.acceptor

In the requestor, I am using the default credentials with name aws only, and for the acceptor I am using aliased credentials with name aws.my-second-account

Now let’s jump on to the module and see how things have been changed there.

#3 Adding required_providers in variable.tf of module

While calling the module I am passing the two credentials with the name aws.requestor and aws.acceptor. Similarly, I will have to make the module aware that It should expect credentials for two different accounts with name requestor and acceptor this name requestor and acceptor are totally my choice, you can name them anything you want. This is totally up to you.

Now let’s see the updated variables.tf

 
    terraform {
        required_providers {
          aws = {
            source                = "hashicorp/aws"
            configuration_aliases = [aws.requestor, aws.acceptor]
          }
        }
      }
      
      variable "peer_region" {}
      variable "requestor_vpc_id" {}
      variable "acceptor_vpc_id" {}
      
      
      data "aws_vpc" "requestor" {
        provider = aws.acceptor
        id       = var.acceptor_vpc_id
      }
      
      data "aws_route_tables" "requestor" {
        provider = aws.requestor
        vpc_id   = var.requestor_vpc_id
      }
      
      data "aws_vpc" "acceptor" {
        provider = aws.acceptor
        id       = var.acceptor_vpc_id
      }
      
      data "aws_route_tables" "acceptor" {
        provider = aws.acceptor
        vpc_id   = var.acceptor_vpc_id
      }

Along with adding the required_provider block, I am also passing the provider information in the data variables that I was using to get the route table information.

Now we are only left with one last change

#4 Updating the module resources with the correct provider information

As we show how we have updated the data variables with the provider information so that the can use the credentials of the appropriate account. Similarly, we have updated our resource definitions so that they can also be created in the respective account. see the updated code snippet below.


    # Requester's side of the connection.
    resource "aws_vpc_peering_connection" "peer" {
      peer_owner_id = data.aws_caller_identity.peer.account_id
      peer_region   = var.peer_region
      vpc_id        = var.requestor_vpc_id
      peer_vpc_id   = var.acceptor_vpc_id
      auto_accept   = false
      provider      = aws.requestor
    }
    
    # Accepter's side of the connection.
    resource "aws_vpc_peering_connection_accepter" "peer" {
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
      auto_accept               = true
      provider                  = aws.acceptor
    }
    
    #Module      : ROUTE REQUESTOR
    #Description : Create routes from requestor to acceptor.
    resource "aws_route" "requestor_vpc_peering" {
      count                     = length(data.aws_route_tables.requestor.ids)
      route_table_id            = tolist(data.aws_route_tables.requestor.ids)[count.index]
      destination_cidr_block    = data.aws_vpc.acceptor.cidr_block
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
      provider                  = aws.requestor
    }
    
    #Module      : ROUTE ACCEPTOR
    #Description : Create routes from acceptor to requestor.
    resource "aws_route" "acceptor_vpc_peering" {
      count                     = length(data.aws_route_tables.acceptor.ids)
      route_table_id            = tolist(data.aws_route_tables.acceptor.ids)[count.index]
      destination_cidr_block    = data.aws_vpc.requestor.cidr_block
      vpc_peering_connection_id = aws_vpc_peering_connection.peer.id
      provider                  = aws.acceptor
    }

With this last change, I will be able to create a cross-account VPC peering in one go. I don’t have to go to two separate places in order to do the same.

Last Tip

You can use this cross-account module for a single account as well. you only have to pass the same credentials in place of both aws.requestor and aws.acceptor See the example below.


    module "vpc-peering-cross-account-1-account2" {
        source           = "../../modules/vpc-peering/cross-account"
        requestor_vpc_id = "ENTER VPC ID OF THE FIRST ACCOUNT HERE"
        acceptor_vpc_id  = "ENTER VPC ID OF THE SECOND ACCOUNT HERE"
        peer_region      = "YOUR REGION"
        providers        = {
          aws.requestor = aws
          aws.acceptor  = aws
        }
      }

References:

Refer to previous series on Terraform best practices

Get In Touch

East Delhi, New Delhi

connect@iopshub.com

+91 73038 37023