Infrastructure as a Code (Part 2) AWS Networking (VPC)

Faroug Mohammed
14 min readAug 2, 2022

Welcome to the second part of the infrastructure as Code using Terraform to leverage the Network on AWS infrastructure.
Refer to this Part one if you are not familiar with Terraform.

Don’t worry about the code. you can clone the repository with branches that explained each part here → https://github.com/FAROUG/aws_infrastructure_terraform.git

In this part, we will demonstrate how we can create virtual private clouds (VPCs), Subnets, Route Tables, NAT, Internet Gateways, VPC Peering, Elastic IP address, etc.

Virtual Private Cloud (VPC)

In nutshell, 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 data centre, with the benefits of using the scalable infrastructure of AWS.

How it works

Amazon Virtual Private Cloud (Amazon VPC) gives you full control over your virtual networking environment, including resource placement, connectivity, and security. You can launch resources such as Amazon Elastic Compute Cloud (EC2) and Amazon Relational Database Service (RDS) instances. You can also define how your VPCs communicate with each other across accounts, Availability Zones, or AWS Regions, or within the same region as we will see. In the example below, network traffic is being shared between two VPCs within each Region. »

https://medium.com/r/?url=https%3A%2F%2Faws.amazon.com%2Fvpc%2F

so let’s start by defining two VPCs.

  • Public VPC → will contain private like (web servers, application servers, log-stash servers, etc) and public subnets to provision public servers like a bastion host.
  • Private VPC → used to provision the private resources that we don’t want to have public access to like Database server, DocumentDB, etc.

The idea of creating two VPCs is for more security and isolation as to run the private Clusters (documentDB, ElasticSearch, RDS, etc ) in a complete private VPC and by using VPC peering and route table we can delegate access to allow specific or all the private subnets of the Public VPC to communicate with this private subnets of the Private VPC.

The diagram below is an illustration of the public and private VPCs with public and private subnets in N. Virginia region, however, will automate the creation of the Subnets based on the available availability zones in that region.

by using the resource block in Terraform will declare two VPCs with local names that we will use to refer to these resources from elsewhere in the same Terraform module. check this link to know more about resources in Terraform

so let’s create a new terraform file and name it VPC.tf

resource "aws_vpc" "AWS_PRIVATE_VPC" {   cidr_block           = var.AWS_PRIVATE_VPC_CIDR   enable_dns_support   = var.enable_dns_support_status   enable_dns_hostnames = var.enable_dns_hostnames_status   instance_tenancy     = var.instance_tenancy_type   tags = {      Name        = var.AWS_PRIVATE_VPC_NAME      Terraform   = var.isTerraformCreation      Environment = var.Environment_NAME   }}resource "aws_vpc" "AWS_PUBLIC_VPC" {   cidr_block           = var.AWS_VPC_PUBLIC_CIDR   enable_dns_support   = var.enable_dns_support_status   enable_dns_hostnames = var.enable_dns_hostnames_status   instance_tenancy     = var.instance_tenancy_type   tags = {      Name        = var.AWS_PUBLIC_VPC_NAME      Terraform   = var.isTerraformCreation      Environment = var.Environment_NAME   }}

In the VPC.tf file, several variables need to declare in the variable file (vars. tf). The idea of using the variable file is to create a reusable Terraform configuration rather than modifying each file separately, as well as to have a central place for the modification.

variable "AWS_ACCESS_KEY" {}variable "AWS_SECRET_KEY" {}variable "AWS_SWITCH_ROLE" {}variable "AWS_REGION" {   default = "us-east-1"}### General Tags that will be used accross this terraformvariable "Environment_NAME" {   default = "example-app"}variable "Environment_TYPE" {   default = "prod"}variable "isTerraformCreation" {   default = "true"}### VPC Sectionvariable "enable_dns_support_status" {   default = true}variable "enable_dns_hostnames_status" {   default = true}variable "instance_tenancy_type" {   default = "default"}variable "AWS_PRIVATE_VPC_NAME" {   default = "private-vpc"}variable "AWS_PRIVATE_VPC_CIDR" {   default = "10.0.0.0/16"}variable "AWS_PUBLIC_VPC_NAME" {   default = "public-vpc"}variable "AWS_VPC_PUBLIC_CIDR" {   default = "173.31.0.0/16"}

For more explanation and details about the aws_vpc resource and its attribute check the terraform documentation in this link.

if you are not familiar with the tag concept please refer to the AWS documentation to know more → https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html

