Introduction:
VPC Peering is a networking feature in AWS that allows two Virtual Private Clouds (VPCs) to communicate with each other privately using AWS’s internal network. Once a peering connection is established, resources such as EC2 instances or internal services in one VPC can communicate with resources in another VPC using private IP addresses, without routing traffic over the public internet.
This makes VPC peering a simple and effective solution for connecting isolated VPC environments while maintaining low latency and strong network-level isolation.
>> Key points about VPC Peering:
- Traffic stays within the AWS backbone (more secure and low latency)
- Works across regions and accounts
- Supports private IP communication
- Requires explicit route table updates
- Is non-transitive (A <-> B and B <-> C does NOT mean A <-> C automatically)
Hands on Exercise
This mini project demonstrates how to build a cross-region, multi-VPC architecture on AWS using Terraform and VPC peering. The setup provisions two VPCs in different AWS regions, launches EC2 instances in each VPC, and connects them using a private VPC peering connection.
The project focuses on practical AWS networking concepts such as:
- Cross-region VPC peering
- Route table configuration for private traffic
- Security group rules for controlled inter-VPC access
- Managing networking infrastructure using Infrastructure as Code
To access the EC2 instances, a separate SSH key pair is generated for each region, allowing secure login into the instances deployed in their respective VPCs.
# 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
# Set permissions (on Linux/Mac)
chmod 400 vpc-peering-demo.pem
#locals.tf
# Local values for VPC Peering Demo
locals {
# User data template for Primary instance
primary_user_data = <<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y apache2
systemctl start apache2
systemctl enable apache2
echo "<h1>Primary VPC Instance - ${var.primary_region}</h1>" > /var/www/html/index.html
echo "<p>Private IP: $(hostname -I)</p>" >> /var/www/html/index.html
EOF
# User data template for Secondary instance
secondary_user_data = <<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y apache2
systemctl start apache2
systemctl enable apache2
echo "<h1>Secondary VPC Instance - ${var.secondary_region}</h1>" > /var/www/html/index.html
echo "<p>Private IP: $(hostname -I)</p>" >> /var/www/html/index.html
EOF
}
#variables.tf
# Variables for VPC Peering Demo
variable "primary_region" {
description = "Primary AWS region for the first VPC"
type = string
default = "us-east-1"
}
variable "secondary_region" {
description = "Secondary AWS region for the second VPC"
type = string
default = "us-west-2"
}
variable "primary_vpc_cidr" {
description = "CIDR block for the primary VPC"
type = string
default = "10.0.0.0/16"
}
variable "secondary_vpc_cidr" {
description = "CIDR block for the secondary VPC"
type = string
default = "10.1.0.0/16"
}
variable "primary_subnet_cidr" {
description = "CIDR block for the primary subnet"
type = string
default = "10.0.1.0/24"
}
variable "secondary_subnet_cidr" {
description = "CIDR block for the secondary subnet"
type = string
default = "10.1.1.0/24"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "primary_key_name" {
description = "Name of the SSH key pair for Primary VPC instance (us-east-1)"
type = string
default = ""
}
variable "secondary_key_name" {
description = "Name of the SSH key pair for Secondary VPC instance (us-west-2)"
type = string
default = ""
}
# main.tf
# VPC Peering Demo
# This demo creates two VPCs in different regions and establishes peering between them
# Primary VPC in us-east-1
resource "aws_vpc" "primary_vpc" {
provider = aws.primary
cidr_block = var.primary_vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "Primary-VPC-${var.primary_region}"
Environment = "Demo"
Purpose = "VPC-Peering-Demo"
}
}
# Secondary VPC in us-west-2
resource "aws_vpc" "secondary_vpc" {
provider = aws.secondary
cidr_block = var.secondary_vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "Secondary-VPC-${var.secondary_region}"
Environment = "Demo"
Purpose = "VPC-Peering-Demo"
}
}
# Subnet in Primary VPC
resource "aws_subnet" "primary_subnet" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
cidr_block = var.primary_subnet_cidr
availability_zone = data.aws_availability_zones.primary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "Primary-Subnet-${var.primary_region}"
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_subnet_cidr
availability_zone = data.aws_availability_zones.secondary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "Secondary-Subnet-${var.secondary_region}"
Environment = "Demo"
}
}
# Internet Gateway for Primary VPC
resource "aws_internet_gateway" "primary_igw" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
tags = {
Name = "Primary-IGW"
Environment = "Demo"
}
}
# Internet Gateway for Secondary VPC
resource "aws_internet_gateway" "secondary_igw" {
provider = aws.secondary
vpc_id = aws_vpc.secondary_vpc.id
tags = {
Name = "Secondary-IGW"
Environment = "Demo"
}
}
# 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-Route-Table"
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-Route-Table"
Environment = "Demo"
}
}
# Associate route table with Primary subnet
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 route table with Secondary subnet
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 (Requester side - Primary VPC)
resource "aws_vpc_peering_connection" "primary_to_secondary" {
provider = aws.primary
vpc_id = aws_vpc.primary_vpc.id
peer_vpc_id = aws_vpc.secondary_vpc.id
peer_region = var.secondary_region
auto_accept = false
tags = {
Name = "Primary-to-Secondary-Peering"
Environment = "Demo"
Side = "Requester"
}
}
# VPC Peering Connection Accepter (Accepter side - Secondary VPC)
resource "aws_vpc_peering_connection_accepter" "secondary_accepter" {
provider = aws.secondary
vpc_peering_connection_id = aws_vpc_peering_connection.primary_to_secondary.id
auto_accept = true
tags = {
Name = "Secondary-Peering-Accepter"
Environment = "Demo"
Side = "Accepter"
}
}
# 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_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.id
depends_on = [aws_vpc_peering_connection_accepter.secondary_accepter]
}
# Security Group for Primary VPC EC2 instance
resource "aws_security_group" "primary_sg" {
provider = aws.primary
name = "primary-vpc-sg"
description = "Security group for Primary VPC instance"
vpc_id = aws_vpc.primary_vpc.id
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "ICMP from Secondary VPC"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = [var.secondary_vpc_cidr]
}
ingress {
description = "All traffic from Secondary VPC"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = [var.secondary_vpc_cidr]
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "Primary-VPC-SG"
Environment = "Demo"
}
}
# Security Group for Secondary VPC EC2 instance
resource "aws_security_group" "secondary_sg" {
provider = aws.secondary
name = "secondary-vpc-sg"
description = "Security group for Secondary VPC instance"
vpc_id = aws_vpc.secondary_vpc.id
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "ICMP from Primary VPC"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = [var.primary_vpc_cidr]
}
ingress {
description = "All traffic from Primary VPC"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = [var.primary_vpc_cidr]
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "Secondary-VPC-SG"
Environment = "Demo"
}
}
# EC2 Instance in Primary VPC
resource "aws_instance" "primary_instance" {
provider = aws.primary
ami = data.aws_ami.primary_ami.id
instance_type = var.instance_type
subnet_id = aws_subnet.primary_subnet.id
vpc_security_group_ids = [aws_security_group.primary_sg.id]
key_name = var.primary_key_name
user_data = local.primary_user_data
tags = {
Name = "Primary-VPC-Instance"
Environment = "Demo"
Region = var.primary_region
}
depends_on = [aws_vpc_peering_connection_accepter.secondary_accepter]
}
# EC2 Instance in Secondary VPC
resource "aws_instance" "secondary_instance" {
provider = aws.secondary
ami = data.aws_ami.secondary_ami.id
instance_type = var.instance_type
subnet_id = aws_subnet.secondary_subnet.id
vpc_security_group_ids = [aws_security_group.secondary_sg.id]
key_name = var.secondary_key_name
user_data = local.secondary_user_data
tags = {
Name = "Secondary-VPC-Instance"
Environment = "Demo"
Region = var.secondary_region
}
depends_on = [aws_vpc_peering_connection_accepter.secondary_accepter]
}
VPC Definitions
Primary and Secondary VPCs
Two VPCs are created across different AWS regions: a Primary VPC in us-east-1 and a Secondary VPC in us-west-2.
Each VPC is assigned a unique CIDR block and has DNS support and DNS hostnames enabled. This setup demonstrates a cross-region, multi-VPC architecture where private communication is established using VPC peering.
Subnets
Primary and Secondary Subnets
Each VPC contains a subnet configured within its respective CIDR range. For this demo, the subnets are configured with public IP assignment enabled to simplify instance access and testing. These subnets host EC2 instances that communicate with each other over private IPs through VPC peering, without relying on the public internet for inter-VPC traffic.
Internet Gateways
Primary and Secondary VPC Internet Gateways
An Internet Gateway is attached to each VPC to allow outbound internet access for EC2 instances (for package installation, testing, and SSH access).
Although internet access is enabled, inter-VPC communication still flows only through the VPC peering connection, not via the internet.
Route Tables
Primary and Secondary Route Tables
Each VPC has its own route table:
- A default route (0.0.0.0/0) pointing to the Internet Gateway for outbound internet access
- A peering route pointing to the CIDR block of the peer VPC via the VPC peering connection
This ensures that traffic destined for the peer VPC is routed privately through AWS’s internal network.
Route Table Associations
Subnet-to-Route Table Mapping
Each subnet is explicitly associated with its corresponding route table. This guarantees that both internet-bound traffic and peering traffic follow the intended routing paths.
VPC Peering Connections
Cross-Region VPC Peering
A VPC peering connection is established between the Primary VPC and the Secondary VPC across regions.
The peering request is created from the Primary VPC and explicitly accepted in the Secondary VPC, completing the cross-region peering setup and avoiding any assumptions about transitive routing.
Peering Route Configuration
Bidirectional Peering Routes
Bidirectional routes are added to both VPC route tables:
- The Primary VPC routes traffic destined for the Secondary VPC CIDR through the peering connection
- The Secondary VPC routes traffic destined for the Primary VPC CIDR through the same peering connection
This enables private, low-latency communication between the two VPCs using private IP addresses.
Security Groups for EC2 Instances
Instance-Level Security Controls
Security groups are configured separately for each VPC:
- SSH access is allowed for administrative purposes
- ICMP traffic is permitted between the two VPC CIDR ranges for connectivity testing
- Internal TCP traffic is allowed only between the peered VPCs
These rules ensure controlled access while enabling necessary inter-VPC communication.
EC2 Instances
Primary and Secondary EC2 Instances
One EC2 instance is launched in each VPC using region-specific AMIs.
Each instance runs a simple Apache web server configured via user data, allowing verification of connectivity using private IP addresses. Communication between instances occurs exclusively over the VPC peering connection.
To access the instances, region-specific SSH key pairs are used. Keys must be available locally to connect to the respective EC2 instances.
Final Outcome
The final setup successfully establishes cross-region VPC peering between two VPCs, allowing EC2 instances to communicate securely using private IP addresses.
This project demonstrates how VPC peering, route tables, and security groups work together to enable private networking across AWS regions—without relying on the public internet for inter-VPC traffic.
Conclusion
This mini project demonstrates how AWS VPC peering combined with Terraform can be used to build a reliable cross-region networking setup that enables private communication between resources in multiple VPCs. By creating an explicit VPC peering connection, configuring bidirectional routes, and applying VPC-scoped security group rules, EC2 instances in separate regions are able to communicate using private IP addresses over the AWS backbone, without relying on the public internet for inter-VPC traffic.
The project also highlights important real-world networking concepts such as non-transitive VPC peering, the necessity of manual route table configuration, and how security groups act as the final gatekeepers for network access. Managing the entire setup through Infrastructure as Code (Terraform) ensures consistency, repeatability, and easier debugging compared to manual configurations.
Overall, this project serves as a strong foundation for understanding AWS networking at scale and can be further extended to more advanced architectures involving private subnets, bastion hosts, Transit Gateway, or centralized networking models in production environments.
>> Connect With Me
If you enjoyed this post or want to follow my #30DaysOfAWSTerraformChallenge journey, feel free to connect with me here:
💼 LinkedIn: Amit Kushwaha
🐙 GitHub: Amit Kushwaha
📝 Hashnode / Amit Kushwaha
🐦 Twitter/X: Amit Kushwaha





Top comments (0)