Cross-Account VPC Peering Connection With Terraform

Chandara Chea
5 min readJun 7, 2020

A VPC (Virtual Private Cloud) peering connection is a networking connection between two VPCs that allows traffic between them using private IPv4 addresses or IPv6 addresses. VPC peering connection can be created between own VPCs, or with a VPC in another AWS account.

A VPC peering connection helps to facilitate the transfer of data. Instances in either VPC can communicate with each other as if they are within the same network. For example, if you have more than one AWS account, you can peer the VPCs across those accounts to create a file sharing network. You can also use a VPC peering connection to allow other VPCs to access resources you have in one of your VPCs.

In this article, I’m going to talk about how I setup VPC peering connection on AWS by using Terraform.

I do not own this image. Credit to image owner.

Setup

In this scenario, we are having 3 VPCs:

  • VPC A for shared resources (e.g VPN , etc)
  • VPC B for dev environment
  • VPC C for test environment

We are going to setup a central VPC (VPC A), and a VPC peering connection between VPC A and VPC B, and between VPC A and VPC C. All VPCs are in the different AWS accounts, and do not have overlapping CIDR blocks.

Let’s assume that our VPC B is a dev environment VPC and VPC C is a test environment VPC, which we have already created. Each VPC has 3 subnets: public, private and database subnets.

  • dev CIDRs : 10.20.0.0/16
  • test CIDRs: 10.10.0.0/16

And now we want to crate VPC A ,which is the root environment (for shared resources), and create VPC peering connection from the root environment to our dev and test environment VPCs. The root environment that we are going to create has CIDRs 10.50.0.0/16

Project Structure

main.tf
terraform.tf
outputs.tf
variables.tf
modules/
|-- vpc-peering/
|-- main.tf
|-- variables.tf

main.tf

I use 3 providers here because each VPC resides in different AWS accounts. Terraform commands will be run from root profile (where VPC is located), so it needs assume_role in other profiles to be able to access resources in VPC B and VPC C.

provider "aws" {
region = var.region
}
provider "aws" {
alias = "root"
region = var.region
shared_credentials_file = "$Home/.aws/credentials" # -->replace with your path to .aws/credentials
version = "~> 2.13"
profile = "root"
}
provider "aws" {
alias = "dev"
region = var.region
shared_credentials_file = "$Home/.aws/credentials"
version = "~> 2.13"
assume_role {
role_arn = "arn:aws:iam::############:role/Admin"
}
}
provider "aws" {
alias = "test"
region = var.region
shared_credentials_file = "$Home/.aws/credentials"
version = "~> 2.13"
assume_role {
role_arn = "arn:aws:iam::::############::role/Admin"
}
}

Use aws_caller_identity to get the access to the effective Account ID, User ID, and ARN in which Terraform is authorized.

data "aws_caller_identity" "dev" {
provider = aws.dev
}
data "aws_caller_identity" "test" {
provider = aws.test
}

Create root VPC by using shared module from AWS

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = var.vpc_name
cidr = var.cidrs[terraform.workspace]
azs = var.availability_zones
private_subnets = var.private_subnets[terraform.workspace]
public_subnets = var.public_subnets[terraform.workspace]
intra_subnets = var.database_subnets[terraform.workspace]
enable_dns_support = true
enable_dns_hostnames = true
database_subnet_group_tags = {
Name = "DB subnet group"
Environment = terraform.workspace
}
enable_nat_gateway = true
enable_vpn_gateway = false
single_nat_gateway = true
tags = {
Terraform = "true"
Environment = terraform.workspace
}
}

To have VPC peering connection, normally we need to create a VPC connection from requester side and we need to accept the connection from acceptor side. However, if we are doing this with terraform, terraform will automatically handle the connection on acceptor side, so we don’t need to accept the request manually.

modules/vpc-peering/main.tf

