Build a 3-Tier AWS Architecture Using Terraform

Set up a VPC, three subnets, an RDS MySQL instance, and an application load balancer.

In this article, I’ll document my first Terraform project: building a 3-tier network configuration in AWS. We will be following the general Terraform workflow: Write, Plan, Apply.

For better resource creation speed, adaptability, and portability, we will use four separate files to store variables and output commands. We will create a total of 18 AWS resources through Terraform, including “aws_vpc”, “aws_subnet”, “aws_internet_gateway”, “aws_db_instance”, and “aws_alb”.


  1. Deploy a VPC with CIDR
  2. Create 2 public subnets with CIDR and, and a private subnet with CIDR ‘’.
  3. Create an RDS MySQL instance.
  4. Create a Load Balancer that will direct traffic to the public subnets.


  1. The AWS CLI configured with AWS account credentials, and a familiarity with AWS cloud architecture.
  2. Terraform installed on your home system.
  3. A text editor such as Atom, Visual Studio Code, or PyCharm, with the Terraform plug-in installed.

Step 1: Write

First, create a new directory for the four Terraform source files we will be working with:,, terraform.tfvars, and

Open in your text editor. AWS will be our plug-in provider, so the top of should include:

provider "aws" {
region = var.region

Back in the CLI, run “terraform init” in the new directory to initialize with AWS. Next, return to and enter the following code to create a simple VPC with the required CIDR block:

resource "aws_vpc" "terraform_vpc" {
cidr_block = var.vpc_cidr_block
instance_tenancy = var.instance_tenancy

tags = {
Name = var.vpc_name

Create the public subnets that will be associated with our VPC, and specify their CIDR blocks and availability zones:

resource "aws_subnet" "public_subnet_1" {
vpc_id =
cidr_block = var.public_subnet_cidr_block_1
availability_zone = var.public_subnet_1_az

tags = {
Name = var.public_subnet_name_1

resource "aws_subnet" "public_subnet_2" {
vpc_id =
cidr_block = var.public_subnet_cidr_block_2
availability_zone = var.public_subnet_2_az

tags = {
Name = var.public_subnet_name_2

Enter the code for the private subnet:

resource "aws_subnet" "private_subnet_1" {
cidr_block = var.private_subnet_cidr_block_1
vpc_id =
availability_zone = var.private_subnet_1_az

tags = {
Name = var.tagkey_name_private_subnet_1

Create the internet gateway for the public subnets:

resource "aws_internet_gateway" "default" {
vpc_id =

Next, enter the code for the route tables and route table associations for each subnet, for example:

resource "aws_route_table" "public_subnet_1_to_internet" {
vpc_id =

route {
cidr_block = ""
gateway_id =

tags = {
Name = var.public_route_table_1

resource "aws_route_table_association" "internet_for_public_subnet_1" {
route_table_id =
subnet_id =

Include an elastic IP address and a NAT gateway for communication with the private subnet:

resource "aws_eip" "eip_1" {
count = "1"

resource "aws_nat_gateway" "natgateway_1" {
count = "1"
allocation_id = aws_eip.eip_1[count.index].id
subnet_id =

Include a NAT gateway route table, and associate the private subnet accordingly, for example:

resource "aws_route_table" "natgateway_route_table_1" {
count = "1"
vpc_id =

route {
cidr_block = ""
nat_gateway_id = aws_nat_gateway.natgateway_1[count.index].id

tags = {
Name = var.tagkey_name_natgateway_route_table_1

resource "aws_route_table_association" "private_subnet_1_to_natgateway" {
count = "1"
route_table_id = aws_route_table.natgateway_route_table_1[count.index].id
subnet_id =

The following code will direct AWS to create a new key pair:

resource "tls_private_key" "public_key" {
algorithm = "RSA"
rsa_bits = 4096

resource "aws_key_pair" "ec2_key" {
key_name = var.key_name
public_key = tls_private_key.public_key.public_key_openssh

Create the RDS database instance within our VPC:

resource "aws_db_instance" "rds_mysql_instance" {
allocated_storage = var.rds_allocated_storage
engine = var.rds_engine
engine_version = var.rds_engine_version
instance_class = var.rds_instance_class
name = var.rds_name
username = var.rds_username
password = var.rds_password
parameter_group_name = var.rds_parameter_group_name
skip_final_snapshot = var.rds_skip_final_snapshot
publicly_accessible = var.rds_publicly_accessible
vpc_security_group_ids = []

Next, configure a security group for the RDS instance to allow access via specified ports:

resource "aws_security_group" "alb_sg" {
name = var.sg_name
description = var.sg_description
vpc_id =

ingress {
from_port = var.rds_from_port
to_port = var.rds_to_port
protocol = "tcp"
description = "MySQL"
self = true

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
description = "HTTP"
cidr_blocks = [""]

ingress {
from_port = 443
to_port = 443
protocol = "tcp"
description = "HTTPS"
self = true

egress {
from_port = var.sg_egress_from_port
to_port = var.sg_egress_to_port
protocol = var.sg_egress_protocol
cidr_blocks = var.sg_egress_cidr_blocks

tags = {
Name = var.sg_tagname

A last resource we will include in is the application load balancer, which we need to direct towards the public subnets:

resource "aws_alb" "alb" {
name = var.alb_name
internal = var.alb_internal
load_balancer_type = var.load_balancer_type
security_groups = []
subnets = [,]

enable_deletion_protection = var.enable_deletion_protection

tags = {
Environment = var.alb_tagname

For the file, we need to list and describe the variables called by in HCL format, as follows:

variable "region" {
type = string
default = "us-east-1"
description = "default region"

variable "vpc_cidr_block" {
type = string
default = ""
description = "default vpc_cidr_block"

The file terraform.tfvars will hold a key=value list of variables to pass through all of the “var.” prefix functions in My list includes 37 keywords for each occurence of “var.” in, with corresponding values. This list can be customized for your particular needs. Here is an example of the syntax:

vpc_name                             = "week17_vpc"
public_subnet_cidr_block_1 = ""
public_subnet_cidr_block_2 = ""

For the file, we will specify what values we want returned at the end of a successful “terraform apply” command. I have chosen the VPC, the RDS instance type, the three subnets, and the application load balancer. These are formatted in simple HCL style using the resource name, for example:

output "rds_instance_type" {
value = var.rds_instance_class
description = "RDS instance type"

Step 2: Plan

For the second part of the Terraform workflow, we’ll prepare the Terraform files we’ve configured for deployment and review Terraform’s assessment of their syntax and architectural design.

  • To clean up and streamline our source files, run: terraform fmt
  • To have Terraform assess the validity of our source files, run: terraform validate
  • For a list of the actions Terraform will perform, run: terraform plan

The list generated by ‘terraform plan’ will show a green ‘+’ next to the item to be created, and a red ‘-’ to indicate items to be removed. Details of the resources to be created are listed, and any problems with the source files will generate an error message. Terraform will specify the source file and location in which any errors occurred, as well as suggest troubleshooting steps for the particular error.

After a successful run of “terraform plan”, review the output to confirm the resources you’ve called for in the source files are listed in Terraform’s deployment preview. My “terraform plan” output ended with:

Plan: 18 to add, 0 to change, 0 to destroy.

Changes to Outputs:
+ alb_name = “week17alb”
+ private_subnet_1 = “”
+ public_subnet_1 = “”
+ public_subnet_2 = “”
+ rds_instance_type = “db.t3.micro”
+ vpc = “”

Once the fmt, validate, and plan commands are successful, we are ready to launch the Terraform file and create our AWS resources.

Step 3: Apply

This brings us to the third step in the Terraform workflow, Apply. The command “terraform apply” will provision the resources listed in the source files. Terraform will halt the command to prompt a response to the question:

Do you want to perform these actions?
Terraform will perform the actions described above.
Only ‘yes’ will be accepted to approve.

Enter a value:

Enter “yes” when prompted, and Terraform will begin listing the AWS resources as they are being created, and will list the time each resource took to create when they are finished. This process may take a few minutes, so sit back and relax while Terraform does it’s job.

When finished, Terraform will display your requested outputs and list the number of items added, changed, or destroyed:

Apply complete! Resources: 18 added, 0 changed, 0 destroyed.


alb_name = “week17alb”
private_subnet_1 = “”
public_subnet_1 = “”
public_subnet_2 = “”
rds_instance_type = “db.t3.micro”
vpc = “"

Further confirm the resources Terraform listed in its plan output by logging into the AWS console and navigating to the VPC and RDS consoles. A few of the resources created in my runthrough for this article are shown below:

RDS Database
Load Balancer

Finally, to avoid incurring costs for the AWS resources, do not forget to run “terraform destroy” after you complete the project.

Great work completing your AWS infrastructure build with Terraform!

Thank you for joining me in this short walkthrough.

(I must acknowledge the fine work of Paul Zhao, whose github repo I forked to use as a template for this project. For Paul’s original Terraform files, visit

DevOps Engineer pivoting from the legal profession.