Deploying a Two-Tier Architecture with Terraform

A Beginner’s Guide to Deploying a Two-Tier Architecture

Melissa (Mel) Foster
Women in Technology
16 min readJul 24, 2023

--

Image: from the author.

As I wrap up my time with Level Up in Tech, one of our last projects continues to surround Terraform. I highly enjoy working with Terraform and will continue to push myself after the program to expand my knowledge. Building upon my previous project, we will build a Two-Tier AWS Architecture and deploy with Terraform. I will be the first one to admit I got hung up and tried to over complicate my code. Stick to the Documentation provided below it the Helpful Resources section, and take your time. If you get frustrated, note that you can always stop your EC2s, or you can always run a Terraform Destroy, and start again.

You got this!

Objectives //

Create a highly available two-tier AWS architecture containing the following:

  • Create VPC with:
    -Public Subnets for the Web Server Tier
    -2 Private Subnets for the RDS Tier
  • Launch an Auto Scaling group that spans 2 subnets in your VPC
    min 2 max 5
  • Appropriate route tables
  • Launch an EC2 Instance with your choice of webserver in each public web tier subnet (Apache, NGINX, etc.)
  • One RDS MySQL Instance (micro) in the private RDS subnets
  • Security Groups properly configured for needed resources ( web servers, RDS)
  • Deploy this using Terraform Cloud as a CI/CD tool to check your build.
  • Optional Push your code to GitHub

To follow along with this project you will need //

  • Access to AWS
  • An Configured AWS Cloud9 with AWS CLI
  • Terraform Cloud Account
  • An Optional GitHub Account
  • Attention to Details

Setting up Cloud9 for Success//

For details setting up a Cloud9 Environment you can refer to this previous tutorial, look for the header: Creating an AWS Cloud9 Environment.

Optional GitHub Clone Repo//

Since starting my transition into tech, I have built the habit of pushing all and any code that I configure to GitHub. I recommend to all my readers, and anyone working on building a resource to work from.

  • Select Source Control from left hand menu
  • Select Clone Repo
Screen capture ©Melissa (Mel) Foster
  • Add Clone GitHub Repo link
  • Create new branch from Source Control Tab
    It is best practice to create a new branch to prevent from directly merging files without verification/approval.

Creating a Working Directory //

To help you stay organized create a working directory for this project. Example:wk22Terraform

mkdir <NAME YOUR DIRECTORY>
cd <NEWlY CREATED WORKING DIRECTORY>

Log Into Terraform Cloud //

With our Cloud9 Environment set up we can now log into Terraform Cloud.

  • Run
terraform login
  • Follow the prompt and copy and past http into your a new tab on your web browser
  • Enter in a Description
  • Select Generate token
  • Copy & Save token in a safe place as this is the only time you will have access to it. **Once you leave this page you will not have access to the token.**
Note: This is the one and only time you will have access! Safely Secure it where you can access again if needed
  • Paste the token in your CLI
  • Press enter
    Note: If you right click paste or “shift+ctrl+v” you will not see the token, just press enter and trust.
Success!

Awesome!! Going to take a quick pause on our Cloud9 Environment, and set up our Terraform Cloud to better allow us to complete the objectives successfully.

Setting up Terraform Cloud for Success //

  • Navigate to your Terraform Cloud https://app.terraform.io/
    Note: If you need to create an account check out the instructions here.
  • Select an existing organization or create a new one if this is your first time
  • Create Workspace
  • Settings → Variable Sets
  • Select Create variable set

On the next window we will be configuring Terraform Cloud to use our AWS Credentials. Note: If you misplaced or need to create new credentials, you can following the documentation linked here.

  • Configure Name
  • Add Description
Scroll Down to Variable Set Scope
  • Apply to specific projects and workspace
    Note: Setting to specific projects and workspace for Demo
  • Select Default Project
  • Select the workspace
Scroll Down to Variables
  • Set Environment
  • Enter Key: AWS_ACCESS_KEY_ID
  • Enter Value: This would be your Access Key ID
  • Mark Sensitive
  • Select Add variable
  • Select Add variable to repeat the process for our AWS_SECRET_ACCESS_KEY
  • Enter Key: AWS_SECRET_ACCESS_KEY
  • Enter Value: This would be your Secret Access Key
  • Mark Sensitive
  • Select Add variable
  • Select Save variable Set

