Terraform – AWS VPCs

Talks about VPCs, Subnets & CIDR blocks, Routing tables, Internet Gateway and NAT Gateway

Introduction

Wow – Finally – made it to VPCs. This one I always wanted to write. VPCs are at the very heart of AWS – VPC stands for Virtual Private Cloud as the name suggests your own cloud! your own thing. All the resources you create reside inside a VPC. Till now we have only used the default-vpc. No more from this blog onwards – exciting world awaits. When you are inside a VPC – it isolates your resources on the network level to a point where if you need two resources which are across the VPCs it requires quite a special effort(i.e mystical beast called VPC peering). You can create as many VPCs as you want.

This blog post is mostly theoretical concepts about two concepts – Subnets and Routing. While you are inside these concepts we will also discuss big bad CIDR blocks, before touching Internet & NAT Gateways and finally finishing it all off with some terraform code.

Trust me, it is not that difficult to grasp these concepts as people may make you believe. If you have been working on the cloud it’s not that difficult- well bring it on!

When you first create your AWS account – AWS creates a few default components for you. They are

  • Default VPC
  • Public Subnet in each Availability Zone(AZ)
  • Internet Gateway attached to the VPC

They are shown below.

default VPC

The name of the availability zone(AZ) may change depending upon which region you are creating your VPC. The resources can be created inside the subnet. You can explore this in your AWS Console as well.

default VPC
default Subnets

Note: There is a default security group also created but it is not shown in the diagram above. Each resource is assigned a security group which can go across subnets and AZs. More on that later in the post.

VPC & Subnets

The process of dividing your network into smaller networks is called subnetting. Subnets help you create compartmentalize your resources. They do so by assigning IP addresses based on a given IP address range. Each resource created inside a subnet is given an IP address from the address range which has been assigned to the subnet. The question which pops to mind is how do I provide this IP address range. The answer lies in CIDR blocks.

CIDR Blocks

Going back into Networking Theory(which I did not care in college days!) Let’s try to understand traditional subnetting. In the traditional world of networking, for a subnet, we would have an IP address and netmask/bitmask. For example, IP address of 192.168.0.1 and netmask of 255.255.255.0. IP addresses and Netmask are made up of 4 octets each.

An IP address of 192.168.0.1 has four octets – Octet 1 is 192, Octet 2 is 168, Octet 3 is 0 and Octet 4 is 1. Octet is any information presented in 8bits. Each IP address and a netmask is made up of 4 octets.

A netmask/bitmask of 255.255.255.0 meant few things

  • First 24bits (8 bits from each octet) of the IP address represent the subnet and are are static. the last octet represents the host.
  • It also alluded to the number of IP addresses available inside the subnet(remember the last octet was zero!). In this case 28 or 256. From 192.168.0.0 to 192.168.0.255. Only the last octet kept varying. Also known as IP address range.

Now, this type of IP address and netmask notation had its shortcomings and issues(not delving into those) but to resolve all of that a new standard was created called CIDR. CIDR stands for Classless inter-domain routing. Well just remember CIDR for short 😋. Our example above can simply be written in CIDR form as 192.168.0.1/24.

Have a read here. Very Very helpful especially this IPv4 section. This subsection takes a lot of pain out of what we are going to do in the next coming sections. Phew! let’s get back to our world of cloud.

So, we have now got an idea that all subnets have a CIDR block assigned to them. Keep in mind, VPC also has a CIDR block assigned to it. Because it is also an isolated network. See below the screen grab of the default-vpc from AWS console.

default-vpc CIDR block

Private Subnets

Whenever, a subnet is provisioned, by default it does not have any access outside the VPC. It is also assigned a CIDR range. So it looks like below. Now if you are wondering if you cannot access the resources created in a private subnet what use do those resources have. Well, they can still be accessed by other resources in the VPC(across the subnets) – if the security groups allow!!!. You definitely don’t want the outside world to access your database directly!!.

Let’s analyse this diagram

  • VPC – cw-vpc-1 which has 216 or just about 65K IP addresses possible.
  • Private Subnet – cw-pvt-subnet-1 which has 28 or 256 IP addresses possible.

Keep this in mind – we will be having this subnet in our terraform code.

Public Subnets

Public subnets are subnets which have access to the internet. Access to the internet is provided via Internet Gateway(more on that later) which means that these resources have public IP addresses in addition to private IP addresses. Which also means that they can be accessed from the internet. Extending our previous diagram to include a public subnet. So something like this below

