Deploying a Two-Tier Architecture with Terraform
A Beginner’s Guide to Deploying a Two-Tier Architecture
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
- 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.**
- 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.
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
- Apply to specific projects and workspace
Note: Setting to specific projects and workspace for Demo - Select Default Project
- Select the workspace
- 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
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
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
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
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
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.
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;
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
It may take a bit, but after awhile you should see a Apply complete message.
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!
Helpful Resources //
Join me on https://www.linkedin.com/in/melissafoster08/ or follow me at https://github.com/mel-foster
All Screen Capture Images ©Melissa (Mel) Foster