Configure Terraform Cloud as Remote Backend //

In my previous project we set up an AWS S3 Bucket as our backend. This time we will be utilizing the Terraform Cloud.

  • Create File → New Text File → Save As → terraform.tf→ Save
    Note: If you configure a workspace in the cloud block of the terraform.tf that doesn’t exist. Terraform Cloud will create that new workspace for you when you run terraform init. Be sure to use the one we created to prevent errors and stay organized.
#Configure Terraform Cloud as Backend
terraform {
cloud {
organization = "MFosterLUIT22"

workspaces {
name = "Wk22-MelFoster"
}
}
}
  • Run
terraform fmt

terraform init
Success!

Awesome!! Now, you can close terraform.tf and we can build our infrastructure.

Building our Infrastructure //

Remaining in our CWD, we will create the files we will need to successfully build and deploy our infrastructure.

  • Create File → New Text File → Save As → providers.tf→ Save
  • Create File → New Text File → Save As → variables.tf→ Save
  • Create File → New Text File → Save As → script.sh→ Save

Note: My routine is to keep all files open and rotate through them as needed while building my infrastructure. With hands-on experience, you will find a rhythm that works for you. I will do my best to break down into sections to aid with understanding our objectives. Remember to utilize Documentation in Helpful Resource Section.

providers.tf //

  • Enter terraform source & version
  • Enter provider & region
#Wk22 Providers
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.7.0"
}
}
}

provider "aws" {
region = var.aws_region
}

variables.tf //

  • Set region
#Wk22 Main Variables

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

script.tf //

  • Create script to Bootstrap Apache to our EC2 at launch
#!/bin/bash
sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install apache2 -y
sudo apt-get systemctl start apache2
sudo apt-get systemctl enable apache2

echo "<html><head><title> Apache 2023 Terraform </title>
</head>
<body>
<h1>Congratulations! YOU DID IT!!! <h1>
<hr>
<article>
<p> Welcome Green Team!! You successfully launched an AWS EC2 Instance with a Custom Apache webpage, and completed the WK22 Terraform Objectives! <p>
<header><p> Completed by Mel Foster 07/09/2023 </p></body>
</html>" >/var/www/html/index.html
Examples. Note: Some images are not updated, but can still be used as reference

With these few configurations set, let’s continue building our two-tier infrastructure.

Starting from top to bottom, let’s build our VPC. Remember we will be updating our variables.tf as we go.

  • Create File → New Text File → Save As →main.tf→ Save
  • Define availability zones
  • Create custom vpc
  • Create 2 Public Subnets for web-tier
  • Create 2 Private Subnets for data-base tier
#Wk22 main

#Define Availability Zone
data "aws_availability_zones" "available" {}

#Create a VPC
resource "aws_vpc" "wk22_vpc" {
cidr_block = var.vpc-cidr
enable_dns_hostnames = true

tags = {
Name = "${var.name}-vpc"
}
}

#Create Public Subnets
resource "aws_subnet" "public_subnets" {
count = 2
vpc_id = aws_vpc.wk22_vpc.id
cidr_block = cidrsubnet(var.vpc-cidr, 8, count.index + 100)
availability_zone = element(data.aws_availability_zones.available.names, count.index)
map_public_ip_on_launch = true

tags = {
Name = var.public_subnets[count.index]
}
}

#Create Private Subnet
resource "aws_subnet" "private_subnets" {
count = 2
vpc_id = aws_vpc.wk22_vpc.id
cidr_block = cidrsubnet(var.vpc-cidr, 8, count.index)
availability_zone = element(data.aws_availability_zones.available.names, count.index)

tags = {
Name = var.public_subnets[count.index]
}
}

Update variable.tf //

Add the following to our variable.tf

  • Configure Availability Zone
  • Configure VPC CIDR
  • Configure Subnets
#Name for Project
variable "name" {
default = "mel_wk22_project"
}

#VPC Variables
variable "vpc-cidr" {
type = string
default = "10.0.0.0/16"
}

