How to create a VPC using Terraform?

How to create a VPC using Terraform?

Running your applications comes up with other challenges too and one of those challenges is having a robust network set up to host all parts in one place. We have to set up VPC (Virtual Private Cloud), internet gateway, subnet, etc. to make sure our application is working properly. The other aspect of this is to manage the infrastructure once it is ready and deployed. This is where Terraform comes in handy. Terraform is an Infrastructure-as-a-code that helps you to define infrastructure in code and you can easily maintain it for future updates.

If you are not aware of the networking fundamentals on AWS, read the article AWS Networking Fundamentals before going deep with Terraform in this article.

You need to have some information about how Terraform works. If you don't know about Terraform, I suggest going through its documentation to have a basic idea about it before diving into the article.

I will post the snippets and add some description in steps here. You can also find the complete module at a GitHub repo aws-vpc-terraform.

Directory Structure

aws-vpc-terraform

The above directory structure of the module has the following key files:

  • main: Contains the entire module and all the resources we will discuss in some time.
  • output: Defines the output provided by the module. This provider returns the vpcId of the VPC created by the module.
  • provider: Defines the provider required for the module to work properly. You can also think of it as dependencies required for the module. This module needs the AWS module from Hashicorp (creators of Terraform). The AWS module will allow us to use the resources available in the AWS to create our desired infrastructure.
  • variables: It contains the input variables required by the module to complete its task. For the sale of this article, I have set default values to the variables but they can be easily made required by removing the default value.

As mentioned above the most important file is main.tf which contains all the code for the resources we are about to create. Let's go through each resource statement in file and understand them a bit.

  • First, we are going to query about the availability zones in the region where we wish to deploy the resources. We will distribute all the resources or create redundant resources to have high availability for our application.
data "aws_availability_zones" "availableAZ" {}
  • Create a VPC resource. Here var.cidr is one of the variables we defined to set up a CIDR for this VPC. any other resources we create will use the IP address from the range defined here.
# VPC
resource "aws_vpc" "vpc" {
  cidr_block                       = var.cidr
  instance_tenancy                 = "default"
  enable_dns_support               = true
  enable_dns_hostnames             = true
  assign_generated_ipv6_cidr_block = true

  tags = {
    Name = var.namespace
    Namespace = var.namespace
  }
}
  • Public Subnets are small pockets in each availability zone that can be accessed via the internet directly. Any public-facing part of your application like a web app will be deployed here. We will be creating one public subnet in each availability zone.
# Public Subnet
resource "aws_subnet" "publicsubnet" {
  count                   = 3
  cidr_block              = tolist(var.publicSubnetCIDR)[count.index]
  vpc_id                  = aws_vpc.vpc.id
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.availableAZ.names[count.index]

  tags = {
    Name = "${var.namespace}-publicsubnet-${count.index + 1}"
    AZ   = data.aws_availability_zones.availableAZ.names[count.index]
    Namespace = var.namespace
  }

  depends_on = [aws_vpc.vpc]
}
  • Private Subnets are small pockets in each availability zone that you want only your applications to access like database servers, internal APIs, etc. We will be creating one public subnet in each availability zone.
# Private Subnet
resource "aws_subnet" "privatesubnet" {
  count             = 3
  cidr_block        = tolist(var.privateSubnetCIDR)[count.index]
  vpc_id            = aws_vpc.vpc.id
  availability_zone = data.aws_availability_zones.availableAZ.names[count.index]

  tags = {
    Name = "${var.namespace}-privatesubnet-${count.index + 1}"
    AZ   = data.aws_availability_zones.availableAZ.names[count.index]
    Namespace = var.namespace
  }

  depends_on = [aws_vpc.vpc]
}
  • For our VPC to have internet connectivity, we need to deploy an Internet gateway. This will allow our VPC to receive traffic from the internet and send some traffic out as well.

# Internet Gateway
resource "aws_internet_gateway" "internetgateway" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.namespace}-InternetGateway"
    Namespace = var.namespace
  }

  depends_on = [aws_vpc.vpc]
}
  • Internet gateway will give internet access to our VPC but what if we want to give one side internet access to the services deployed in our private subnet. This is required if we want to connect to third-party services or need to run scheduled updates on the services deployed in the private subnet. That is why we need to create NAT Gateway with Elastic IP. We will be creating one for each availability zone. If one Availability Zone is down you will still be able to service your request.
# Elastic IP
resource "aws_eip" "elasticIPs" {
  count = 3
  vpc   = true

  tags = {
    Name = "elasticIP-${count.index + 1}"
    Namespace = var.namespace
  }

  depends_on = [aws_internet_gateway.internetgateway]
}

# NAT Gateway
resource "aws_nat_gateway" "natgateway" {
  count         = 3
  allocation_id = aws_eip.elasticIPs[count.index].id
  subnet_id     = aws_subnet.publicsubnet[count.index].id

  tags = {
    Name = "${var.namespace}-NATGateway-${count.index + 1}"
    AZ   = data.aws_availability_zones.availableAZ.names[count.index]
    Namespace = var.namespace
  }

  depends_on = [aws_internet_gateway.internetgateway]
}
  • We have all the major parts of the network and now it is time to create route tables. Route Tables define which traffic can flow to which resource. We will create a Route Table for public and private subnets.
  • Public Route Table will have the traffic flowing from Internet Gateway directly. We will also create an association record to associate the newly created route table with the public subnets.
# Route Table for Public Routes
resource "aws_route_table" "publicroutetable" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.internetgateway.id
  }

  tags = {
    Name = "${var.namespace}-publicroutetable"
    Namespace = var.namespace
  }

  depends_on = [aws_internet_gateway.internetgateway]
}

# Route Table Association - Public Routes
resource "aws_route_table_association" "routeTableAssociationPublicRoute" {
  count          = 3
  route_table_id = aws_route_table.publicroutetable.id
  subnet_id      = aws_subnet.publicsubnet[count.index].id

  depends_on = [aws_subnet.publicsubnet,  aws_route_table.publicroutetable]
}
  • Private Route Table will have the traffic flowing from NAT Gateway. We will also create an association record to associate the newly created route table with the private subnets.
# Route Table for Private Routes
resource "aws_route_table" "privateroutetable" {
  count  = 3
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.natgateway[count.index].id
  }

  tags = {
    Name = "${var.namespace}-privateroutetable-${count.index + 1}"
    AZ   = data.aws_availability_zones.availableAZ.names[count.index]
    Namespace = var.namespace
  }

  depends_on = [aws_nat_gateway.natgateway]
}

# Route Table Association - Private Routes
resource "aws_route_table_association" "routeTableAssociationPrivateRoute" {
  count          = 3
  route_table_id = aws_route_table.privateroutetable[count.index].id
  subnet_id      = aws_subnet.privatesubnet[count.index].id

  depends_on = [aws_subnet.privatesubnet, aws_route_table.privateroutetable]
}
  • One thing to note here is that the term Private and Public in front of the Route Tables, Subnets etc. is used to denote the type of applications they are suited to deploy. These terms are commonly used in the industry as it is but you can name them anything if you are aware of what it is used for.

Our entire module is ready. In order to run it, you have to first initialize the Terraform, see the plan and apply it to create your VPC using Terraform.

terraform init
terraform plan
terraform apply

That's all related to deploying and managing your VPC using Terraform. In upcoming articles, I will write more about creating other services and deploying some common things using Terraform. Till then Happy Coding.