Create a VPC

Tech: AWS - Terraform
Posted: 2021-02-18
Last Updated: 2021-02-18

What is a VPC

In this post I am going to cover some of the basics of an AWS VPC and show how I deploy one using Terraform

For some basic terminology we can look at docs from AWS - Amazon VPC Concepts



Amazon Virtual Private Cloud (Amazon VPC) enables you to launch AWS resources into a virtual network that you've defined. This virtual network closely resembles a traditional network that you'd operate in your own data center, with the benefits of using the scalable infrastructure of AWS.
The following are the key concepts for VPCs:
  • Virtual private cloud (VPC) — A virtual network dedicated to your AWS account.
    • There's no additional charge for using a VPC. There are charges for the following VPC components: Site-to-Site VPN connection, PrivateLink, Traffic Mirroring, and a NAT gateway. For more information, see Amazon VPC Pricing.
  • Subnet — A subnet is a range of IP addresses in your VPC. You can launch AWS resources into a specified subnet. Use a public subnet for resources that must be connected to the internet, and a private subnet for resources that won't be connected to the internet .
  • Route table — A set of rules, called routes, that are used to determine where network traffic is directed. You can explicitly associate a subnet with a particular route table. Otherwise, the subnet is implicitly associated with the main route table.
  • Internet gateway — A gateway that you attach to your VPC to enable communication between resources in your VPC and the internet.
  • VPC endpoint — Enables you to privately connect your VPC to supported AWS services and VPC endpoint services powered by PrivateLink without requiring an internet gateway, NAT device, VPN connection, or AWS Direct Connect connection. Instances in your VPC do not require public IP addresses to communicate with resources in the service. Traffic between your VPC and the other service does not leave the Amazon network. For more information, see AWS PrivateLink and VPC endpoints.
  • CIDR block —Classless Inter-Domain Routing. An internet protocol address allocation and route aggregation methodology. For more information, see Classless Inter-Domain Routing in Wikipedia.
From Wikipedia -
The number of addresses inside a network or subnet may be calculated as 2address length − prefix length, where address length is 128 for IPv6 and 32 for IPv4. For example, in IPv4, the prefix length /29 gives: 2 ^ 32 − 29 = 2^3 = 8 addresses.


Network Planner

Looking at Wikipedia we can see CIDR masks that give us a starting point for planning a network
To plan out our VPC we will use a network planner tool - IPv4 Address Planner

We will give our VPC a 19 mask (2^32-19 = 2^13 = 8,092 addresses) and assume a uniform subnet size of 24 (2^32-24 = 2^8 = 256 addresses) which will give us room for 20 subnets in our VPC as shown by this plan:
We will use this information to construct a VPC that looks like this:
Now we can begin to create out resource definitions in Terraform working from the outside in of our diagram.

Code located here