#Subnet Variables
variable "public_subnets" {
default = ["public_subnet_1", "public_subnet_2"]
}

variable "private_subnets" {
default = ["private_subnet_1", "private_subnet2"]
}

Awesome! Feel free to save as needed, and check for formatting and validate syntax as you write your code.

terraform fmt 

terraform validate

With our vpc and subnets set up we can move forward and work on creating our routing tables.

Take a quick little moment for a stretch! I am throwing a lot of information at you, and it can be overwhelming. I promise the more you work with Terraform the more it makes sense.

Note: If you need to stop at any time, make sure to save all your work in your Cloud9 Environment. Push to GitHub, and stop any EC2 Instance you have running. Do not terminate it just yet.

When you are ready our next step is to create our routes.tf, and work on configuring our routing tables.

  • Create File → New Text File → Save As → routes.tf

routes.tf //

  • Configure Route Table for Public Subnets
  • Configure Route Table for Private Subnets
  • Configure Route Table Associations
#Wk 22 Route Tables

#Public Route Table
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.wk22_vpc.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.wk22_igw.id
}
tags = {
Name = "${var.name}-public_rt"
}
}

#Private Route Table
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.wk22_vpc.id

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gateway.id
}
tags = {
Name = "${var.name}-private-rt"
}
}

#ROUTE TABLE ASSOCIATIONS

#Public Route Table
resource "aws_route_table_association" "public" {
for_each = { for idx, subnet in aws_subnet.public_subnets : idx => subnet }
subnet_id = each.value.id
route_table_id = aws_route_table.public_rt.id
}



#Private Route Table
resource "aws_route_table_association" "private" {
for_each = { for idx, subnet in aws_subnet.private_subnets : idx => subnet }
subnet_id = each.value.id
route_table_id = aws_route_table.private_rt.id
}
  • Save
  • Check formatting & validate syntax
terraform fmt

terraform validate

Create Internet Gateway //

Alright, we are now ready to create our Internet Gateway.

  • Create File → New Text File → Save As → gateway.tf

Our gateway.tf will include:

  • Internet Gateway Resource Block
  • Elastic IP Address associating with our IGW
  • Create a NAT Gateway
#Create Internet Gateway
resource "aws_internet_gateway" "wk22_igw" {
vpc_id = aws_vpc.wk22_vpc.id

tags = {
Name = "${var.name}-igw"
}
}

#Create Elastic IP
resource "aws_eip" "wk22_eip" {
domain = "vpc"
depends_on = [aws_internet_gateway.wk22_igw]
}

#Create Nat Gateway
resource "aws_nat_gateway" "nat_gateway" {
allocation_id = aws_eip.wk22_eip.id
subnet_id = aws_subnet.public_subnets[0].id

tags = {
Name = "${var.name}-nat_gateway"
}
}
  • Save
  • Check formatting & validate syntax
terraform fmt

terraform validate

Moving right along, we can create our Web-Tier Security Group. Here we will configure our security group to allow all HTTP & SSH traffic.

  • Create File → New Text File → Save As → web-sg.tf
#wk22 Web Tier Security Group
#Create a security group that allows traffic from the internet
resource "aws_security_group" "web_sg" {
name = "web_sg"
description = "Allow web traffic"
vpc_id = aws_vpc.wk22_vpc.id

ingress {
description = "Allow all HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
description = "Allow all HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
description = "Allow all SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
description = "Allow MySQL"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name}-web_sg"
}
}

Feel free to close files that you are completely finished working with. Highly recommend keeping variables.tf open just a little bit longer. We aren’t done with that one yet.

Configure our EC2 Instance //

I feel like so far we are set up for success. Our network security settings are now all out of the way. Our objective is to launch an EC2 Instance with your choice of webserver in each public web tier subnet. I will be launching Apache.

  • Create File → New Text File → Save As → ec2.tf
  • Configure to obtain public subnets in VPC
  • Set User Data to pull from our script.sh
#EC2 Configuration

# Create Launch Template Resource Block & Bootstrap Apache
resource "aws_launch_template" "asg_template" {
name = var.name
image_id = var.instance_ami
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web_sg.id]
user_data = filebase64("script.sh")

tags = {
Name = "${var.name}-template"
}
}