Great, now let’s run our changes and see terraform in action by running the terraform plan to make sure everything is fine and then run terraform apply.

terraform plan

faroug@Farougs-MBP aws_infrastructure_terraform % terraform plan --out out.terraformvar.AWS_ACCESS_KEYEnter a value:var.AWS_SECRET_KEYEnter a value:Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the followingsymbols:+ createTerraform will perform the following actions:# aws_vpc.AWS_PRIVATE_VPC will be created+ resource "aws_vpc" "AWS_PRIVATE_VPC" {+ arn                                  = (known after apply)+ cidr_block                           = "10.0.0.0/16"+ 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)+ ipv6_cidr_block_network_border_group = (known after apply)+ main_route_table_id                  = (known after apply)+ owner_id                             = (known after apply)+ tags                                 = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-private-vpc"+ "Terraform"        = "true"}+ tags_all                             = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-private-vpc"+ "Terraform"        = "true"}}# aws_vpc.AWS_PUBLIC_VPC will be created+ resource "aws_vpc" "AWS_PUBLIC_VPC" {+ arn                                  = (known after apply)+ cidr_block                           = "173.31.0.0/16"+ 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)+ ipv6_cidr_block_network_border_group = (known after apply)+ main_route_table_id                  = (known after apply)+ owner_id                             = (known after apply)+ tags                                 = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-public-vpc"+ "Terraform"        = "true"}+ tags_all                             = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-public-vpc"+ "Terraform"        = "true"}}Plan: 2 to add, 0 to change, 0 to destroy.─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Saved the plan to: out.terraformTo perform exactly these actions, run the following command to apply:terraform apply "out.terraform"

terraform apply

faroug@Farougs-MBP aws_infrastructure_terraform %   terraform apply "out.terraform"aws_vpc.AWS_PUBLIC_VPC: Creating...aws_vpc.AWS_PRIVATE_VPC: Creating...aws_vpc.AWS_PRIVATE_VPC: Still creating... [10s elapsed]aws_vpc.AWS_PUBLIC_VPC: Still creating... [10s elapsed]aws_vpc.AWS_PUBLIC_VPC: Creation complete after 14s [id=vpc-0f3c9b57545c510ac]aws_vpc.AWS_PRIVATE_VPC: Creation complete after 14s [id=vpc-06fb2f454203468c9]Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Let’s see it in the AWS console

Great, now we have the VPCs let us proceed with creating the other network resources (Subnets, NAT, Internet Gateway, VPC peering, table route, etc.) and later will configure them with their corresponding resources.

VPC Subnets

As I stated before that the goal is to create a dynamic terraform configuration that will run across all the AWS regions. therefore to create subnets that span across all the availability zones we need to get the number and the name of the available availability zones in the targeted region, note that availability zones are not identical, in some regions, there are 3 or 4 or 6 as in N. Virginia region

So before starting the implementation of the subnets, there are helpful terraform resources called data and output that help us to achieve that.

In the below code we will use the data source to get the current region, and its availability zones and then use the output source to expose this information or values.

»Data Sources

Data sources allow Terraform to use the information defined outside of Terraform, defined by another separate Terraform configuration, or modified by functions.

data "aws_region" "current" {}data "aws_availability_zones" "available" {    state = "available"}

»Output Values

make information about your infrastructure available on the command line, and can expose information for other Terraform configurations to use. Output values are similar to return values in programming languages.

output "aws_region" {   value = data.aws_region.current.name}output "aws_availability_zones" {   value = data.aws_availability_zones.available.names}

Please copy the above command and add them in a new file name it (vpc_subnets.tf) and run the terraform plan to retrieve these values on the terminal.

vpc_subnets.tf
faroug@Farougs-MBP aws_infrastructure_terraform % terraform plan -out out.terraformChanges to Outputs:+ aws_availability_zones = [+ "us-east-1a",+ "us-east-1b",+ "us-east-1c",+ "us-east-1d",+ "us-east-1e",+ "us-east-1f",]+ aws_region             = "us-east-1"You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

as you can see we have retrieved the region name and the availability zones in that region. You can edit the region on the vars.tf

Useful terraform commands or options

»Command: output

The terraform output command is used to extract the value of an output variable from the state file.

terraform output -raw aws_region
us-east-2% terraform output -json aws_availability_zones
["us-east-2a","us-east-2b","us-east-2c"]

»Command: apply

The terraform apply command executes the actions proposed in a Terraform plan.

»Command: plan