Let’s analyse this diagram

  • VPC – cw-vpc-1 which has 216 or just about 65K IP addresses available.
  • Private Subnet – cw-pvt-subnet-1 which has 28 or just about 256 IP addresses available. Cannot access resources on the internet or services on the internet can access the components inside the subnet
  • Public Subnet – cw-pub-subnet-2 which has 28 or just about 256 IP addresses available.

Now that let’s add a our security groups to the mix of things!

Security Groups

Each Security group is assigned to only one VPC. They operate across the subnets and hence also across the AZs. So if you have two EC2 instances each sitting in a different subnets in the same VPC the same security group can be applied if required. Extending our architecture diagram. See below

Let’s analyse the diagram above

  • VPC – cw-vpc-1 which has 216 or just about 65K IP addresses available.
  • Private Subnet – cw-pvt-subnet-1 which has 28 or just about 256 IP addresses available. Cannot access resources on the internet or services on the internet can access the components inside the subnet
  • Public Subnet – cw-pub-subnet-2 which has 28 or just about 256 IP addresses available.
  • Security group – cw-pvt-sg-1. Which only allows ssh and HTTP traffic to the ec2 instances. Security groups are specific to a VPC and can be assigned in any availability zone.

VPC & Traffic Routing

Now we have the basic components in place. We understand public and private subnets. We also understand that resources created in public subnets have a public IP address in addition to a private IP address. Let’s dig a bit deeper and understand how the flow of network traffic occurs between these components. In this section, we will discuss three new components. Internet Gateway, NAT Gateway, most important routes and routing tables and finally routing associations.

Internet Gateway

Internet Gateway is a service which allows traffic from the internet to access your applications. For example, if you have a web application which should be accessed via the internet, then it should be in a subnet which has an internet gateway attached to it. That application may intern access resources – like another EC2 instance or a database in the private subnet, you do not want your database to be directly accessible on the internet!!!

BTW – In the default, aws VPC there is already a default Internet Gateway linked to it.

NAT Gateway

NAT Gateways solve a very interesting problem. Here is a use case – say suppose you have a server in the private subnet. Now this server needs to update its server software from the internet but the resources on the internet should not be able to access it. An example of that may be your build server like Jenkins or a nexus repository. Both are usually internal servers but need access to the internet to download software from the internet.

Routes and Routing Tables

A route table consists of rules which define where and how the network traffic is directed from any subnet or gateway. These rules are also known as routes. Each route consists of two items of configuration

  • Destination – IP addresses to which the network traffic needs to go. Remember CIDR blocks 😉
  • Target – The service used to send the network traffic to those destination IP addresses.

Let’s look at an example below

Finally, let’s look at route associations. It is not that difficult but important all the same. Once a route table is created it needs to be associated with a subnet. So that subnet can start sending network traffic around and communicate. This is handled by a route association.

Note: A routing table can be assigned to one or more subnets. However, a subnet can have only one routing table associated with it.

Finally, this brings us to the part we have all been dying to see – the code.

Terraform Code

Below is the diagram of what we are planning to do. We will create an architecture with the following AWS resources. I have blanked out the ssh keys so you would need to set those up for yourself in the code. Here is the link to GitHub repo.

  • One VPC
  • Three public subnets
  • Two private subnets
  • Two security groups
  • One internet gateway
  • One Elastic IP address
  • One NAT gateway
  • Two EC2 instances

See the final architecture below.

Let’s start with what with creating a vpc – cw-vpc-1.

Step 1 – Create a simple VPC configuration – vpc.tf
resource "aws_vpc" "cw_vpc_1" {
  cidr_block = var.CW_VPC_CIDR_BLOCK
  instance_tenancy = "default"
  enable_dns_support = "true"
  enable_dns_hostnames = "true"
  enable_classiclink = "false"

  tags = {
    Name = var.CW_VPC_NAME
  }
}

Let’s quickly look at what we are trying to achieve

  • CIDR Block is provided as a variable. Refer to the whole section of cidr blocks above.
  • instance_tenancy – Tells AWS that we want to create our EC2 instances on shared infrastructure by default.
  • enable_dns_support – This when set to true means that AWS Route 53 to resolve the hostnames to correct IP addresses.
  • enable_dns_hostnames – This means AWS will provide the EC2 instance with a public address with a public hostname based on IP address.
  • Name – This is a special tag which gives name to the VPC.

