Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min read
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/0 route

  • Route 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.com

  • Account-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…

More from this blog

B

Build With Rajesh

31 posts