Implementing VPC Peering with Terraform: An Exploration of Its Advantages
Tags: webdev, beginners, aws, infrastructureascode
Published: 2023-08-05 12:50:26

Recently, to achieve a specific task, we configured a Lambda application, which was placed in the private subnet of VPC1, to use the public URL of the ALB (which connects to the private subnet's ECS) in the public subnet of VPC2, via the NAT Gateway in the public subnet of the same VPC.

However, since the traffic to the ALB was solely from Lambda, we decided to switch to a private connection via VPC peering to meet the following non-functional requirements:

- Cost: Using VPC peering reduces data transfer volume compared to using NAT Gateway, leading to cost savings.
- Performance: It can minimize network latency between VPCs, which is effective for applications that require high data transfer volume or low latency.
- Security: Since communication takes place within a private network connection in AWS, it eliminates the risk of the communication going out to the internet.

In this article, we'll outline how to implement VPC peering with Terraform to meet these requirements.

## Implementation
This time, we have adopted the following directory structure. We have prepared directories corresponding to each environment (dev, stg, prod), and reusable modules (route_table, security_group, vpc_peering).

```lua
-- terraform-project/
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf
      -- stg/
         -- backend.tf
         -- main.tf
      -- prod/
         -- backend.tf
         -- main.tf
   -- modules/
      -- route_table/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- security_group/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- vpc_peering/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
```

### VPC Peering
VPC peering is a feature that creates a direct network connection between two VPCs within AWS. In the `vpc_peering/main.tf` file below, we are getting the VPC IDs from the SSM Parameter Store and using them to set up VPC peering.

`modules/vpc_peering/main.tf`
```tf
data "aws_ssm_parameter" "lambda_vpc_id" {
  name = "/vpc/id/lambda"
}

data "aws_ssm_parameter" "ecs_vpc_id" {
  name = "/vpc/id/ecs"
}

// For VPC peering within the same account, connect and approve at once
resource "aws_vpc_peering_connection" "vpc_peering" {
  vpc_id      = data.aws_ssm_parameter.lambda_vpc_id.value
  peer_vpc_id = data.aws_ssm_parameter.ecs_vpc_id.value
  auto_accept = true
}
```
Additionally, we are outputting the ID of the VPC peering in `outputs.tf` so it can be used in other modules.

`modules/vpc_peering/outputs.tf`
```tf
output "vpc_peering_connection_id" {
  description = "Connection Id of VPC Peering"
  value       = aws_vpc_peering_connection.vpc_peering.id
}
```

### Route Table
Next, we will create a route table that sets the route for network traffic within the VPC. Here, we are setting up routes that go via the peering connection using the CIDR blocks of each VPC and the ID of the VPC peering. We have `lambda_local_nat` as the route table where the Lambda is located, and `ecs_local` as the route table where the ALB is located.

`modules/route_table/main.tf`
```tf
data "aws_ssm_parameter" "lambda_vpc_cidr" {
  name = "/vpc/cidr/lambda"
}

data "aws_ssm_parameter" "ecs_vpc_cidr" {
  name = "/vpc/cidr/ecs"
}

resource "aws_route_table" "lambda_local_nat" {
  vpc_id = data.aws_ssm_parameter.lambda_vpc_id.value
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = data.aws_ssm_parameter.lambda_nat_gw_id.value
  }
  tags = {
    Name = "lambda-vpc-private-local-nat-rtb"
  }
}

// Split considering the impact on existing routes
resource "aws_route" "peering_route" {
  route_table_id            = aws_route_table.lambda_local_nat.id
  destination_cidr_block    = data.aws_ssm_parameter.ecs_vpc_cidr.value
  vpc_peering_connection_id = var.vpc_peering_connection_id
}

resource "aws_route_table" "ecs_local" {
  vpc_id = data.aws_ssm_parameter.ecs_vpc_id.value

  tags = {
    Name = "ecs-vpc-private-local-rtb"
  }
}

// Split considering the impact on existing routes
resource "aws_route" "ecs_peering_route" {
  route_table_id                = aws_route_table.ecs_local.id
  destination_cidr_block        = data.aws_ssm_parameter.lambda_vpc_cidr.value
  vpc_peering_connection_id     = var.vpc_peering_connection_id
}
```

In the route_table module, we define a variable in `variables.tf` to accept the ID of the VPC peering as an input.

`modules/route_table/variables.tf`
```tf
variable "vpc_peering_connection_id" {
  description = "The ID of the VPC peering connection"
  type        = string
}
```

### Security Group
Finally, let's configure the security group for the ALB. This allows us to create rules that only allow communication from specific IP ranges. Since our Lambda is located in two private subnets, we are specifying each subnet's CIDR block.

```tf:modules/security_group/main.tf
data "aws_ssm_parameter" "lambda_private_subnet_cidr1" {
  name = "/subnet/private/cidr1/lambda"
}

data "aws_ssm_parameter" "lambda_private_subnet_cidr2" {
  name = "/subnet/private/cidr2/lambda"
}

resource "aws_security_group_rule" "elb_sg_ingress_443_lambda" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  security_group_id = aws_security_group.elb_sg.id
  cidr_blocks       = [data.aws_ssm_parameter.lambda_private_subnet_cidr1.value, data.aws_ssm_parameter.lambda_private_subnet_cidr2.value]
}
```

### Setting up Private DNS and Certificates
That concludes the implementation in Terraform (all that's left is to run terraform apply), but since VPC Peering deals with private connections, you will need to change the connection destination for the Application Load Balancer (ALB) in your Lambda application from a public URL to a private DNS.

In addition, to establish an HTTPS connection, you will need to create a private certificate using AWS Certificate Manager (ACM)'s Private Certificate Authority (CA) and assign that certificate to the ALB's listener.

Here are the setup instructions:

1. Creating a Private CA
- Select "Private CAs" from the AWS Certificate Manager (ACM) dashboard and click "Create CA".
- Choose the type of Root CA or Subordinate CA and fill in the details of the CA.
- Click "Next", confirm your settings.
- If there are no issues, click "Confirm and create" to create your Private CA.
2. Issuing a Private Certificate
- Once your Private CA is created, return to the "Certificate Manager" dashboard and click "Get started".
- On the certificate setup screen, select "Private" and click "Request a certificate".
- Enter the domain name that includes the private DNS name and set up the details.
- Once you've completed your setup, click "Confirm and request".
3. Assigning the Certificate to the ALB's Listener
- Go to the settings of the ALB and select the listener in question.
- Select "Change certificate" from the "Actions" dropdown and choose the private certificate you created.

## Conclusion
In this article, we thoroughly explained how to implement VPC peering to meet specific non-functional requirements. By using VPC peering, we can make use of private network connections within AWS, allowing for cost reduction, improved performance, and enhanced security.

Managing these configurations using Terraform allows us to codify our infrastructure, maintain consistency across environments, and easily repurpose it. We also demonstrated how to set up an HTTPS connection for ALB using ACM's private CA.

As we continue to pursue AWS best practices, let's build more efficient and secure systems.