At then of this we have the following architecture. Pretty empty picture but still is an important step in what we are trying to achieve.

Step-1
Step 2 – Create private subnets – private-subnets.tf

For the purpose of this blog entry we will create two private subnets

# Provision private subnets
resource "aws_subnet" "main-private-1" {
  cidr_block = var.CW_PRIVATE_SUBNETS["eu-west-2a"]
  vpc_id = aws_vpc.cw_vpc_1.id
  map_public_ip_on_launch = "false"
  availability_zone = "eu-west-2a"

  tags = {
    Name = var.CW_PRIVATE_SUBNET_NAMES["eu-west-2a"]
  }
}

resource "aws_subnet" "main-private-2" {
  cidr_block = var.CW_PRIVATE_SUBNETS["eu-west-2b"]
  vpc_id = aws_vpc.cw_vpc_1.id
  map_public_ip_on_launch = "false"
  availability_zone = "eu-west-2b"

  tags = {
    Name = var.CW_PRIVATE_SUBNET_NAMES["eu-west-2b"]
  }
}

At the end of step 2 your architecture will look like this

Step 2
Step 3 – Create public subnets – public-subnets.tf
# public subnets
resource "aws_subnet" "main-public-1" {
  cidr_block = var.CW_PUBLIC_SUBNETS["eu-west-2a"]
  vpc_id = aws_vpc.cw_vpc_1.id
  map_public_ip_on_launch = "true"
  availability_zone = "eu-west-2a"

  tags = {
    Name = var.CW_PUBLIC_SUBNET_NAMES["eu-west-2a"]
  }
}

resource "aws_subnet" "main-public-2" {
  cidr_block = var.CW_PUBLIC_SUBNETS["eu-west-2b"]
  vpc_id = aws_vpc.cw_vpc_1.id
  map_public_ip_on_launch = "true"
  availability_zone = "eu-west-2b"

  tags = {
    Name = var.CW_PUBLIC_SUBNET_NAMES["eu-west-2b"]
  }
}

At the end of step 3 your architecture will look like this

Step 3
Step 4 – Create internet gateway – internet-gateway.tf

Let’s start putting together an internet gateway configuration. See below

resource "aws_internet_gateway" "cw_ig" {
  vpc_id = aws_vpc.cw_vpc_1.id
 
  tags = {
    Name = var.CW_IG_NAME
  }
}

At the end of step 4 your architecture will look like this

Step 5 – Create NAT gateway – nat-gateway.tf

Creation of NAT gateway is a slightly more involved process as it requires a public IP address. It requires us to do the following

  • Create an elastic IP address
  • Create a subnet for NAT gateway
  • Create the NAT gateway
    • Assign a public IP address
    • Assign a public subnet
    • Associate an internet gateway to access the internet
resource "aws_eip" "cw-nat-elastic-ip" {
  vpc = true

  tags = {
    Name = "Elastic-IP-for-NAT-Gateway"
  }
}

resource "aws_subnet" "main-nat-subnet" {
  cidr_block = var.CW_PUBLIC_SUBNETS["eu-west-2b"]
  vpc_id = aws_vpc.cw_vpc_1.id
  map_public_ip_on_launch = "true"
  availability_zone = "eu-west-2b"

  tags = {
    Name = var.CW_NAT_SUBNET_NAME
  }
}

resource "aws_nat_gateway" "cw-nat-gateway" {
  allocation_id = aws_eip.cw-nat-elastic-ip
  subnet_id = aws_subnet.main-nat-subnet.id
  depends_on = [aws_internet_gateway.cw_ig]

  tags = {
    Name = var.CW_NAT_NAME
  }
}

At the end of step 5 your architecture will look like

Step 5
Step 6 – Create route tables – route-tables.tf

In this step we will create two route tables. One each for public and private subnets.

resource "aws_route_table" "cw_public_route" {
  vpc_id = aws_vpc.cw_vpc_1.id

  route {
    cidr_block = "0.0.0.0/0" # All resources in public subnet are accessible from all internet.
    gateway_id = aws_internet_gateway.cw_ig.id
  }

  tags = {
    Name = "CW-Public-route"
  }
}

