Provision an EC2 Instance in a VPC with Terraform

Isuru SIriwardana
6 min readMay 30, 2021

A reference solution for provisioning EC2 instances in a VPC using Terraform.

Introduction

Provisioning an EC2 instance in AWS is one of the simplest tasks. However, it is not be enough to have only an EC2 instance provisioned. It is required to limit access to the EC2 instance to provide network isolation while putting required routing mechanisms, and also there could be requirements to mount external storage volumes to the EC2 instance.

1. Overall solution

This solution reference consist of following features provisioned and the complete code is avaialble in this GitHub repository location,

  • provision public and private subnet in three availability zones.
  • expose the EC2 instance to the internet via an Internet Gateway.
  • place required route associations in route tables.
  • create a NAT gateway to allow internet access in private subnets.
  • attach an EBS volument to the EC2 instance.

Code Organization

Code associated with this solution in the repository is organized as below,

| — backend.tf
| — ebs.tf
| — instance.tf
| — internetgateway.tf
| — key.tf
| — nat.tf
| — providers.tf
| — securitygroup.tf
| — subnets.tf
| — vars.tf
| — vpc.tf
  • backend.tf: Configures the Terraform backend requires to store state remotely. In this case the backend is AWS S3.
  • ebs.tf: Declares the AWS EBS resource required to be provisioned to mount into the EC2 instances.
  • instance.tf: Declares the AWS EC2 instance required in the solution.
  • internetgateway.tf: Declares the internet gateway, the route table for public access, and routing rules for the public subnet.
  • key.tf: Declares the public key required to place in EC2 instance.
  • nat.tf: Declares the NAT gateway, the private route table, and routing rules for the private subnet.
  • providers.tf: Declares the AWS terrform provider.
  • securitygroup.tf: Declares the security group associated with the VPC to define ingress and egress.
  • subnets.tf: Declares the public and private subnets of the VPC.
  • vars.tf: Declares all the variables to be used by the infrastructure code.
  • vpc.tf: Declares the virtual private cloud.

Solution Overview

This section briefly explains each component placed in the solution when it is necessary, with the associated Terraform code.

VPC

VPC simply stands for virtual private network. A VPC provides network level isolations to resources launched within it, which means all our resources will be located in our own network.

By deafult AWS will provide a default VPC for our use, but for small and medium use cases we can create our own VPC per region. Resources located in two VPCs can’t communicate with each other using their private IP addresses. But it is possible to connect two VPCs and make that happen, which is referred as VPC peering.

In this solution, I have created a single VPC with mutliple subnets spreaded accross 3 avaialability zones in one region. This VPC uses the 10.0.0.0/16 address space, allowing us to use the address space starting like 10.0.x.x. The IP addresses we are going to use within the VPC are all private. These private IP addresses are in different private subnets. These cannot be used in internet publically.

For example,

2. Possible Address Spaces for the VPC

This table shows some of the IP address ranges that can possibly use in both VPC and other subnets. How does these values are deterimed? This is determined using the subnet mask. This is further explained in the below table.

3. Subnet mask calculation examples

If we considered the reference solution, we can notice that our VPC is covering 3 availability zones. These availability zones has 2 subnets in each, one public and one private. Subnet `main-public-2` has the address space 10.0.2.0/24, wich means it has 256 public IP addresses ranging from 10.0.2.0 to 10.0.2.255. Subnet `main-private-1` has the address space 10.0.4.0/24, wich means it has 256 public IP addresses (- the IP addresses reserved by AWS) ranging from 10.0.4.0 to 10.0.4.255.

Public subnets are connected to the internet gateway and those will get assigned public IP addresses. Resources within the private subnetes are not accessible from the internet. But, resources within the public subnets can access the resources within private subnets because these are in the same VPC, given that the firewall rules are allowed this.

Typically, public subnet can be used to place internet facing resources such as load balancers and application servers. Private subnets are more suitable for databases and other backend services that does not have any purpose on enabling public access.

