AWS VPC Peering using Terraform
VPC Peering

Why do we want to connect these two vpc together?
If we have a server in Primary VPC and the resources a user request must be residing in secondary VPC. So we need to connect these VPC together for communication. Also we need to make the connection secure.
Costs
This demo creates resources that incur AWS charges:
EC2 instances (t2.micro)
Data transfer between regions
VPC peering data transfer
Project Diagram

(Open the image in new tab for clarity)
Creating SSH Key Pairs
# For us-east-1
aws ec2 create-key-pair --key-name vpc-peering-demo --region us-east-1 --query 'KeyMaterial' --output text > vpc-peering-demo.pem
# For us-west-2
aws ec2 create-key-pair --key-name vpc-peering-demo --region us-west-2 --query 'KeyMaterial' --output text > vpc-peering-demo-west.pem
After creating the files for both the regions, we need to give them permission for read (400).
# Set permissions (on Linux/Mac)
chmod 400 *.pem
Creating primary and secondary VPC
resource "aws_vpc" "primary_vpc" {
cidr_block = var.primary_vpc_cidr
provider = aws.primary
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "Primary-VPC-${var.primary}"
}
}
resource "aws_vpc" "secondary_vpc" {
cidr_block = var.secondary_vpc_cidr
provider = aws.secondary
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "Secondary-VPC-${var.secondary}"
}
}
Refer the code repo for the variables. Link.
Subnet for Primary VPC and Secondary VPC
# Subnet in Primary VPC
resource "aws_subnet" "primary_subnet" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
cidr_block = var.primary_vpc_cidr
availability_zone = data.aws_availability_zones.primary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "Primary-Subnet-${var.primary}"
Environment = "Demo"
}
}
# Subnet in Secondary VPC
resource "aws_subnet" "secondary_subnet" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
cidr_block = var.secondary_vpc_cidr
availability_zone = data.aws_availability_zones.secondary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "Secondary-Subnet-${var.secondary}"
Environment = "Demo"
}
}
Internet Gateway configuration
resource "aws_internet_gateway" "primary_igw" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
tags = {
Name = "Primary-IGW-${var.primary}"
Environment = "Demo"
}
}
resource "aws_internet_gateway" "secondary_igw" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
tags = {
Name = "Secondary-IGW-${var.secondary}"
Environment = "Demo"
}
}
Route Table for Primary and Secondary VPC
# Route table for Primary VPC
resource "aws_route_table" "primary_rt" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.primary_igw.id
}
tags = {
Name = "Primary-RT-${var.primary}"
Environment = "Demo"
}
}
# Route table for Secondary VPC
resource "aws_route_table" "secondary_rt" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.secondary_igw.id
}
tags = {
Name = "Secondary-RT-${var.secondary}"
Environment = "Demo"
}
}
Route Table Association
# Associate the route table with the subnet in Primary VPC
resource "aws_route_table_association" "primary_rta" {
provider = aws.primary
subnet_id = aws_subnet.primary_subnet.id
route_table_id = aws_route_table.primary_rt.id
}
# Associate the route table with the subnet in Primary VPC
resource "aws_route_table_association" "secondary_rta" {
provider = aws.secondary
subnet_id = aws_subnet.secondary_subnet.id
route_table_id = aws_route_table.secondary_rt.id
}
VPC Peering connection from A→B and B→A
# VPC Peering Connection (Requester side - Primary VPC)
resource "aws_vpc_peering_connection" "primary_to_secondary_peering" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
peer_vpc_id = aws_vpc.secondary_vpc.id # destination id
peer_region = var.secondary # destination region
auto_accept = false
tags = {
Name = "Primary-to-Secondary-Peering"
Environment = "Demo"
Side = "Requester"
}
}
# VPC Peering Connection Accepter (Accepter side - Secondary VPC) - Acceptor
resource "aws_vpc_peering_connection_accepter" "secondary_peering_accepter" {
provider = aws.secondary
vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary_peering.id
auto_accept = true
tags = {
Name = "Secondary-Peering-Accepter"
Environment = "Demo"
Side = "Accepter"
}
}
Route table and VPC configuration
# Add route to Secondary VPC in Primary route table
resource "aws_route" "primary_to_secondary" {
provider = aws.primary
route_table_id = aws_route_table.primary_rt.id
destination_cidr_block = var.secondary_vpc_cidr
vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary.id
depends_on = [aws_vpc_peering_connection_accepter.secondary_peering_accepter]
}
# Add route to Primary VPC in Secondary route table
resource "aws_route" "secondary_to_primary" {
provider = aws.secondary
route_table_id = aws_route_table.secondary_rt.id
destination_cidr_block = var.primary_vpc_cidr
vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary_peering.id
depends_on = [aws_vpc_peering_connection_accepter.secondary_peering_accepter]
}
Resolving known issues
1. Restrict SSH Access
Replace the SSH rule to allow only specific IPs (not 0.0.0.0/0)
ingress {
description = "SSH from trusted IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.cidr_list[0]
}
For testing, we are not keeping this code. I will test the connection from my system.
2. Limit Outbound (Egress) Traffic
Instead of allowing all outbound traffic, restrict to only necessary protocols (e.g., HTTP/HTTPS):
egress {
description = "Allow HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
3. Restrict VPC Peering Traffic
Only allow required ports from the peer VPC, not all TCP:
ingress {
description = "HTTP from Secondary VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.secondary_vpc_cidr]
}
ingress {
description = "HTTPS from Secondary VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.secondary_vpc_cidr]
}
Add more subnets (private subnets)
# Subnet in Primary VPC
resource "aws_subnet" "primary_subnet" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
cidr_block = var.primary_vpc_cidr
availability_zone = data.aws_availability_zones.primary.names[0]
map_public_ip_on_launch = false
tags = {
Name = "Primary-Subnet-${var.primary}"
Environment = "Demo"
}
}
# Subnet in Secondary VPC
resource "aws_subnet" "secondary_subnet" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
cidr_block = var.secondary_vpc_cidr
availability_zone = data.aws_availability_zones.secondary.names[0]
map_public_ip_on_launch = false
tags = {
Name = "Secondary-Subnet-${var.secondary}"
Environment = "Demo"
}
}
Implement NAT gateways
Create an Elastic IP (EIP) for each NAT gateway.
Create the NAT gateway in a public subnet.
Update the private subnet’s route table to route internet-bound traffic (
0.0.0.0/0) through the NAT gateway.
# PRIMARY
# Elastic IP for NAT Gateway in Primary VPC
resource "aws_eip" "primary_nat_eip" {
provider = aws.primary
domain = "vpc"
}
# NAT Gateway in Primary public Subnet
resource "aws_nat_gateway" "primary_nat_gw" {
provider = aws.primary
allocation_id = aws_eip.primary_nat_eip.id
subnet_id = aws_subnet.primary_subnet.id # Must be a public subnet
tags = {
Name = "Primary-NAT-GW-${var.primary}"
Environment = "Demo"
}
}
# Private Route Table for Primary Private Subnet
resource "aws_route_table" "primary_private_rt" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.primary_nat_gw.id
}
tags = {
Name = "Primary-NAT-GW-${var.primary}"
Environment = "Demo"
}
}
# Associate the private route table with the private subnet in Primary VPC
resource "aws_route_table_association" "primary_private_rta" {
provider = aws.primary
subnet_id = aws_subnet.primary_private_subnet.id
route_table_id = aws_route_table.primary_private_rt.id
}
# SECONDARY
# Elastic IP for NAT Gateway in Primary VPC
resource "aws_eip" "secondary_nat_eip" {
provider = aws.secondary
domain = "vpc"
}
# NAT Gateway in Primary public Subnet
resource "aws_nat_gateway" "secondary_nat_gw" {
provider = aws.secondary
allocation_id = aws_eip.secondary_nat_eip.id
subnet_id = aws_subnet.secondary_subnet.id # Must be a public subnet
tags = {
Name = "Secondary-NAT-GW-${var.secondary}"
Environment = "Demo"
}
}
# Private Route Table for Secondary Private Subnet
resource "aws_route_table" "secondary_private_rt" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.secondary_nat_gw.id
}
tags = {
Name = "Secondary-NAT-GW-${var.secondary}"
Environment = "Demo"
}
}
# Associate the private route table with the private subnet in Primary VPC
resource "aws_route_table_association" "primary_private_rta" {
provider = aws.secondary
subnet_id = aws_subnet.secondary_private_subnet.id
route_table_id = aws_route_table.secondary_private_rt.id
}
Add VPC Flow Logs for traffic analysis
Create an S3 bucket or CloudWatch Logs group to store the logs.
Create an IAM role with permissions to write logs.
Attach the flow log to your VPC or subnets.
S3 Bucket for Flow Logs
resource "aws_s3_bucket" "flow_logs_bucket" {
bucket = "my-vpc-flow-logs"
}
IAM Role for Flow Logs
resource "aws_iam_role" "flow_logs_role" {
name = "vpc-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "flow_logs_policy" {
name = "vpc-flow-logs-policy"
role = aws_iam_role.flow_logs_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"s3:PutObject"
]
Effect = "Allow"
Resource = "*"
}
]
})
}
VPC Flow Logs for Primary VPC
resource "aws_flow_log" "primary_vpc_flow_log" {
provider = aws.primary
log_destination = aws_s3_bucket.flow_logs_bucket.arn
iam_role_arn = aws_iam_role.flow_logs_role.arn
traffic_type = "ALL"
vpc_id = aws_vpc.primary_vpc.id
}
VPC Flow Logs for Secondary VPC
resource "aws_flow_log" "secondary_vpc_flow_log" {
provider = aws.secondary
log_destination = aws_s3_bucket.flow_logs_bucket.arn
iam_role_arn = aws_iam_role.flow_logs_role.arn
traffic_type = "ALL"
vpc_id = aws_vpc.secondary_vpc.id
}
Video reference:
Arigato!