provider "aws" {
alias = "src"
}
provider "aws" {
alias = "dst"
}
### acceptor route tabledata "aws_route_table" "acceptor_rtb" {
provider = aws.dst
vpc_id = var.peer_vpc_id
filter {
name = "tag:Name"
values = [var.acceptor_intra_subnet_name]
}
}
### Requester's side of the connection.resource "aws_vpc_peering_connection" "requester_connection" {
vpc_id = var.vpc_id
peer_vpc_id = var.peer_vpc_id
peer_owner_id = var.peer_owner_id
peer_region = var.peer_region
auto_accept = false
tags = {
Side = "Requester"
}
}
### Accepter's side of the connection.resource "aws_vpc_peering_connection_accepter" "accepter_connection" {
provider = aws.dst
vpc_peering_connection_id = aws_vpc_peering_connection.requester_connection.id
auto_accept = true
tags = {
Side = "Accepter"
}
}
### Requestor routeresource "aws_route" "requestor_route" {
route_table_id = var.route_table_id
destination_cidr_block = var.acceptor_cidr_block
vpc_peering_connection_id = aws_vpc_peering_connection.requester_connection.id
}
### Accepter routeresource "aws_route" "accepter_route" {
provider = aws.dst
route_table_id = data.aws_route_table.acceptor_rtb.route_table_id
destination_cidr_block = var.requestor_cidr_block
vpc_peering_connection_id = aws_vpc_peering_connection.requester_connection.id
}

modules/vpc-peering/variables.tf

variable "vpc_id" {}variable "peer_vpc_id" {}variable "peer_owner_id" {}variable "peer_region" {}variable "acceptor_cidr_block" {}variable "route_table_id" {}variable "requestor_cidr_block" {}variable "acceptor_intra_subnet_name" {}

Add vpc-peering module to main.tf

main.tf

module root_vpc_to_dev {
source = "./modules/vpc_peering"
peer_region = var.region
vpc_id = module.vpc.vpc_id
peer_vpc_id = var.vpc_ids["dev"]
peer_owner_id = data.aws_caller_identity.dev.account_id
acceptor_cidr_block = var.cidrs["dev"]
requestor_cidr_block = var.cidrs["root"]
route_table_id = module.vpc.public_route_table_ids[0]
acceptor_intra_subnet_name = var.acceptor_intra_subnet_names["dev"]
providers = {
aws.src = aws.root
aws.dst = aws.dev
}
}
module root_vpc_to_test {
source = "./modules/vpc_peering"
peer_region = var.region
vpc_id = module.vpc.vpc_id
peer_vpc_id = var.vpc_ids["test"]
peer_owner_id = data.aws_caller_identity.test.account_id
acceptor_cidr_block = var.cidrs["test"]
requestor_cidr_block = var.cidrs["root"]
route_table_id = module.vpc.public_route_table_ids[0]
acceptor_intra_subnet_name = var.acceptor_intra_subnet_names["test"]
providers = {
aws.src = aws.root
aws.dst = aws.test
}
}

variables.tf

variable "vpc_name" {}variable "region" {
description = "AWS Region for vpc"
default = "eu-west-1"
}
variable "cidrs" {
type = map
default = {
dev = "10.20.0.0/16"
test = "10.10.0.0/16"
root = "10.50.0.0/16"
}
}
variable "vpn_name" {
description = "OpenVPN name"
}
variable "availability_zones" {
type = list(string)
default = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}
variable "private_subnets" {
type = map
default = {
dev = ["10.20.1.0/24", "10.20.3.0/24", "10.20.5.0/24"]
test = ["10.10.1.0/24", "10.10.3.0/24", "10.10.5.0/24"]
root = ["10.50.1.0/24", "10.50.3.0/24", "10.50.5.0/24"]
}
}
variable "public_subnets" {
type = map
default = {
dev = ["10.20.0.0/24", "10.20.2.0/24", "10.20.4.0/24"]
test = ["10.10.0.0/24", "10.10.2.0/24", "10.10.4.0/24"]
root = ["10.50.0.0/24", "10.50.2.0/24", "10.50.4.0/24"]
}
}
variable "database_subnets" {
type = map
default = {
dev = ["10.20.50.0/24", "10.20.51.0/24", "10.20.52.0/24"]
test = ["10.10.50.0/24", "10.10.51.0/24", "10.10.52.0/24"]
root = ["10.50.50.0/24", "10.50.51.0/24", "10.50.52.0/24"]
}
}
variable "vpc_ids" {
type = map
default = {
dev = "REPLACE_WITH_YOUR_VPCID"
test = "REPLACE_WITH_YOUR_VPCID"
}
}
variable "acceptor_intra_subnet_names" {
type = map
default = {
dev = "REPLACE_WITH_YOUR_DEV_INTRA_SUBNET_NAME"
test = "REPLACE_WITH_YOUR_TEST_INTRA_SUBNET_NAME"
}
}

References

--

--