Skip to main content

Command Palette

Search for a command to run...

Virtual Private Clouds on AWS

Published
9 min read

Introduction

A Virtual Private Cloud (VPC) is a logically isolated virtual network with a cloud provider.

AWS accounts come with a default VPC in each region that is good for getting started. That being said, it is always good to create custom VPCs based on specific application requirements, security needs and long-term scalability goals. Investing time to thoughtfully design VPC infrastructure can pay dividends in the form of a robust, secure and adaptable cloud networking foundation.

VPC Overview

A VPC in AWS spans an entire AWS region — with all its Availability Zones (AZs). AZs are physically isolated data centers within the AWS network. Within each AZ you can have one or more subnets — a subnet can only exist in one AZ.

Components of a VPC

The components of a VPC include;

Subnets

A subnet is a range of IP addresses that resides within a single Availability Zone(AZ) in a VPC. Examples of subnets include;

  1. Public subnet: A public subnet has a direct route to the Internet Gateway. Instances deployed in the public subnet can directly access the internet and are also directly reachable from the internet.

  2. Private subnet: A private subnet does not have a direct route to the Internet Gateway. Instances in a private subnet can access the internet through a NAT Gateway or NAT device.

  3. VPN-only subnet: A VPN-only subnet is one that only has a route to a site-to-site VPN connection.

  4. Isolated subnet: An isolated subnet is one with no routes to resources outside the VPC

Gateways

Gateways are connectivity points within a VPC. Below are the various types of gateways;

  1. Internet Gateways: These allow instances in a public subnet to connect to the internet. Instances within a VPC are only aware of their private IP addresses. The internet gateway logically provides one-to-one NAT when communicating from the VPC to the internet and vice versa.

  2. NAT and Egress-only Gateways: These allow instances in private subnets to connect to the internet while remaining unreachable from the internet. NAT gateways are used for IPv4 traffic whereas Egress-only gateways are used for IPv6 traffic.

    When deployed in a private subnet, a NAT gateway is considered a private NAT gateway and just like any other instance deployed within the private subnet, it won’t have internet access.

  3. Transit Gateways: These allow connecting two VPCs or a VPC and an on-premise network.

  4. VPN Gateways: These allow creation of an IPsec VPN connection between a VPC and an on-premise network.

  5. VPC peering connection: These allow secure and direct communication between two VPCs within the AWS infrastructure. The two VPCs can be in two different accounts or AWS regions however their CIDR blocks should not overlap. Traffic between peered VPCs never traverses the public internet and instances communicate using their private IP addresses as though they’re in the same network.

When connecting two VPCs within AWS, default to using a VPC peering connection because it introduces no extra piece of physical hardware or gateway — hence no additional failure points — and attracts no additional charge.

Route tables

Route tables contain a set of rules(routes) that determine where network traffic from a subnet or gateway is directed. When directing traffic, the route is chosen based on the longest prefix that matches its destination i.e the most specific route match.

Access Control Lists and Security Groups

Access Control Lists (ACLs) and Security Groups control traffic to and from a network interface. ACLs work at subnet level whereas Security Groups work at instance level. A VPC comes with a default ACL that allows all inbound and outbound traffic.

ACLs are stateless whereas Security Groups are stateful. By being stateless, it is possible that an instance can reach a destination however return traffic from the destination is rejected. When configuring an ACL care must be taken to ensure that there are explicit rules to allow return traffic for allowed outbound traffic. For Security Groups, return traffic is allowed regardless of the configured rules i.e they’re stateful.

Setting up a VPC

Prerequisites

This is going to be a hands-on lab. Before starting;

  • Install Terraform and Terragrunt on your computer. To easily switch between Terraform versions, I recommend installing tfenv. Installation and usage instructions for tfenv are found on the link https://github.com/tfutils/tfenv

  • Have an AWS account. AWS provides a generous free tier that can be used to complete this hands-on lab at a minimal cost.

Terraform is an Infrastructure as Code(IaC) tool. IaC is the act of declaratively configuring infrastructure and storing the declarative configuration in source control. One of the biggest benefits of IaC is that it makes it easy to review how our infrastructure is set up.

Planning the VPC CIDR blocks

The first key decision to make is the CIDR block to use for the VPC. The following are the supported private IP address ranges on AWS — RFC 1918.

  • 10.0.0.0/8

  • 172.16.0.0/12

  • 192.168.0.0./16

An excellent cheatsheet for subnets and and other IP address CIDR network references is found at freecodecamp

The source code for all the steps described in the next sections is located at the Github repository https://github.com/ivanoronee/aws-vpc-overview

Note: You can follow along, creating one resource at a time by downloading the code and commenting out all sections — except the locals section — of the main.tf file, and only uncommenting and trying out the one’s being discussed. Instructions on how to run the code are found in the repository.

Below is the configuration that will be used for this walkthrough;

data "aws_availability_zones" "available" {}

locals {
 vpc_cir_block        = "172.16.0.0/16"
 nat_gateway_subnet   = aws_subnet.public[element(keys(local.public_subnets_cidr_az_mapping), 0)]
 nat_gateway_az       = local.nat_gateway_subnet["availability_zone"]
 public_internet_cidr = "0.0.0.0/0"
 availability_zones   = slice(data.aws_availability_zones.available.names, 0, 3)

 public_subnets_cidr_az_mapping = {
   "172.16.10.0/24" = local.availability_zones[0],
   "172.16.20.0/24" = local.availability_zones[1],
   "172.16.30.0/24" = local.availability_zones[2]
 }

 private_subnets_cidr_az_mapping = {
   "172.16.40.0/24" = local.availability_zones[0],
   "172.16.50.0/24" = local.availability_zones[1],
   "172.16.60.0/24" = local.availability_zones[2]
 }

 resource_tags = {
   "ManagedBy" : "Terraform"
 }
}