resource "aws_route_table" "cw_private_route" {
  vpc_id = aws_vpc.cw_vpc_1.id
  route {
    cidr_block     = "0.0.0.0/0" # Access all internet. But none from internet.
    nat_gateway_id = aws_nat_gateway.cw-nat-gateway.id
  }

  tags = {
    Name = "CW-Private-route"
  }
}

At the end of step 6 your architecture will look like

Step 7 – Associate routes with subnets – route-associations.tf

Each subnet is now associated with a route via a route association resource.

  • Public subnets are assigned Internet gateway. It means all the resources in the public subnet will be accessiable from the internet for the servers given in CIDR range for the internet gateway.
  • Privates subnets are assigned NAT Gateway if their resource need to access internet but no servers on the internet would be able to access the resources inside the subnet.
resource "aws_route_table_association" "cw-main-public-1-ra" {
  route_table_id = aws_route_table.cw_public_route.id
  subnet_id = aws_subnet.main-public-1.id
}

resource "aws_route_table_association" "cw-main-public-2-ra" {
  route_table_id = aws_route_table.cw_public_route.id
  subnet_id = aws_subnet.main-public-2.id
}

resource "aws_route_table_association" "cw-main-private-1-ra" {
  route_table_id = aws_route_table.cw_private_route.id
  subnet_id = aws_subnet.main-private-1.id
}

resource "aws_route_table_association" "cw-main-private-2-ra" {
  route_table_id = aws_route_table.cw_private_route.id
  subnet_id = aws_subnet.main-private-2.id
}

At the end of step 7 your architecture will look like

Step 8 – Create security groups for EC2 instances – security-groups.tf

The security groups are configured in such a way that. The security group have the following configuration

  • Only resources inside of the public security group would be able to access the resources inside the private security group.
  • Both security groups only allow SSH connections.
  • Resources inside public security group can be accessed from a certain public IP address
# Any resources in this security group are only accessible via ssh
resource "aws_security_group" "cw_public_sg" {
  name = var.CW_PUBLIC_SG
  vpc_id = aws_vpc.cw_vpc_1.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = var.CW_SG_PUBLIC_CIDR_BLOCKS
  }

  egress {
    from_port = 0
    protocol = "-1"
    to_port = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Any resources in this security group are only accessible via public subnet main-public-1 subnet IP address
# only SSH access is allowed.
resource "aws_security_group" "cw_private_sg" {
  name = var.CW_PRIVATE_SG
  vpc_id = aws_vpc.cw_vpc_1.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    security_groups = [aws_security_group.cw_public_sg.id]
  }

  egress {
    from_port = 0
    protocol = "-1"
    to_port = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

At the end of step 8 your architecture should look like this below.

Step 9 – Create EC2 instances – instances.tf

Finally, the last step is to create EC2 instances which will reside inside the subnets of the VPC and would be associated with security groups.

resource "aws_instance" "cw_pub_instance" {
  ami = "ami-0fc841be1f929d7d1"
  instance_type = var.CW_INSTANCE_TYPE
  subnet_id = aws_subnet.main-public-1.id
  vpc_security_group_ids = [aws_security_group.cw_public_sg.id]
  key_name = aws_key_pair.my_blog_key.key_name

  tags = {
    Name = "CW-Public-instance"
  }
}

resource "aws_instance" "cw_pvt_instance" {
  ami = "ami-0fc841be1f929d7d1"
  instance_type = var.CW_INSTANCE_TYPE
  subnet_id = aws_subnet.main-private-1.id
  key_name = aws_key_pair.my_blog_key.key_name
  vpc_security_group_ids = [aws_security_group.cw_private_sg.id]

  tags = {
    Name = "CW-Private-instance"
  }
}


output "cw_ec2_pub_instance" {
  value = aws_instance.cw_pub_instance.public_ip
}

At the end of step-9 your architecture looks like this. Finally we have two EC2 nodes which we can see.

All the code is also available on github on this link. Before we close blog entry this let’s see all of it in action.

Demo

Step 1 – Run terraform apply

Step 2 – SSH into the public instance

Step 3 – SSH into private instance from public instance

Use the private IP address to ssh into the private instance.

This brings us to the end of this long blog post. The next few posts will be exploring some more interesting aspects of VPC like endpoints, Transit gateways and peering. I hope you find this post helpful. If you like this post please do share it!

Leave a Comment