Skip to main content

Command Palette

Search for a command to run...

AWS VPC Peering using Terraform

Published
7 min read

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

  1. Create an Elastic IP (EIP) for each NAT gateway.

  2. Create the NAT gateway in a public subnet.

  3. 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

  1. Create an S3 bucket or CloudWatch Logs group to store the logs.

  2. Create an IAM role with permissions to write logs.

  3. 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!

More from this blog

Code Companions

32 posts