Creating the VPC

resource "aws_vpc" "main" {
 cidr_block           = local.vpc_cir_block
 instance_tenancy     = "default"
 enable_dns_hostnames = false

 tags = merge(
   { "Name" = var.vpc_name },
   local.resource_tags,
 )
}

Configuring the public subnets

As earlier stated, a public subnet is one with a route to the Internet Gateway. To better understand the steps for creating a public subnet, please take a look at the diagram shared in the overview and see how the following steps build it;

  1. Create the public subnets

resource "aws_subnet" "public" {
 for_each          = local.public_subnets_cidr_az_mapping
 vpc_id            = aws_vpc.main.id
 cidr_block        = each.key
 availability_zone = each.value

 tags = merge(
   { Name = "${var.vpc_name}-public-${each.value}" },
   local.resource_tags
 )
}
  1. Create a route table for the public subnets

resource "aws_route_table" "public" {
 vpc_id = aws_vpc.main.id

 tags = merge(
   { Name = "${var.vpc_name}-public" },
   local.resource_tags
 )
}
  1. Associate all public subnets with the public route table

resource "aws_route_table_association" "public" {
 for_each       = aws_subnet.public
 subnet_id      = each.value.id
 route_table_id = aws_route_table.public.id
}
  1. Create an Internet Gateway for the public subnets

resource "aws_internet_gateway" "main" {
 vpc_id = aws_vpc.main.id

 tags = merge(
   { Name = "${var.vpc_name}-igw" },
   local.resource_tags
 )
}
  1. Add a route to the Internet Gateway in the public route table

resource "aws_route" "igw" {
 route_table_id         = aws_route_table.public.id
 destination_cidr_block = local.public_internet_cidr
 gateway_id             = aws_internet_gateway.main.id
}

With the above, we have a VPC and public subnets in which resources — such as EC2 instances — that can reach the internet and also be reachable from the internet can be deployed.

Configuring the private subnets

As stated, private subnets do not have a direct route to the Internet Gateway. Instances in private subnets can access the internet through a NAT Gateway or NAT device. The following steps configure a public subnet to match what is shown in the diagram in the overview;

  1. Create the private subnets

resource "aws_subnet" "private" {
 for_each          = local.private_subnets_cidr_az_mapping
 vpc_id            = aws_vpc.main.id
 cidr_block        = each.key
 availability_zone = each.value

 tags = merge(
   { Name = "${var.vpc_name}-private-${each.value}" },
   local.resource_tags
 )
}
  1. Create a route table for the private subnets

resource "aws_route_table" "private" {
 vpc_id = aws_vpc.main.id

 tags = merge(
   { Name = "${var.vpc_name}-private" },
   local.resource_tags
 )
}
  1. Associate all private subnets with the private route table

resource "aws_route_table_association" "private" {
 for_each       = aws_subnet.private
 subnet_id      = each.value.id
 route_table_id = aws_route_table.private.id
}
  1. Configure internet access for the private subnets

  2. Provision an Elastic IP address for use by the NAT Gateway
resource "aws_eip" "nat" {
 domain = "vpc"

 tags = merge(
   { Name = "${var.vpc_name}-natgw-${local.nat_gateway_az}" },
   local.resource_tags
 )
 depends_on = [aws_internet_gateway.main]
}
  1. Create public NAT gateway
resource "aws_nat_gateway" "public" {
 allocation_id = aws_eip.nat.id
 subnet_id     = local.nat_gateway_subnet["id"]

 tags = merge(
   { Name = "${var.vpc_name}-public-nat-${local.nat_gateway_az}" },
   local.resource_tags
 )
 depends_on = [aws_internet_gateway.main]
}
  1. Add a route to NAT gateway in the private subnets' route table
resource "aws_route" "nat" {
 route_table_id         = aws_route_table.private.id
 destination_cidr_block = local.public_internet_cidr
 nat_gateway_id         = aws_nat_gateway.public.id
}

Visualization of the created VPC resources

Notes

  • Each of the route tables has two routes although we only explicitly configured one. The extra route is a default route that is automatically added by AWS for communication within the VPC subnets.

  • For a more resilient setup, create a NAT gateway for each private subnet. Because they come at a cost, we’ve only specified and used one for all private subnets.

  • We did not explicitly configure network ACLs. In this case, we are relying on the default ACLs configured by AWS — these allow all inbound and outbound traffic.

  • We did not explicitly configure Security Groups. We are relying on the default Security Groups configured by AWS — these only allow inbound and outbound traffic to/from instances within the VPC.

  • A shorter way to configure the VPC is to use Terraform AWS modules. This is an ideal option when you fully understand the concepts around VPCs in AWS. Below is the equivalent configuration to setup the VPC above using the Terraform AWS modules

      module "vpc" {
        source  = "terraform-aws-modules/vpc/aws"
        version = "5.12.0"
    
        name               = var.vpc_name
        cidr               = local.vpc_cir_block
        azs                = local.availability_zones
        private_subnets    = local.private_subnets_cidr_az_mapping
        public_subnets     = local.public_subnets_cidr_az_mapping
        enable_nat_gateway = true
        single_nat_gateway = true
    
        tags = local.resource_tags
      }
    

Resources