2-Tier Architecture Setup on AWS Using Terraform
Architecture Diagram

Custom Modules
Using custom modules for this project, check out the previous blogs to know more about it.
EC2
We create an EC2 instance and pass on the data we have inside the user_data.sh. It is a simple flask application with a few endpoints, like: /, /health, /db_info.
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"]
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = [var.web_security_group_id]
associate_public_ip_address = false
user_data = templatefile("${path.module}/templates/user_data.sh", {
db_host = var.db_host
db_username = var.db_username
db_password = var.db_password
db_name = var.db_name
})
tags = {
Name = "${var.project_name}-web-server"
Environment = var.environment
}
}
RDS
This Terraform code sets up a private MySQL database (RDS instance) in AWS, fully isolated in your VPC's private subnets for security.
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-db-subnet-group"
Environment = var.environment
}
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
allocated_storage = var.allocated_storage
storage_type = "gp2"
engine = "mysql"
engine_version = var.engine_version
instance_class = var.instance_class
db_name = var.db_name
username = var.db_username
password = var.db_password
parameter_group_name = "default.mysql8.0"
skip_final_snapshot = true
vpc_security_group_ids = [var.db_security_group_id]
db_subnet_group_name = aws_db_subnet_group.main.name
publicly_accessible = false
tags = {
Name = "${var.project_name}-rds"
Environment = var.environment
}
}
Secrets
Using secrets, we generate a secure, random password and store it in AWS Secrets Manager, so it's never exposed in our code or Terraform state.
resource "random_password" "db_password" {
length = 16
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
resource "random_id" "suffix" {
byte_length = 4
}
resource "aws_secretsmanager_secret" "db_password" {
name = "${var.project_name}-${var.environment}-db-password-${random_id.suffix.hex}"
description = "Database password for ${var.project_name}"
tags = {
Name = "${var.project_name}-db-password"
Environment = var.environment
}
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db_password.result
engine = "mysql"
host = ""
})
}
Security Group
We configure three security groups to control network access for a typical web application:
ALB Security Group: Allows HTTP traffic (port 80) from anywhere (the internet) to your Application Load Balancer. It also allows all outbound traffic from the ALB.
Web Server Security Group: Only allows HTTP traffic (port 80) from the ALB (so only the load balancer can reach your web servers). It also allows SSH (port 22) from anywhere, but in a private setup, SSH access would typically be restricted to a bastion or VPN.
Database Security Group: Only allows MySQL traffic (port 3306) from the web server security group, so only your web servers can connect to the database. All outbound traffic is allowed.
These groups enforce a layered security model: the internet reaches —→ ALB — (talks to) —→ web servers — (talks to) —→database. With no direct public access to web or database servers.
Refer to the code in github repo linked at the top.
VPC
No explanation for the configuration. Refer to the repo for the information about the connection between all our components.
ALB
We sets up an Application Load Balancer to distribute HTTP traffic to our web servers.
resource "aws_lb" "main" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [var.alb_security_group_id]
subnets = var.public_subnet_ids
tags = {
Name = "${var.project_name}-alb"
Environment = var.environment
}
}
resource "aws_lb_target_group" "web" {
name = "${var.project_name}-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = 30
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.project_name}-tg"
Environment = var.environment
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
resource "aws_lb_target_group_attachment" "web" {
target_group_arn = aws_lb_target_group.web.arn
target_id = var.target_id
port = 80
}
Driver file - root main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Secrets Module
module "secrets" {
source = "./modules/secrets"
project_name = var.project_name
environment = var.environment
db_username = var.db_username
}
# VPC Module
module "vpc" {
source = "./modules/vpc"
project_name = var.project_name
environment = var.environment
aws_region = var.aws_region
vpc_cidr = var.vpc_cidr
public_subnet_cidrs = var.public_subnet_cidrs
private_subnet_cidrs = var.private_subnet_cidrs
}
# Security Groups Module
module "security_groups" {
source = "./modules/security_groups"
project_name = var.project_name
environment = var.environment
vpc_id = module.vpc.vpc_id
}
# RDS Module
module "rds" {
source = "./modules/rds"
project_name = var.project_name
environment = var.environment
private_subnet_ids = module.vpc.private_subnet_ids
db_security_group_id = module.security_groups.db_sg_id
db_name = var.db_name
db_username = var.db_username
db_password = module.secrets.db_password
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
engine_version = var.db_engine_version
}
# EC2 Module (Now in Private Subnet)
module "ec2" {
source = "./modules/ec2"
project_name = var.project_name
environment = var.environment
instance_type = var.ec2_instance_type
subnet_id = module.vpc.private_subnet_ids[0]
web_security_group_id = module.security_groups.web_sg_id
db_host = module.rds.db_endpoint
db_username = var.db_username
db_password = module.secrets.db_password
db_name = var.db_name
}
# ALB Module
module "alb" {
source = "./modules/alb"
project_name = var.project_name
environment = var.environment
vpc_id = module.vpc.vpc_id
public_subnet_ids = module.vpc.public_subnet_ids
private_subnet_ids = module.vpc.private_subnet_ids
target_id = module.ec2.instance_id
alb_security_group_id = module.security_groups.alb_sg_id
}
After cloning the project, run:
terraform init
terraform validate
terraform plan
terraform apply --auto-approve
After verification, make sure to:
terraform destroy --auto-approve
Verification
Endpoint verification:

Checking the endpoint: /health

Video Reference:
Arigato!