Like previously mentioned, VPC is declared inside vpc.tf:

# VPCresource "aws_vpc" "main" {  cidr_block           = "10.0.0.0/16"  instance_tenancy     = "default"  enable_dns_support   = "true"  enable_dns_hostnames = "true"  enable_classiclink   = "false"  tags = {    Name = "main"  }}

In here, first we define our VPC. It has the IP range of `10.0.0.0/16`. Then we set the instance tenancy to default. Which means multiple instances in one physical hardware. The we enable DNS support and hostnames, which gives private host and domain names for instances within the VPC.

Then we have defined 3 public subnets with their own address spaces, which is in subnets.tf. These subnets are linked to the VPC via the `vpc_id` property. Each public subnet will receive it’s own public IP address when launching them. However the private sunets won’t get a public IP address at the launch.

Additionally, we have securitygroup.tf, which defines ingress and egress traffic to SSH into the resources.

Internet Gateway

Internet gateway is the component in VPC that provides communication between the resources in VPC and the public internet. A typical internet gateway serves two purposes,

  • it provides a target in VPC route tables for internet routable traffic.
  • provides network address translation for instances that have a public IP address assigned.

Internet gateway associated with this reference solution includes declaring an internet gateway resource, a route table, and then three route table associations to define routing rules to allow internet traffic into public subnets. This can be found in securitygroups.tf in the refrence repository.

NAT Gateway

NAT gateway is the component in VPC that enables instances in a private subnet to connect to the internet, or other AWS services, and prevent the internet origin connections reaching the private resources. The NAT gateway should always reside in a public subnet and a non changable elastic IP should be assigned to it.

Similar to internet gateway, we define a route table and route table association, in addition to the NAT gateway resource. Related code can be found in nat.tf in the reference repository.

EC2 Instance

EC2 instances are the virtual machine instances. These can be launched in the default VPC or in a private VPC and attach it to a subnet.

# EC2
resource "aws_instance" "example" {
ami = var.AMIS[var.AWS_REGION]
instance_type = "t2.micro"

# the VPC subnet
subnet_id = aws_subnet.main-public-1.id
# the security group
vpc_security_group_ids = [aws_security_group.allow-ssh.id]
# the public SSH key
key_name = aws_key_pair.mykeypair.key_name
}

In here, we have to explicity declare the the subnet this EC2 instance should be launched. Otherwise the EC2 instance will be placed in the default VPC’s subnet. Then, we have linked the security group we created to allow SSH, and the public key of the key pair to be used for the handshake.

Associated code for provisioning an EC2 instance is available in instance.tf in the reference repository.

EBS Volume

EBS volumes stands for Elastic Block Storage. This provides the means of storage for the EC2 instances that we are provisioning. By default every EC2 instance will get a default EBS storage instance with 8 GB. These default EBS storage will be terminated when the EC2 instance is deprovisioning.

# EBS
resource "aws_ebs_volume" "ebs-volume-1" {
availability_zone = "ap-southeast-1a"
size = 20
type = "gp2"
tags = {
Name = "extra volume data"
}
}
resource "aws_volume_attachment" "ebs-volume-1-attachment" {

device_name = "/dev/xvdh"
volume_id = aws_ebs_volume.ebs-volume-1.id
instance_id = aws_instance.example.id
}

When defining, we have to define both the EBS instance and the volume attachment. The volume attachment will make sure that the EBS volume will get mounted in the EC2 instance.

Associated code for provisioning an EC2 instance is available in ebs.tf in the reference repository.

Deployment

Prerequisites

First step of the deployment is to generate a ssh key pair,

$ ssh-keygen -t rsa

Verify and change the variables defined in the vars.tf or override them using a variable file with the name terraform.tfvars. Pay attention to the AWS region and the AMIs.

Initialize the providers,

$ terraform init

Verify the changes,

$ terraform plan

Provision the resources,

$ terraform apply

Once everything is done, don’t forget to deprovision the resources,

$ terraform destroy

Source

Associated code is available in this respository path >>

--

--