Hero Illustration
Software Development, Terraform

Infrastructure as Code Using Terraform – Creating an AWS Virtual Private Cloud

In this article, we are going to implement Infrastructure as Code (IAS) using Terraform to build cloud infrastructure on AWS. We won’t discuss IAS concepts in detail, but for those of you who are not familiar or need some refreshing, there is a good article that summaries it nicely here.

We will be using Windows 10 to setup AWS cloud infrastructure, although you can also use other operating systems with some adjustments. Here is an overview of what we will build.

We will create a VPC with two subnets – a Public subnet which have access to/from the internet, and a private subnet which only has access to the internet. So let’s get started.

Preparing the Tools 

We are going to use Terraform as our cloud provisioning tool. Terraform is an opensource tool to create cloud infrastructure based on the definition files that we will create. To install Terraform, follow this instruction on their official website. We can check if Terraform is installed properly by using this command on your command prompt:

terraform -v 

It should display your Terraform’s version.

terraform -v 
Terraform v0.12.25 

Next, we need to configure our AWS Credential which will be used by Terraform. These credentials will be stored in file located at “%USERPROFILE%.aws\credentials”.

aws_access_key_id = ******************** 
aws_secret_access_key = **************** 

Follow the instructions for details on how to configure your AWS credentials file.

Creating Definition File 

The Definition file is where we put our cloud infrastructure resources. The term “resource” will be used for any infrastructure we create such as VPC, subnets, EC2 instances and others. Terraform will automatically detect the definition file which has the file name “main.tf”. Let’s create one and start defining cloud resources.

Virtual Private Cloud (VPC) 

A VPC is a virtual private network which can be used to logically separate cloud resources. For example, we can separate cloud resources for development and production. First, we’ll define the VPC in the “main.tf” file:

resource "aws_vpc" "development" { #1 
  cidr_block = "" #2 
  tags = { 
    Environment = "development" #3 
  1. Define a resource with type “aws_vpc” and name “development”
  2. Define the IP Address range for the network
  3. Define the tag for the resource


Like the VPC, the subnet is used to logically separate cloud resources but inside VPC. Modify the definition file to add two subnets:

resource "aws_subnet" "private" { #1 
  vpc_id     = aws_vpc.development.id #2 
  cidr_block = "" #3 
  tags = { 
    Environment = "development" 
    Access = "private" 
resource "aws_subnet" "public" {  
  vpc_id     = aws_vpc.development.id 
  cidr_block = "" 
  tags = { 
    Environment = "development" 
    Access = "public" 
  1. Define a subnet resource and its name
  2. Define the VPC ID using a reference from the other resource
  3. Define the IP Address range

Internet Gateway

We have defined two subnets – public and private. The Public subnet is used to create resources which can access external networks and can be accessed from external networks i.e. the internet directly. To enable this, we need to create an Internet Gateway:

resource "aws_internet_gateway" "igw" { #1 
  vpc_id = aws_vpc.development.id 
  tags = { 
    Environment = "development" 

resource "aws_route_table" "public" { #2 
  vpc_id = aws_vpc.development.id 
  route { 
    cidr_block = "" 
    gateway_id = aws_internet_gateway.igw.id 
  tags = { 
    Environment = "development" 
resource "aws_route_table_association" "public" { #3 
  subnet_id      = aws_subnet.public.id 
  route_table_id = aws_route_table.public.id 
  1. Define an internet gateway
  2. Define a route table which will route traffic to the internet gateway
  3. Associate the route table to route traffic from the selected subnet to the internet gateway

Network Address Translation (NAT) Gateway

The Private subnet is where we put cloud resources which cannot be accessed from the external network. However, these resources might need access to an external network (i.e. internet), for example to update the operating system, download files, etc. Therefore, the NAT Gateway is needed to route traffic to external network.

resource "aws_eip" "nat" { #1 
  vpc      = true 
resource "aws_nat_gateway" "ngw" { #2 
  allocation_id = aws_eip.nat.id #3 
  subnet_id = aws_subnet.public.id #4 
resource "aws_route_table" "private" { #5 
  vpc_id = aws_vpc.development.id 
  route { 
    cidr_block = "" 
    nat_gateway_id = aws_nat_gateway.ngw.id #6 
  tags { 
    Environment = "development" 
resource "aws_route_table_association" "private" { #7 
  subnet_id = aws_subnet.private.id 
  route_table_id = aws_route_table.private.id 
  1. Define the EIP for NAT Gateway
  2. Define the NAT Gateway
  3. Associate the EIP to be used by the NAT Gateway
  4. Define the subnet to use this NAT Gateway. The subnet should be public because the NAT Gateway will use Internet Gateway to traffic from private subnet to internet.
  5. Define a route table to route traffic from the selected subnet to the NAT Gateway
  6. Route traffic to the NAT Gateway
  7. Associate the private subnet with the route table

Applying Terraform Definition Files 

Once we have completed the definition files, we can start to execute Terraform commands to build our cloud infrastructure. But before that, there are some checks that we need to do first. It’s good practice to validate the definition files and see what will be applied by Terraform.

Terraform provides two commands for this. To validate the definition files, execute this command in the same directory we put “main.tf”.

terraform validate 

If the definition file is valid, we will get message “Success! The configuration is valid.”

To check what will applied by Terraform, execute the following command:

terraform plan 

Terraform will tell us the details of resources that will be created, updated, and removed: 

Refreshing Terraform state in-memory prior to plan... 
The refreshed state will be used to calculate this plan, but will not be 
persisted to local or remote state storage. 
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_eip.nat will be created 
  + resource "aws_eip" "nat" { 
      + allocation_id     = (known after apply) 
      + association_id    = (known after apply) 
      + customer_owned_ip = (known after apply) 
      + domain            = (known after apply) 
      + id                = (known after apply) 
      + instance          = (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) 
      + vpc               = true 

We can also find the summaries at the end:

Plan: 10 to add, 0 to change, 0 to destroy. 
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. 

We can also use this information to check if it will impact existing resources. Once we’re satisfied with the plan, we can tell Terraform to create the infrastructure using the following command:

terraform apply -auto-approve 

The flag “-auto-approve” is used to skip terraform asking for confirmation. We use it because we have already reviewed the plan.

It will take time depending on how many resources are created, removed, or destroyed. After Terraform is finished applying the definition files, we can go to the AWS Web Console to check that the VPC, subnets, and other resources have been created.

Another Terraform command which might be useful is: 

terraform fmt 

The above command will format our definition file before we put into source code repository.

Wrap Up

Now our infrastructure is in the form of code. This will make sure that any infrastructure changes can be performed by anyone with exactly the same result. We can review the changes before applying them, and automate the execution with an existing continuous integration server.

Source code available on Github.

Kustian – Technical Evangelist

Contact us to learn more!

Please complete the brief information below and we will follow up shortly.

    ** All fields are required
    Leave a comment