Provisioning AWS Network using Terraform Modules

- By Devashish Meena on September 26, 2018

At Shippable, we love using Terraform. From using it sparsely just a few years back, we've now reached a stage where every single component of all our environments is managed using Terraform. Hard to believe? Here's all our infrastructure code to prove it ! We've also published a few posts earlier which outline our process for managing infrastructure.Some of these are,

- Provisioning AWS Infrastructure Using Terraform

- Provisioniong AWS VPC With Terraform

- Provision AWS EC2 Virtual Machines Using Terraform

So why a new post? Terraform now supports Modules that provide an easy way to break down different parts of the infrastructure into reusable components. They also provide a Registry where users can publish their modules. Users can download "verified" modules from the registry and use them directly as building blocks for their infrastructure. We decided to give this a try by creating a complete, production-ready infrastructure(similar to what we use). The objectives of the tutorial are to

- Logically break down infrastructure components into modules

- Reuse and chain modules to create component decoupling

- Drive all configuration from one file

We'll create a VPC layout that looks like this: 

 

aws_vpc

For folks who prefer reading code over plain English, full source is available here: https://github.com/ric03uec/prov_aws_vpc_terraform.

 

Scenario

We'll provision following components in the course of this tutorial

- VPC

- 1 Public Subnet

- 1 NAT Instance

- 2 Private Subnets

- 2 Security Groups

 

Project layout

All the modules will be added to separate folders like vpc, sn-public(for public subnet), nat etc. These will be called from the main.tf file in project root in a logical order to build the infrastructure. The objective is to stack the components in logical "layers" with each layer consuming the output of the previous layer(s).

We'll have at least three files for each module: variables.tf, main.tf and output.tf. This structure is based on the conventions recommended by Terraform to create modules.

- variables.tf: defines the list of variables that the module expects. These variables can either be set by a different module, hardcoded or set in environments but as long as these are available, the module will work

- main.tf: contains the module logic e.g. in the case of nat, it'll create all the components required to set up NAT instance correctly.

- output.tf: exports a set of values from the module. These can either be used by the module caller to print variable values or by dependent modules to take some actions. This can is how we'll chain different modules together to provision a VPC with public and private subnets.

There is no restriction on the number of files we can have in each module as long as the contracts defined in variables.tf and output.tf are not broken.

We've also set sane defaults for all the values so the project will work out of the box without any changes. Any value can be overriden by either changing the variables in variables.tf file in project root or by setting a corresponding environment variable before running terraform commands.

Lets walk through the main.tf file in project root and see how we can assemble our VPC from ground up.

 

VPC

Almost always, this will be the starting point of anything you do in AWS. Instead of using the default VPC, we'll create one and build other components on top of it. The VPC configuration is driven from variables.tf file in project root. The VPC has 10.0.0.0/16 CIDR range in us-east-2 region. The module exports the VPC id, name and region. 

As mentioned earlier, the code for VPC module is in vpc folder.

module "custom_vpc" {
source = "./vpc"

vpc_region = "${var.vpc_region}"
vpc_name = "${var.vpc_name}"
vpc_cidr_block = "${var.vpc_cidr_block}"
}

 

Public Subnet

The second step is to build a public subnet on top of the VPC we just created. The module, in folder sn-public, takes VPC information and subnet configuration(CIDR, name, AZ) as inputs. Internally, the module creates a subnet with specified CIDR range and attaches an Internet Gateway to it. According to AWS

If a subnet's traffic is routed to an internet gateway, the subnet is known as a public subnet.

So we've just created a public subnet. The module exports the subnet ID and subnet name.

module "public_subnet" {
source = "./sn-public"

vpc_id = "${module.custom_vpc.vpc_id}"
vpc_region = "${var.vpc_region}"
subnet_cidr = "${var.pub_sn_cidr}"
subnet_name = "${var.pub_sn}"
subnet_az = "${var.pub_sn_az}"
}

 

NAT Gateway

Before we move on to provision private subnets, we'll first create a NAT Instance. We're assuming that any instances in private subnets will need to connect to public Internet to pull some information (packages, updates, API calls etc). Because of this, the instances will need to connect to a NAT Gateway to reach Internet. Also, NAT instances can only be provisioned in a public subnet. We'll use the public subnet we just provisioned to bring up a NAT Instance.

module "nat_gateway" {
source = "./nat"

vpc_id = "${module.custom_vpc.vpc_id}"
nat_ami_id = "${var.nat_ami_id}"
nat_instance_type = "${var.nat_instance_type}"
pub_sn = "${module.public_subnet.subnet_name}"
pub_sn_id = "${module.public_subnet.subnet_id}"
pub_sn_az = "${var.pub_sn_az}"
pri_sn_cidr = [
"${var.pri_sn_01_cidr}",
"${var.pri_sn_02_cidr}"
]
}

We're using a NAT Instance here instead of a NAT Gateway because an instance can be used as a bastion host to connect to instances in private subnets. You can read more about differences between NAT Instance and NAT Gateway here.

 

Private Subnets

This is where we'll demonstrate the reusability of Terraform modules. We've created a module called `sn-private` which is called twice with different values to create two private subnets that connect to the same NAT Instance. This enforces us to create structurally similar components which reduces fragility in the system. If the composition of one private subnet needs to be changed, it'll change for each and every subnet.

module "private_subnet_01" {
source = "./sn-private"

vpc_id = "${module.custom_vpc.vpc_id}"
vpc_region = "${var.vpc_region}"
subnet_cidr = "${var.pri_sn_01_cidr}"
subnet_name = "${var.pri_sn_01}"
subnet_az = "${var.pri_sn_01_az}"
nat_gw_id = "${module.nat_gateway.nat_gateway_id}"
}

The private subnets export subnet ID and subnet name.

 

Security Groups

The final component we'll provision is a set of security groups. It wasn't absolutely necessary to keep these as modules but we did so anyway for the sake of consistency.

This module just exports the ID's of different security groups. Once this final step is done, just run the code using steps mentioned here.

Conclusion

This tutorial shows how Terraform Modules can help us create a clean structure for infrastructure code by following "Separation of Concerns" philosophy. The code is easy to maintain and encourages creating small, reusable components.

 

Next Steps

In next few tutorials, we'll add more components in this infrastructure to take it closer to a production-like environment. This will need

- Adding public and private instances

- Setting up access control

- Adding load balancers to route public traffic to internal services

 

Food for thought

OK, so we have a fully functional code that works end to end without any issues. How do we solve the real problems that pop up once we need take this code from a POC to Production ?

- Where is the state file stored ?

- Whose Access/Secret keys are being used to run terraform commands?

- How do we make sure everyone runs same terraform version?

- What't the guarantee that terraform commands runs on the same platform (OS, kernel, bash etc) every single time?

- Who ran this, when and why?

 

We'll answer all these questions in the next set of tutorials with live examples and demos. For now, lets just terraform destroy the whole thing !

Topics: terraform, AWS, tutorial