The Region is defined by the provider
1 2 3provider "aws" { region = "us-west-2" }
We will use terraform to manage our default route table that comes with our vpc and define a separate one for our public subnet. We create a route in our main route table for our nat gateway and create a route for our internet gateway for our public subnet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41## Create main route table resource "aws_default_route_table" "main" { default_route_table_id = aws_vpc.vpc.default_route_table_id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.i.id } tags = merge( var.base_tags, { Name = "${var.resource_prefix}-main-rt" }, ) } ## Create route table resource "aws_route_table" "i_public" { vpc_id = aws_vpc.vpc.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } tags = merge( var.base_tags, { Name = "${var.resource_prefix}-rt" }, ) } ## Associate route tables with subnets resource "aws_route_table_association" "i_subnet_private" { subnet_id = aws_subnet.subnet_private.id route_table_id = aws_default_route_table.main.id } resource "aws_route_table_association" "i_subnet_public" { subnet_id = aws_subnet.subnet_public.id route_table_id = aws_route_table.i_public.id }
The VPC is defined by the aws_vpc resource using a variable to pass in the cidr block
1 2 3 4 5 6 7 8 9 10 11 12 13 14## Create vpc resource "aws_vpc" "vpc" { cidr_block = var.cidr_vpc instance_tenancy = "default" enable_dns_support = true enable_dns_hostnames = true tags = merge( var.base_tags, { Name = "${var.resource_prefix}-vpc" }, ) }
We attach an internet gateway to the VPC with the aws_internet_gateway resource using the vpc id
1 2 3 4 5 6 7 8 9 10## Create internet gateway resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.vpc.id tags = merge( var.base_tags, { Name = "${var.resource_prefix}-igw" }, ) }
Then we define a nat gateway to allow our private subnet instances to egress to the internet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21resource "aws_nat_gateway" "i" { allocation_id = aws_eip.i.id subnet_id = aws_subnet.subnet_public.id tags = merge( var.base_tags, { Name = "${var.resource_prefix}-natgw" }, ) } resource "aws_eip" "i" { vpc = true tags = merge( var.base_tags, { Name = "${var.resource_prefix}-eip" }, ) }
We can then create our private and public subnets using the aws_subnet resource and again using variables for the subnet cidr blocks and availability zones
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29## Create public subnet resource "aws_subnet" "subnet_public" { vpc_id = aws_vpc.vpc.id cidr_block = var.cidr_subnet_public map_public_ip_on_launch = "true" availability_zone = var.availability_zone tags = merge( var.base_tags, { Name = "${var.resource_prefix}-vpc-subnet-public" Access = "public" }, ) } ## Create private subnet resource "aws_subnet" "subnet_private" { vpc_id = aws_vpc.vpc.id cidr_block = var.cidr_subnet_private map_public_ip_on_launch = "false" availability_zone = var.availability_zone tags = merge( var.base_tags, { Name = "${var.resource_prefix}-vpc-subnet-private" Access = "private" }, ) }
Within our terraform directory
We run terraform init to generate our local state
Then we can run a plan to see what will be created
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215terraform plan An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_default_route_table.main will be created + resource "aws_default_route_table" "main" { + default_route_table_id = (known after apply) + id = (known after apply) + owner_id = (known after apply) + route = [ + { + cidr_block = "0.0.0.0/0" + egress_only_gateway_id = "" + gateway_id = "" + instance_id = "" + ipv6_cidr_block = "" + nat_gateway_id = (known after apply) + network_interface_id = "" + transit_gateway_id = "" + vpc_endpoint_id = "" + vpc_peering_connection_id = "" }, ] + tags = { + "Name" = "sample_company-main-rt" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc_id = (known after apply) } # aws_eip.i will be created + resource "aws_eip" "i" { + allocation_id = (known after apply) + association_id = (known after apply) + carrier_ip = (known after apply) + customer_owned_ip = (known after apply) + domain = (known after apply) + id = (known after apply) + instance = (known after apply) + network_border_group = (known after apply) + network_interface = (known after apply) + private_dns = (known after apply) + private_ip = (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) + public_ipv4_pool = (known after apply) + tags = { + "Name" = "sample_company-eip" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc = true } # aws_internet_gateway.igw will be created + resource "aws_internet_gateway" "igw" { + arn = (known after apply) + id = (known after apply) + owner_id = (known after apply) + tags = { + "Name" = "sample_company-igw" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc_id = (known after apply) } # aws_nat_gateway.i will be created + resource "aws_nat_gateway" "i" { + allocation_id = (known after apply) + id = (known after apply) + network_interface_id = (known after apply) + private_ip = (known after apply) + public_ip = (known after apply) + subnet_id = (known after apply) + tags = { + "Name" = "sample_company-natgw" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } } # aws_route_table.i_public will be created + resource "aws_route_table" "i_public" { + id = (known after apply) + owner_id = (known after apply) + propagating_vgws = (known after apply) + route = [ + { + cidr_block = "0.0.0.0/0" + egress_only_gateway_id = "" + gateway_id = (known after apply) + instance_id = "" + ipv6_cidr_block = "" + local_gateway_id = "" + nat_gateway_id = "" + network_interface_id = "" + transit_gateway_id = "" + vpc_endpoint_id = "" + vpc_peering_connection_id = "" }, ] + tags = { + "Name" = "sample_company-rt" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc_id = (known after apply) } # aws_route_table_association.i_subnet_private will be created + resource "aws_route_table_association" "i_subnet_private" { + id = (known after apply) + route_table_id = (known after apply) + subnet_id = (known after apply) } # aws_route_table_association.i_subnet_public will be created + resource "aws_route_table_association" "i_subnet_public" { + id = (known after apply) + route_table_id = (known after apply) + subnet_id = (known after apply) } # aws_subnet.subnet_private will be created + resource "aws_subnet" "subnet_private" { + arn = (known after apply) + assign_ipv6_address_on_creation = false + availability_zone = "us-west-2a" + availability_zone_id = (known after apply) + cidr_block = "10.1.1.0/24" + id = (known after apply) + ipv6_cidr_block_association_id = (known after apply) + map_public_ip_on_launch = false + owner_id = (known after apply) + tags = { + "Access" = "private" + "Name" = "sample_company-vpc-subnet-private" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc_id = (known after apply) } # aws_subnet.subnet_public will be created + resource "aws_subnet" "subnet_public" { + arn = (known after apply) + assign_ipv6_address_on_creation = false + availability_zone = "us-west-2a" + availability_zone_id = (known after apply) + cidr_block = "10.1.0.0/24" + id = (known after apply) + ipv6_cidr_block_association_id = (known after apply) + map_public_ip_on_launch = true + owner_id = (known after apply) + tags = { + "Access" = "public" + "Name" = "sample_company-vpc-subnet-public" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } + vpc_id = (known after apply) } # aws_vpc.vpc will be created + resource "aws_vpc" "vpc" { + arn = (known after apply) + assign_generated_ipv6_cidr_block = false + cidr_block = "10.1.0.0/19" + default_network_acl_id = (known after apply) + default_route_table_id = (known after apply) + default_security_group_id = (known after apply) + dhcp_options_id = (known after apply) + enable_classiclink = (known after apply) + enable_classiclink_dns_support = (known after apply) + enable_dns_hostnames = true + enable_dns_support = true + id = (known after apply) + instance_tenancy = "default" + ipv6_association_id = (known after apply) + ipv6_cidr_block = (known after apply) + main_route_table_id = (known after apply) + owner_id = (known after apply) + tags = { + "Name" = "sample_company-vpc" + "createdBy" = "terraform" + "directory" = "terraform_vpc" + "owner" = "Sample Company" } } Plan: 10 to add, 0 to change, 0 to destroy. Changes to Outputs: + private_subnet_id = (known after apply) + public_subnet_id = (known after apply) + vpc_id = (known after apply) ------------------------------------------------------------------------ Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee that exactly these actions will be performed if "terraform apply" is subsequently run.
And then we can run terraform apply to create (hint: the provider override file contains my aws credentials)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42terraform apply -auto-approve aws_eip.i: Creating... aws_vpc.vpc: Creating... aws_eip.i: Creation complete after 1s [id=eipalloc-07a5b6e9969679e9a] aws_vpc.vpc: Still creating... [10s elapsed] aws_vpc.vpc: Creation complete after 13s [id=vpc-0ee271c94e219deea] aws_internet_gateway.igw: Creating... aws_subnet.subnet_public: Creating... aws_subnet.subnet_private: Creating... aws_internet_gateway.igw: Creation complete after 1s [id=igw-083b659e6f53b4b9b] aws_route_table.i_public: Creating... aws_subnet.subnet_private: Creation complete after 1s [id=subnet-017347820c7bb83c8] aws_route_table.i_public: Creation complete after 1s [id=rtb-084443e46fff84c82] aws_subnet.subnet_public: Still creating... [10s elapsed] aws_subnet.subnet_public: Creation complete after 11s [id=subnet-0edd369fe4214404d] aws_route_table_association.i_subnet_public: Creating... aws_nat_gateway.i: Creating... aws_route_table_association.i_subnet_public: Creation complete after 1s [id=rtbassoc-0ebf833870a641f5f] aws_nat_gateway.i: Still creating... [10s elapsed] aws_nat_gateway.i: Still creating... [20s elapsed] aws_nat_gateway.i: Still creating... [30s elapsed] aws_nat_gateway.i: Still creating... [40s elapsed] aws_nat_gateway.i: Still creating... [50s elapsed] aws_nat_gateway.i: Still creating... [1m0s elapsed] aws_nat_gateway.i: Still creating... [1m10s elapsed] aws_nat_gateway.i: Still creating... [1m20s elapsed] aws_nat_gateway.i: Still creating... [1m30s elapsed] aws_nat_gateway.i: Still creating... [1m40s elapsed] aws_nat_gateway.i: Still creating... [1m50s elapsed] aws_nat_gateway.i: Creation complete after 1m57s [id=nat-03043971b4a8e4147] aws_default_route_table.main: Creating... aws_default_route_table.main: Creation complete after 1s [id=rtb-095d0340369c30ec2] aws_route_table_association.i_subnet_private: Creating... aws_route_table_association.i_subnet_private: Creation complete after 1s [id=rtbassoc-04cc0aad7e390cefd] Apply complete! Resources: 10 added, 0 changed, 0 destroyed. Outputs: private_subnet_id = "subnet-017347820c7bb83c8" public_subnet_id = "subnet-0edd369fe4214404d" vpc_id = "vpc-0ee271c94e219deea"
Then we can take a look at the AWS Console to see the results of our terraform run:

As we can see all our resources were created.

To clean it all up again we can just run destroy:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32terraform destroy -auto-approve aws_route_table_association.i_subnet_private: Destroying... [id=rtbassoc-04cc0aad7e390cefd] aws_route_table_association.i_subnet_public: Destroying... [id=rtbassoc-0ebf833870a641f5f] aws_route_table_association.i_subnet_private: Destruction complete after 0s aws_subnet.subnet_private: Destroying... [id=subnet-017347820c7bb83c8] aws_default_route_table.main: Destroying... [id=rtb-095d0340369c30ec2] aws_default_route_table.main: Destruction complete after 0s aws_nat_gateway.i: Destroying... [id=nat-03043971b4a8e4147] aws_route_table_association.i_subnet_public: Destruction complete after 0s aws_route_table.i_public: Destroying... [id=rtb-084443e46fff84c82] aws_subnet.subnet_private: Destruction complete after 1s aws_route_table.i_public: Destruction complete after 1s aws_internet_gateway.igw: Destroying... [id=igw-083b659e6f53b4b9b] aws_nat_gateway.i: Still destroying... [id=nat-03043971b4a8e4147, 10s elapsed] aws_internet_gateway.igw: Still destroying... [id=igw-083b659e6f53b4b9b, 10s elapsed] aws_nat_gateway.i: Still destroying... [id=nat-03043971b4a8e4147, 20s elapsed] aws_internet_gateway.igw: Still destroying... [id=igw-083b659e6f53b4b9b, 20s elapsed] aws_nat_gateway.i: Still destroying... [id=nat-03043971b4a8e4147, 30s elapsed] aws_internet_gateway.igw: Still destroying... [id=igw-083b659e6f53b4b9b, 30s elapsed] aws_nat_gateway.i: Still destroying... [id=nat-03043971b4a8e4147, 40s elapsed] aws_internet_gateway.igw: Still destroying... [id=igw-083b659e6f53b4b9b, 40s elapsed] aws_internet_gateway.igw: Destruction complete after 46s aws_nat_gateway.i: Still destroying... [id=nat-03043971b4a8e4147, 50s elapsed] aws_nat_gateway.i: Destruction complete after 51s aws_eip.i: Destroying... [id=eipalloc-07a5b6e9969679e9a] aws_subnet.subnet_public: Destroying... [id=subnet-0edd369fe4214404d] aws_subnet.subnet_public: Destruction complete after 1s aws_vpc.vpc: Destroying... [id=vpc-0ee271c94e219deea] aws_vpc.vpc: Destruction complete after 0s aws_eip.i: Destruction complete after 1s Destroy complete! Resources: 10 destroyed.

Comments