# Create ASG Resource Block
resource "aws_autoscaling_group" "asg" {
name = "${var.name}-asg"
vpc_zone_identifier = [for subnet in aws_subnet.public_subnets : subnet.id]
desired_capacity = 2
max_size = 5
min_size = 2
tag {
key = "Name"
value = "wk22EC2_Foster"
propagate_at_launch = true
}

launch_template {
id = aws_launch_template.asg_template.id
version = aws_launch_template.asg_template.latest_version
}
}

Once again update our variables.tf. Add the following:

  • Configure AMI (I am using Ubuntu)
  • Set Instance Type
#EC2 Variables
variable "instance_ami" {
type = string
description = "AMI ID for the Ubuntu EC2 instance"
default = "ami-053b0d53c279acc90"
}

variable "instance_type" {
description = "Instance Type"
type = string
default = "t2.micro"
}
  • Save
  • Check formatting & validate
terraform fmt

terraform validate

If we were to stop at this point and run through our Terraform workflow commands

terraform fmt 
terraform init
terraform validate
terraform plan
terraform apply

You should have about 17 to add!

If you took notice, at the beginning of running terraform plan, a message from Terraform Cloud appears.

If you copy and paste that website into your internet browser, you can see that the Cloud is preparing your build.

Looking good! We just have one more objective to complete before apply/deploying. We need to create our MySQL database.

Create Terraform Cloud Database Variables //

To start this section we are going to navigate over to our Terraform Cloud to add variables to our workspace. Our goal here is to create user name and password to sync to our data base. This will allow us to use Terraform Cloud to store our sensitive information. If we hard code this information even marking as sensitive into our files, information could potentially still be written into state file, or even logs. Resulting in anyone with access to the database would be able to see your user name and password. Let’s get secure!

  • Navigate to Terraform Cloud → Workspaces → Wk22 → Variables
  • Add Key = username
  • Add Value = [ENTER THE USER NAME YOU WANT TO USE]
  • Mark Sensitive
  • Select Add variable
  • Select Add variable to repeat the process for creating a database password
  • Add Key = password
  • Add Value = [ENTER THE PASSWORD YOU WANT TO USE]
  • Mark Sensitive
  • Select Add variable
Success!

Time to update our variables.tf to include our Terraform Cloud variables.

variable "password" {}

variable "username" {}
  • Save
  • Check formatting & validate syntax with Terraform commands.

Database Tier //

  • Create File → New Text File → Save As → database.tf
  • Configure private subnets to keep the database private
  • Configure RDS MySQL
    Note: we are referencing our Terraform Cloud variables we already added to our variables.tf
#Wk22 Database

#Configure private subnets created in VPC
resource "aws_db_subnet_group" "mysql_subnet_group" {
name = "mysql_subnet_group"
subnet_ids = [for subnet in aws_subnet.private_subnets : subnet.id]

tags = {
Name = "${var.name}-subnet_group"
}
}

#Launch one RDS MySQL instance in a private subnet
resource "aws_db_instance" "mysql" {
allocated_storage = 10
max_allocated_storage = 20
storage_type = "gp2"
db_subnet_group_name = aws_db_subnet_group.mysql_subnet_group.name
db_name = "wk22_mysql"
engine = "mysql"
engine_version = "8.0.32"
instance_class = "db.t3.micro"
publicly_accessible = false
username = var.username
password = var.password
skip_final_snapshot = true
vpc_security_group_ids = [aws_security_group.mysql_sg.id]
storage_encrypted = true
deletion_protection = false
port = 3306

tags = {
name = "wk22-mysql"
}
}

Next we need to set up coinciding database security group.

  • Create File → New Text File → Save As → db-sg.tf
#wk22 Module DB-Security Group
#Create security group for database tier from the web-server tier

resource "aws_security_group" "mysql_sg" {
name = "mysql_sg"
description = "mysql_sg"
vpc_id = aws_vpc.wk22_vpc.id

ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "wk22-mysql_sg"
}
}
  • Save
  • Check formatting & validate syntax with Terraform commands.

Wow! That’s a lot and we have one more file to create and then we can deploy!

Output //