The terraform plan command creates an execution plan, which lets you preview the changes that Terraform plans to make to your infrastructure. By default, when Terraform creates a plan it:

  • Reads the current state of any already-existing remote objects to make sure that the Terraform state is up-to-date.
  • Compares the current configuration to the prior state and noting any differences.
  • Proposes a set of change actions that should, if applied, make the remote objects match the configuration.

Another useful option is -target provided by Terraform and can be used with both plan and apply commands to apply or plan a specific target resource

terraform apply -target=aws_region.current 

Subnet implementation

Now with the data and output of the region and availability zone exist in the vpc_subnets.tf file “please note that data should be in the same file or you will run into the error.

Reference to undeclared resource: A data resource "aws_availability_zones" "available" has not been declared in the root module.
data "aws_region" "current" {}data "aws_availability_zones" "available" {state = "available"}output "aws_region" {value = data.aws_region.current.name}output "aws_availability_zones" {value = data.aws_availability_zones.available.names}resource "aws_subnet" "AWS_PRIVATE_VPC_SUBNET" {count                   = length(data.aws_availability_zones.available.names)vpc_id                  = aws_vpc.AWS_PRIVATE_VPC.idcidr_block              = cidrsubnet(aws_vpc.AWS_PRIVATE_VPC.cidr_block, 8, count.index * 16)availability_zone       = data.aws_availability_zones.available.names[count.index]map_public_ip_on_launch = false#   enable_vpn_gateway = false #   single_nat_gateway = truetags = {Name             = format("%s-%s", "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PRIVATE_VPC_SUBNETS_NAME}", data.aws_availability_zones.available.names[count.index])Terraform        = var.isTerraformCreationEnvironment_NAME = var.Environment_NAMEEnvironment_TYPE = var.Environment_TYPE}}resource "aws_subnet" "AWS_PUBLIC_VPC_PUBLIC_SUBNET" {   count                   = length(data.aws_availability_zones.available.names)   vpc_id                  = aws_vpc.AWS_PUBLIC_VPC.id   cidr_block              = cidrsubnet(aws_vpc.AWS_PUBLIC_VPC.cidr_block, 8, count.index * 16)   availability_zone       = data.aws_availability_zones.available.names[count.index]   map_public_ip_on_launch = truetags = {   Name             = format("%s-%s", "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PUBLIC_VPC_PUBLIC_SUBNETS_NAME}", data.aws_availability_zones.available.names[count.index])   Terraform        = var.isTerraformCreation   Environment_NAME = var.Environment_NAME   Environment_TYPE = var.Environment_TYPE     }}resource "aws_subnet" "AWS_PUBLIC_VPC_PRIVATE_SUBNET" {count                   = length(data.aws_availability_zones.available.names)vpc_id                  = aws_vpc.AWS_PUBLIC_VPC.idcidr_block              = cidrsubnet(aws_vpc.AWS_PUBLIC_VPC.cidr_block, 8, (count.index * 16) + 8)availability_zone       = data.aws_availability_zones.available.names[count.index]map_public_ip_on_launch = falsetags = {Name             = format("%s-%s", "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PUBLIC_VPC_PRIVATE_SUBNETS_NAME}", data.aws_availability_zones.available.names[count.index])Terraform        = var.isTerraformCreationEnvironment_NAME = var.Environment_NAMEEnvironment_TYPE = var.Environment_TYPE   }}

also, append the below variables related to the subnets to the vars.tf file

## Subnetsvariable "AWS_PRIVATE_VPC_SUBNETS_NAME" {   default = "private-vpc"}variable "AWS_PUBLIC_VPC_PUBLIC_SUBNETS_NAME" {   default = "public-vpc-public"}variable "AWS_PUBLIC_VPC_PRIVATE_SUBNETS_NAME" {   default = "public-vpc-private"}

The above vpc_subnets.tf will create subnets based on the available availability zones in the region, and will also auto-generate VPC CIDR for each subnet base on this formula

### Private VPC Subnets# the below will generate 6 subnets CIDR that each prefix will have a network ID (the first 3 digits in the IP) that is 16+ with 24 as suffix CIDRcidr_block = cidrsubnet(aws_vpc.AWS_PRIVATE_VPC.cidr_block, 8, count.index * 16)### Private VPC Subnetsas we need to create 2 kinds of subnets, one private and another public that crosses all the availability zones, we need to make sure the cidr number is not overlapping with each other and hence I changed the network ID to add 16 for each new subnets## Public Subnets # the below will generate 6 subnets CIDR that each prefix will have a network ID (the first 3 digits in the IP) that is 16 + with 24 as suffix CIDR the range will start from 0 in the third digits of the IPcidr_block = cidrsubnet(aws_vpc.AWS_PUBLIC_VPC.cidr_block, 8, count.index * 16)will generate another 6 like the public, however the third digits will start from 8 and add 16 for each new subnetscidr_block = cidrsubnet(aws_vpc.AWS_PUBLIC_VPC.cidr_block, 8, (count.index * 16) + 8)
Private PVC (generate subnets in each available availability zones in the region)
Public PVC (generate public subnets in each available availability zones in the region)
Public PVC (generate private subnets in each available availability zones in the region)

VPC Peering

let’s create another terraform file for the VPC peering resource (vpc_peering.tf).

The below terraform file will create a VPC peering connection between the two VPCs (Public and Private) that we early create and automatically activate it by accepting the request.

resource "aws_vpc_peering_connection" "AWS_VPC_PERRING" {   peer_vpc_id = aws_vpc.AWS_PUBLIC_VPC.id   vpc_id      = aws_vpc.AWS_PRIVATE_VPC.id   auto_accept = truetags = {   Name             = "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_VPC_PERRING_NAME}"   Terraform        = var.isTerraformCreation   Environment_NAME = var.Environment_NAME   Environment_TYPE = var.Environment_TYPE}accepter {   allow_remote_vpc_dns_resolution = true}requester {   allow_remote_vpc_dns_resolution = true   }}

let’s modify the vars.tf file to include the new variable name AWS_VPC_PERRING_NAME

### Peeringvariable "AWS_VPC_PERRING_NAME" {default = "vpc-peering"}

I must mention that terraform preserves the state of configuration, it will detect any changes and applies them.

Terraform must store state about your managed infrastructure and configuration. This state is used by Terraform to map real world resources to your configuration, keep track of metadata, and to improve performance for large infrastructures.

This state is stored by default in a local file named “terraform.tfstate”, but it can also be stored remotely, which works better in a team environment.

Terraform uses this local state to create plans and make changes to your infrastructure. Prior to any operation, Terraform does a refresh to update the state with the real infrastructure.

var.AWS_ACCESS_KEYEnter a value:var.AWS_SECRET_KEYEnter a value:aws_vpc.AWS_PRIVATE_VPC: Refreshing state... [id=vpc-06fb2f454203468c9]aws_vpc.AWS_PUBLIC_VPC: Refreshing state... [id=vpc-0f3c9b57545c510ac]Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the followingsymbols:+ createTerraform will perform the following actions:# aws_vpc_peering_connection.AWS_VPC_PERRING will be created+ resource "aws_vpc_peering_connection" "AWS_VPC_PERRING" {+ accept_status = (known after apply)+ auto_accept   = true+ id            = (known after apply)+ peer_owner_id = (known after apply)+ peer_region   = (known after apply)+ peer_vpc_id   = "vpc-0f3c9b57545c510ac"+ tags          = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-vpc-peering"+ "Terraform"        = "true"}+ tags_all      = {+ "Environment_NAME" = "example-app"+ "Environment_TYPE" = "prod"+ "Name"             = "example-app-prod-vpc-peering"+ "Terraform"        = "true"}+ vpc_id        = "vpc-06fb2f454203468c9"+ accepter {+ allow_classic_link_to_remote_vpc = false+ allow_remote_vpc_dns_resolution  = true+ allow_vpc_to_remote_classic_link = false}+ requester {+ allow_classic_link_to_remote_vpc = false+ allow_remote_vpc_dns_resolution  = true+ allow_vpc_to_remote_classic_link = false}}Plan: 1 to add, 0 to change, 0 to destroy.─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Saved the plan to: out.terraformTo perform exactly these actions, run the following command to apply:terraform apply "out.terraform"
VPC Peering

»VPC Internet gateway

An internet gateway is a horizontally scaled, redundant, and highly available VPC component that allows communication between your VPC and the internet. It supports IPv4 and IPv6 traffic. It does not cause availability risks or bandwidth constraints on your network traffic.

will create the internet gateway to allow our public subnets in the public VPC to communicate with the internet. and to allow private subnets using a NAT gateway in a public subnet to also communicate with the internet.

so let’s create a new terraform file and name it internet_gateways.tf and include the below code snippet.

resource "aws_internet_gateway" "AWS_PUBLIC_VPC_INTERNET_GATEWAY" {vpc_id = aws_vpc.AWS_PUBLIC_VPC.idtags = {Name             = "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PUBLIC_VPC_IG}"Terraform        = "true"Environment_NAME = var.Environment_NAMEEnvironment_TYPE = var.Environment_TYPE   }}

awesome, now let’s declare the variable AWS_PUBLIC_VPC_IG in the variable file vars.tf.

## Internet Gatewayvariable "AWS_PUBLIC_VPC_IG" {   default = "public-vpc-internet-gateway"}

»VPC NAT gateway

A NAT gateway is a Network Address Translation (NAT) service. You can use a NAT gateway so that instances in a private subnet can connect to services outside your VPC but external services cannot initiate a connection with those instances.

When you create a NAT gateway, you specify one of the following connectivity types:

Public — (Default) Instances in private subnets can connect to the internet through a public NAT gateway, but cannot receive unsolicited inbound connections from the internet. You create a public NAT gateway in a public subnet and must associate an elastic IP address with the NAT gateway at creation. You route traffic from the NAT gateway to the internet gateway for the VPC. Alternatively, you can use a public NAT gateway to connect to other VPCs or your on-premises network. In this case, you route traffic from the NAT gateway through a transit gateway or a virtual private gateway.

Private — Instances in private subnets can connect to other VPCs or your on-premises network through a private NAT gateway. You can route traffic from the NAT gateway through a transit gateway or a virtual private gateway. You cannot associate an elastic IP address with a private NAT gateway. You can attach an internet gateway to a VPC with a private NAT gateway, but if you route traffic from the private NAT gateway to the internet gateway, the internet gateway drops the traffic.

The NAT gateway replaces the source IP address of the instances with the IP address of the NAT gateway. For a public NAT gateway, this is the elastic IP address of the NAT gateway. For a private NAT gateway, this is the private IP address of the NAT gateway. When sending response traffic to the instances, the NAT device translates the addresses back to the original source IP address.

by AWS documentation and definition for private subnets in the public VPC to access the internet, we need to create the following

  • Internet gateway (we already have covered above)
  • ELP (elastic IP address)
  • NAT gateway in a public subnet
  • Attach the ELP with the NAT gateway

NAT gateway will help us connect to our private instances using another service from AWS called session manager that we will see in the coming parts.

ELP (Elastic IP address)

resource "aws_eip" "AWS_PUBLIC_VPC_NAT_ELASTIC_IP" {vpc = truetags = {Name             = "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PUBLIC_VPC_NAT_EIP_NAME}"Terraform        = "true"Environment_NAME = var.Environment_NAMEEnvironment_TYPE = var.Environment_TYPE   }}

NAT (Network Address Translation)

I’ve chosen the first subnet in of the list of public subnets to be the host of the NAT

resource "aws_nat_gateway" "AWS_PUBLIC_VPC_NAT_GATEWAY" {   allocation_id = aws_eip.AWS_PUBLIC_VPC_NAT_ELASTIC_IP.id   subnet_id     = aws_subnet.AWS_PUBLIC_VPC_PUBLIC_SUBNET[0].id # choesen the first subnet, tried to automate it but still figuring it out, if any has solution let me know in the comments below, thanks:)   tags = {      Name             = "${var.Environment_NAME}-${var.Environment_TYPE}-${var.AWS_PUBLIC_VPC_NAT_NAME}"      Terraform        = "true"      Environment_NAME = var.Environment_NAME      Environment_TYPE = var.Environment_TYPE      }}data "aws_nat_gateways" "AWS_PUBLIC_VPC_NAT_GATEWAYS" {    vpc_id = aws_vpc.AWS_PUBLIC_VPC.id    filter {      name   = "state"      values = ["available"]     }}data "aws_nat_gateway" "AWS_PUBLIC_VPC_NAT_GATEWAY_DATA" {   id = aws_nat_gateway.AWS_PUBLIC_VPC_NAT_GATEWAY.id}output "aws_nat_gateway" {   value = data.aws_nat_gateway.AWS_PUBLIC_VPC_NAT_GATEWAY_DATA.public_ip}

let’s declare these new variables in the vars.tf.

## NAT Gateway# ELPvariable "AWS_PUBLIC_VPC_NAT_EIP_NAME" {   default = "public-vpc-nat-ip"}# NATvariable "AWS_PUBLIC_VPC_NAT_NAME" {   default = "public-vpc-nat"}

--

--