VPC Peering Across Regions with VPC Flow Logs (Mini Project-2)

Today’s Terraform learning was a big architectural step forward.
Until now, most of my focus was on single-VPC setups. But in real production environments, infrastructure rarely lives in isolation. So today, I built Mini Project-2, where I connected two VPCs in different AWS regions using VPC Peering, verified communication using EC2 instances, and added VPC Flow Logs for visibility and monitoring — all using Terraform.
This project helped me understand networking, security, and observability together, not as separate concepts.
Project Overview
The goal of this mini project was to:
Create two VPCs in different AWS regions
Establish VPC Peering between them
Allow private communication between EC2 instances
Enable VPC Flow Logs to capture traffic data
Store logs securely in S3 buckets
High-Level Architecture
Primary Region (VPC A) Secondary Region (VPC B)
--------------------- --------------------------
VPC (CIDR A) VPC (CIDR B)
└── Subnet └── Subnet
└── EC2 <—— Peering ——> └── EC2
Both VPCs → Flow Logs → S3 Buckets
Creating Two VPCs in Different Regions
I started by creating two separate VPCs, each using a different AWS provider alias (primary and secondary). DNS support and hostnames were enabled to ensure proper resolution.
Key learning here:
Terraform provider aliases are essential for multi-region setups
Each resource must explicitly reference the correct provider
Subnets, Internet Gateways & Route Tables
For both VPCs, I configured:
One public subnet
Internet Gateway
Route table with
0.0.0.0/0routeRoute table associations
This ensured that:
EC2 instances could access the internet
The VPCs were fully functional before peering
Security Groups for Cross-VPC Communication
To allow communication between the VPCs, I configured security groups with:
SSH access (port 22)
ICMP (ping) between VPC CIDR blocks
Full TCP access between peer VPC CIDRs
This step reinforced an important lesson:
VPC peering alone is not enough — security groups must explicitly allow traffic.
VPC Peering Connection (Cross-Region)
Next came the core of the project — VPC Peering.
Because the VPCs are in different regions, the peering process required:
Creating the peering request in the primary region
Explicit acceptance in the secondary region
Updating route tables on both sides
Once routing was added, both VPCs could communicate using private IPs, without NAT or internet routing.
EC2 Instances for Connectivity Testing
To validate everything, I launched:
One EC2 instance in the primary VPC
One EC2 instance in the secondary VPC
Both instances were placed in their respective subnets and attached to the correct security groups.
This allowed me to:
SSH into instances
Test ping and connectivity across VPCs
Confirm that peering and routing were working correctly
Adding VPC Flow Logs (Observability Layer)
This was one of the most valuable learnings of the day.
I enabled VPC Flow Logs for both VPCs to capture:
Accepted traffic
Rejected traffic
All network activity (traffic_type = ALL)
Flow Log Destination
Separate S3 buckets for each region
Public access blocked
Bucket policies allowing only the VPC Flow Logs service
This setup ensures:
Secure log storage
Visibility into network behavior
Easier debugging and auditing
S3 Buckets for Flow Logs
For both regions, I created:
S3 buckets with public access blocked
Proper bucket policies allowing
vpc-flow-logs.amazonaws.comAccount-level condition checks for added security
This reinforced a critical production practice:
Logs are sensitive data — they must be private and controlled.
Data Sources Used
To keep the configuration dynamic and reusable, I used Terraform data sources for:
Availability Zones
Latest Ubuntu AMIs
AWS account identity
This helped avoid hardcoding values and made the setup more flexible.
Complete Terraform Code
Below is the full working Terraform configuration I used 👇
resource "aws_vpc" "primary" {
cidr_block = var.primary_cidr_block
provider = aws.primary
enable_dns_hostnames = true
enable_dns_support = true
tags = var.primary_tags
}
resource "aws_vpc" "secondary" {
cidr_block = var.secondary_cidr_block
provider = aws.secondary
enable_dns_hostnames = true
enable_dns_support = true
tags = var.secondary_tags
}
resource "aws_subnet" "primary_subnet" {
vpc_id = aws_vpc.primary.id
provider = aws.primary
cidr_block = var.primary_cidr_block
availability_zone = data.aws_availability_zones.primary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "terraform-primary-region-subnet ${var.primary_region}"
}
}
resource "aws_subnet" "secondary_subnet" {
vpc_id = aws_vpc.secondary.id
provider = aws.secondary
cidr_block = var.secondary_cidr_block
availability_zone = data.aws_availability_zones.secondary.names[0]
map_public_ip_on_launch = true
tags = {
Name = "terraform-secondary-region-subnet ${var.secondary_region}"
}
}
resource "aws_internet_gateway" "gw-primary" {
vpc_id = aws_vpc.primary.id
provider = aws.primary
tags = {
Name = "terraform-primary-igw"
}
}
resource "aws_internet_gateway" "gw-secondary" {
vpc_id = aws_vpc.secondary.id
provider = aws.secondary
tags = {
Name = "terraform-secondary-igw"
}
}
resource "aws_route_table" "rt-primary" {
vpc_id = aws_vpc.primary.id
provider = aws.primary
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw-primary.id
}
tags = {
Name = "terraform-primary-rt"
}
}
resource "aws_route_table" "rt-secondary" {
vpc_id = aws_vpc.secondary.id
provider = aws.secondary
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw-secondary.id
}
tags = {
Name = "terraform-secondary-rt"
}
}
resource "aws_route_table_association" "a-primary" {
subnet_id = aws_subnet.primary_subnet.id
route_table_id = aws_route_table.rt-primary.id
provider = aws.primary
}
resource "aws_route_table_association" "a-secondary" {
subnet_id = aws_subnet.secondary_subnet.id
route_table_id = aws_route_table.rt-secondary.id
provider = aws.secondary
}
resource "aws_security_group" "primary-sg" {
name = "primary-sg"
vpc_id = aws_vpc.primary.id
provider = aws.primary
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
protocol = "icmp"
from_port = -1
to_port = -1
cidr_blocks = [var.secondary_cidr_block]
}
ingress {
protocol = "tcp"
from_port = 0
to_port = 65535
cidr_blocks = [var.secondary_cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "primary-sg-example"
}
}
resource "aws_security_group" "secondary-sg" {
name = "sg"
vpc_id = aws_vpc.secondary.id
provider = aws.secondary
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
protocol = "icmp"
from_port = -1
to_port = -1
cidr_blocks = [var.primary_cidr_block]
}
ingress {
protocol = "tcp"
from_port = 0
to_port = 65535
cidr_blocks = [var.primary_cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "secondary-sg-example"
}
}
resource "aws_vpc_peering_connection" "primary-to-secondary" {
# peer_owner_id = var.peer_owner_id
provider = aws.primary
peer_vpc_id = aws_vpc.secondary.id
vpc_id = aws_vpc.primary.id
auto_accept = false
peer_region = var.secondary_region
tags = {
Name = "VPC Peering between primary and secondary"
}
}
resource "aws_vpc_peering_connection_accepter" "peer" {
# region = var.secondary_region
provider = aws.secondary
vpc_peering_connection_id = aws_vpc_peering_connection.primary-to-secondary.id
auto_accept = true
tags = {
Side = "Accepter"
}
}
resource "aws_route" "primary-to-secondary-route" {
provider = aws.primary
route_table_id = aws_route_table.rt-primary.id
destination_cidr_block = var.secondary_cidr_block
vpc_peering_connection_id = aws_vpc_peering_connection.primary-to-secondary.id
depends_on = [aws_vpc_peering_connection_accepter.peer]
}
resource "aws_route" "secondary-to-primary-route" {
provider = aws.secondary
route_table_id = aws_route_table.rt-secondary.id
destination_cidr_block = var.primary_cidr_block
vpc_peering_connection_id = aws_vpc_peering_connection.primary-to-secondary.id
depends_on = [aws_vpc_peering_connection_accepter.peer]
}
resource "aws_instance" "primary-instance" {
provider = aws.primary
ami = data.aws_ami.example-primary.id
instance_type = "t2.micro"
subnet_id = aws_subnet.primary_subnet.id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.primary-sg.id]
tags = {
Name = "TerraformInstance - Primary"
}
depends_on = [aws_vpc_peering_connection_accepter.peer]
}
resource "aws_instance" "secondary-instance" {
provider = aws.secondary
ami = data.aws_ami.example-secondary.id
instance_type = "t2.micro"
subnet_id = aws_subnet.secondary_subnet.id
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.secondary-sg.id]
tags = {
Name = "TerraformInstance - Secondary"
}
depends_on = [aws_vpc_peering_connection_accepter.peer]
}
resource "aws_s3_bucket" "primary_flowlog_s3_bucket" {
bucket = var.s3_bucket_name_primary
provider = aws.primary
}
resource "aws_s3_bucket_public_access_block" "example_primary" {
bucket = aws_s3_bucket.primary_flowlog_s3_bucket.id
provider = aws.primary
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_policy" "bucket_policy_primary" {
bucket = aws_s3_bucket.primary_flowlog_s3_bucket.id
provider = aws.primary
depends_on = [aws_s3_bucket_public_access_block.example_primary]
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSVPCFlowLogsWrite",
"Effect": "Allow",
"Principal": {
"Service": "vpc-flow-logs.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "${aws_s3_bucket.primary_flowlog_s3_bucket.arn}/AWSLogs/*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "${data.aws_caller_identity.current.account_id}"
}
}
},
{
"Sid": "AWSVPCFlowLogsAclCheck",
"Effect": "Allow",
"Principal": {
"Service": "vpc-flow-logs.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "${aws_s3_bucket.primary_flowlog_s3_bucket.arn}"
}
]
})
}
resource "aws_s3_bucket" "flowlog_s3_bucket_secondary" {
bucket = var.s3_bucket_name_secondary
provider = aws.secondary
}
resource "aws_s3_bucket_public_access_block" "example_secondary" {
bucket = aws_s3_bucket.flowlog_s3_bucket_secondary.id
provider = aws.secondary
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_policy" "bucket_policy_secondary" {
bucket = aws_s3_bucket.flowlog_s3_bucket_secondary.id
provider = aws.secondary
depends_on = [aws_s3_bucket_public_access_block.example_secondary]
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSVPCFlowLogsWrite",
"Effect": "Allow",
"Principal": {
"Service": "vpc-flow-logs.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "${aws_s3_bucket.flowlog_s3_bucket_secondary.arn}/AWSLogs/*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "${data.aws_caller_identity.current.account_id}"
}
}
},
{
"Sid": "AWSVPCFlowLogsAclCheck",
"Effect": "Allow",
"Principal": {
"Service": "vpc-flow-logs.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "${aws_s3_bucket.flowlog_s3_bucket_secondary.arn}"
}
]
})
}
resource "aws_flow_log" "primary_vpc_flow_log" {
provider = aws.primary
log_destination = aws_s3_bucket.primary_flowlog_s3_bucket.arn
log_destination_type = "s3"
traffic_type = "ALL"
vpc_id = aws_vpc.primary.id
depends_on = [aws_instance.primary-instance]
}
resource "aws_flow_log" "secondary_vpc_flow_log" {
provider = aws.secondary
log_destination = aws_s3_bucket.flowlog_s3_bucket_secondary.arn
log_destination_type = "s3"
traffic_type = "ALL"
vpc_id = aws_vpc.secondary.id
depends_on = [aws_instance.secondary-instance]
}
📺 Video That Helped Me Understand this concept:
Key Learnings from This Project
✔ Multi-region Terraform using provider aliases
✔ Real-world VPC Peering (not just theory)
✔ Route tables + security groups matter equally
✔ VPC Flow Logs are essential for visibility
✔ Terraform can manage networking, compute, and monitoring together
Final Thoughts
This mini project truly felt like real cloud engineering.
I didn’t just create resources —
I designed connectivity, security, and observability across regions.
Terraform is no longer just about provisioning infrastructure for me —
It’s becoming a tool to design architectures with confidence.
On to the next challenge 💪
Mini Project-3 coming soon…