We can automatically get information from our codes by creating an outputs.tf file, and then add what output vales we would like.
Example of an output:

  • Create File → New Text File → Save As → output.tf
#wk22 output

output "list_of_az" {
value = data.aws_availability_zones.available[*].names
}

# output of VPC Id
output "vpc_id" {
value = aws_vpc.wk22_vpc.id
}

Two Ways to Deploy //

We made it to deployment!! Let’s go!

  • Run Terraform Workflow from CLI
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply -auto-approve

Another way to deploy is from Terraform Cloud.
Note: If you successfully deployed from the CLI that’s awesome. You can run a quick terraform destroy -auto-approve to try out running from Terraform Cloud ULI.

  • Run workflow commands through CLI
terraform init
terraform fmt
terraform validate
terraform plan
  • Scroll up to copy http for quick link to your Terraform Cloud

Alright you ready??

  • Navigate over to your Terraform Cloud in your web browser.
  • Select Actions
  • Select New Run
  • Enter Reason for starting run
    Ours is Deployment of WK22 Infrastructure
  • Choose Run Type (Standard)
  • Select Start Run
Success!

Amazing job so far! It’s that nerve wrecking part where we need to ensure it’s all correctly launched and ready for our team.

Time to Verify //

Utilizing our AWS Console navigate to verify the following:

  • VPC
  • Subnets
  • Security Groups
  • Elastic IP
  • Gateways
  • Route Tables
  • EC2
VPC Created Successfully
Successful Subnets Created
Resource Map Flows Correctly
Successful Security Groups Created
Elastic IP Created Successfully
Successful Creation of Internet Gateway
Successful Creation of NAT Gateway
Successfully Created
Successful EC2’s Launched
Auto Scale working just how we want to see it!
Success!!
Woo-hoo!! SUCCESS!!
Adobe Free Stock Image

Congratulations!! I did one heck of a happy dance getting this to work. My important message to you, is that if you are struggling, it is ok to ask for help. It is ok to walk away and grab a coffee, or restart. My hold up on completing this project was my user data. I had two little words switched. It is important to take your time, and be consistent with your work. Be diligent, be calm and you will find the hold up.

Verify Database //

Our final verification will be of our Database. From your AWS Console navigate to RDS.

Make note of endpoint as we will need this to log into our DB

Now, with our Endpoint in hand we will install MySQL on your public instance. Navigate back to one of your public EC2.

  • Select Connect

Next, after you are connect to your EC2, you will need to install MySQL.

  • Install MySQL
sudo apt install mysql-server -y
  • Run this with your endpoint, and username
    **You will be prompted to enter your password we created previously

Special Note: If you fail to remember the user name and password, that we created on Terraform Cloud as variables. You can delete them, re-add them. Then you will need to relaunch your DB again, by running terraform plan, and terraform approve.

mysql -h <endpoint> -P 3306 -u <USERNAME> -p
  • Show your database
SHOW DATABASES;
Success! Highest of fives to you!!

Optional GitHub //

As always, highly recommend pushing all code you create to GitHub. I have included a direct link to this project. Need a refresh on GitHub, check out this previous walk-through.

Clean Up Time //

Time to Destroy the things we no longer need!

  • Run
terraform destroy -auto-approve
View from Terraform Cloud

It may take a bit, but after awhile you should see a Apply complete message.

Success view from CLI
Successful Clean-up View on Cloud
Adobe Free Stock Image

I am so appreciative of you all supporting me on my journey with Level Up in Tech, and reading all the projects I completed through them. Don’t worry I am still planning on continuing to bring you projects and walk-throughs. See you soon!

--

--

Melissa (Mel) Foster
Women in Technology

𝔻𝕖𝕧𝕆𝕡𝕤 𝗘𝗻𝗴𝗶𝗻𝗲𝗲𝗿 |𝒲𝑜𝓂𝑒𝓃 𝐼𝓃 𝒯𝑒𝒸𝒽 𝒜𝒹𝓋𝑜𝒸𝒶𝓉𝑒 | 𝚂𝚘𝚌𝚒𝚊𝚕 𝙼𝚎𝚍𝚒𝚊 𝙲𝚛𝚎𝚊𝚝𝚘𝚛 | Photographer