Optimizing AWS ALB with RDS Integration
This week, I expanded my AWS Application Load Balancer setup. I moved from HTTP header-based routing to path-based routing and added a complete database layer with RDS MySQL. This project shows how to build a full three-tier web application on AWS using Terraform.
What Changed from the Previous Setup
In my last post, I used HTTP header-based routing where the ALB checked a custom-header value to route traffic. Now I switched to path-based routing where the ALB looks at the URL path instead. I also added a third application that connects to a MySQL database.
Old Setup (Header-Based)
- 2 applications (App1, App2)
- Routing based on HTTP headers
- No database layer
- Query string and host header redirects
New Setup (Path-Based + Database)
- 3 applications (App1, App2, App3)
- Routing based on URL paths
- RDS MySQL Multi-AZ database
- App3 connects to the database
1) Path-Based Routing Instead of Header-Based
The biggest change is how the ALB routes traffic. Instead of checking headers, it now checks the URL path.
App1 Rule – Routes /app1 traffic
hcl
myapp1-rule = {
priority = 10
actions = [{
type = "weighted-forward"
target_groups = [{
target_group_key = "mytg1"
weight = 1
}]
stickiness = {
enabled = true
duration = 3600
}
}]
conditions = [{
path_pattern = {
values = ["/app1*"]
}
}]
}Explanation:
- Any request to
/app1or/app1/anythinggoes to target groupmytg1 - Priority 10 means this rule is checked first
- Stickiness keeps users on the same backend for 1 hour
App2 Rule – Routes /app2 traffic
hcl
myapp2-rule = {
priority = 20
actions = [{
type = "weighted-forward"
target_groups = [{
target_group_key = "mytg2"
weight = 1
}]
}]
conditions = [{
path_pattern = {
values = ["/app2*"]
}
}]
}Explanation:
- Requests to
/app2or/app2/anythinggo to target groupmytg2 - Priority 20 means this is checked after the App1 rule
App3 Rule – Default route for everything else
hcl
myapp3-rule = {
priority = 30
actions = [{
type = "weighted-forward"
target_groups = [{
target_group_key = "mytg3"
weight = 1
}]
}]
conditions = [{
path_pattern = {
values = ["/*"]
}
}]
}Explanation:
- The catch-all pattern
/*routes everything else tomytg3 - This is the lowest priority (30), so it only matches if App1 and App2 rules don’t match
- Perfect for a default application or home page
Why Path-Based Routing?
Path-based routing is more common and user-friendly than header-based routing:
- Easier testing: Just visit
/app1in your browser instead of sending custom headers - Better for end users: Normal web traffic uses paths, not custom headers
- SEO friendly: Search engines can index different paths
- Standard practice: Most microservices use path-based routing
Header-based routing is still useful for API gateways or internal services where you control the client, but for web applications, path-based is the standard.
2) Added RDS MySQL Database
I added a complete database layer with AWS RDS MySQL.
RDS Configuration
hcl
module "rdsdb" {
source = "terraform-aws-modules/rds/aws"
version = "6.9.0"
identifier = var.db_instance_identifier
db_name = var.db_name
username = var.db_username
password = var.db_password
engine = "mysql"
engine_version = "8.0.35"
instance_class = "db.t3.large"
allocated_storage = 20
max_allocated_storage = 100
multi_az = true
create_db_subnet_group = true
subnet_ids = module.vpc.database_subnets
vpc_security_group_ids = [module.rdsdb_sg.security_group_id]
performance_insights_enabled = true
performance_insights_retention_period = 7
}Key features:
- Multi-AZ: Automatic failover to another availability zone
- Auto-scaling storage: Grows from 20GB to 100GB as needed
- Performance Insights: 7-day monitoring enabled
- Backup: Configured backup windows and retention
- Separate subnets: Database runs in dedicated private subnets
Database Security Group
hcl
module "rdsdb_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "5.2.0"
name = "rdsdb-sg"
description = "Access to MySQL DB for entire VPC CIDR Block"
vpc_id = module.vpc.vpc_id
ingress_with_cidr_blocks = [{
from_port = 3306
to_port = 3306
protocol = "tcp"
description = "MySQL access from within VPC"
cidr_blocks = module.vpc.vpc_cidr_block
}]
egress_rules = ["all-all"]
}Explanation:
- Only allows MySQL traffic (port 3306) from within the VPC
- No direct internet access to the database
- Follows security best practices with layered security
3) App3 – Java Application with Database Connection
App3 is a Java Spring Boot application that connects to the RDS database. This is the new piece that makes this a true three-tier application.
App3 EC2 Instance Configuration
hcl
module "ec2_private_app3" {
depends_on = [ module.vpc ]
source = "terraform-aws-modules/ec2-instance/aws"
version = "5.7.0"
name = "${var.environment}-app3"
ami = data.aws_ami.amzlinux2.id
instance_type = var.instance_type
key_name = var.instance_keypair
user_data = templatefile("app3-ums-install.tmpl", {
rds_db_endpoint = module.rdsdb.db_instance_address
})
for_each = toset(["0", "1"])
subnet_id = element(module.vpc.private_subnets, tonumber(each.key))
vpc_security_group_ids = [module.private_sg.security_group_id]
}Key points:
- Uses
templatefile()to inject the RDS endpoint into the user data - Creates 2 instances across different availability zones
- Runs in private subnets (no direct internet access)
App3 Installation Script
bash
#! /bin/bash
sudo amazon-linux-extras enable java-openjdk11
sudo yum clean metadata && sudo yum -y install java-11-openjdk
mkdir /home/ec2-user/app3-usermgmt && cd /home/ec2-user/app3-usermgmt
wget https://github.com/stacksimplify/temp1/releases/download/1.0.0/usermgmt-webapp.war
export DB_HOSTNAME=${rds_db_endpoint}
export DB_PORT=3306
export DB_NAME=webappdb
export DB_USERNAME=dbadmin
export DB_PASSWORD=dbpassword11
java -jar /home/ec2-user/app3-usermgmt/usermgmt-webapp.war > /home/ec2-user/app3-usermgmt/ums-start.log &Explanation:
- Installs Java 11
- Downloads the User Management System WAR file
- Sets database connection environment variables
- The
${rds_db_endpoint}is replaced by Terraform with the actual RDS endpoint - Starts the Java application on port 8080
App3 Target Group
hcl
mytg3 = {
create_attachment = false
name_prefix = "mytg3-"
protocol = "HTTP"
port = 8080
target_type = "instance"
health_check = {
enabled = true
interval = 30
path = "/login"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 6
protocol = "HTTP"
matcher = "200-399"
}
}Important differences from App1/App2:
- Port 8080 instead of 80 (Java applications typically use 8080)
- Health check path is
/login(the login page of the User Management System) - Same stickiness and deregistration settings
4) Updated Security Group for App3
I updated the private security group to allow port 8080 for App3.
hcl
module "private_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "5.2.0"
name = "private-sg"
description = "Security Group with HTTP & SSH port open for entire VPC Block"
vpc_id = module.vpc.vpc_id
ingress_rules = ["ssh-tcp", "http-80-tcp", "http-8080-tcp"]
ingress_cidr_blocks = [module.vpc.vpc_cidr_block]
egress_rules = ["all-all"]
}What changed:
- Added
http-8080-tcpto the ingress rules - This allows App3 to receive traffic from the ALB
- Still only accessible from within the VPC
5) Enhanced Bastion Host
I upgraded the bastion host to include database tools.
bash
#! /bin/bash
sudo yum update -y
sudo rpm -e --nodeps mariadb-libs-*
sudo amazon-linux-extras enable mariadb10.5
sudo yum clean metadata
sudo yum install -y mariadb
sudo mysql -V
sudo yum install -y telnetWhy this matters:
- You can now connect to RDS from the bastion host
- Test database connectivity with
mysql -h <rds-endpoint> -u dbadmin -p - Useful for debugging and running SQL queries
telnethelps test port connectivity
6) Simplified DNS Configuration
I simplified the Route53 setup to use one DNS record instead of multiple.
Old Configuration (2 DNS records)
hcl
# myapps11.rezaops.com
# azure-aks11.rezaops.comNew Configuration (1 DNS record)
hcl
resource "aws_route53_record" "apps_dns" {
zone_id = data.aws_route53_zone.mydomain.zone_id
name = "dns-to-db.rezaops.com"
type = "A"
alias {
name = module.alb.dns_name
zone_id = module.alb.zone_id
evaluate_target_health = true
}
}
```
**Why one record:**
- All apps are behind the same ALB
- Routing happens by path, not by hostname
- Simpler DNS management
- All traffic goes through `dns-to-db.rezaops.com`
## 7) What Was Removed
To make room for the new architecture, I removed the redirect rules:
**Removed:**
- Query string redirect rule (`?website=aws-eks`)
- Host header redirect rule (`azure-aks11.rezaops.com`)
- HTTP header-based routing (`custom-header`)
**Why removed:**
These features were great for learning ALB capabilities, but in a real three-tier application, path-based routing is more practical and user-friendly.
## Architecture Overview
Here's how everything connects:
```
Internet → Route53 (dns-to-db.rezaops.com)
↓
Application Load Balancer (HTTPS)
↓
┌─────────────┬─────────────┬─────────────┐
│ /app1/* │ /app2/* │ /* │
│ ↓ │ ↓ │ ↓ │
│ App1 TG │ App2 TG │ App3 TG │
│ (port 80) │ (port 80) │ (port 8080)│
│ ↓ │ ↓ │ ↓ │
│ 2x EC2 │ 2x EC2 │ 2x EC2 │
│ (Static) │ (Static) │ (Java) │
└─────────────┴─────────────┴──────┬──────┘
↓
RDS MySQL (Multi-AZ)
(Database Subnets)Testing the Setup
After deployment, you can test all three applications:
App1 (Static HTML):
bash
curl https://dns-to-db.rezaops.com/app1/
# Shows pink background pageApp2 (Static HTML):
bash
curl https://dns-to-db.rezaops.com/app2/
# Shows teal background pageApp3 (Java + Database):
bash
curl https://dns-to-db.rezaops.com/
# Shows User Management System login pageDatabase Connection (from bastion):
bash
ssh -i terraform-key.pem ec2-user@<bastion-ip>
mysql -h <rds-endpoint> -u dbadmin -p
# Password: dbpassword11What I Learned
- Path-based routing is simpler: Users just visit different URLs, no need for custom headers
- RDS integration is straightforward: The Terraform RDS module handles most complexity
- Template files are powerful: Using
templatefile()to inject the database endpoint keeps the code clean - Security layers matter: Separate security groups for ALB, EC2, and RDS provide defense in depth
- Multi-AZ is important: Both the database and EC2 instances span multiple availability zones for high availability
Files Added
New files in this project:
c5-06-securitygroup-rdsdbsg.tf– RDS security groupc7-06-ec2instance-private-app3.tf– App3 instancesc13-01-rdsdb-variables.tf– Database variablesc13-02-rdsdb.tf– RDS configurationc13-03-rdsdb-outputs.tf– Database outputsapp3-ums-install.tmpl– App3 installation templatejumpbox-install.sh– Bastion with MySQL clientrdsdb.auto.tfvars– Database configurationsecrets.tfvars– Database password
Summary
I evolved my ALB setup from a header-based routing demo to a complete three-tier web application:
- Changed routing from HTTP headers to URL paths (
/app1,/app2,/) - Added RDS MySQL with Multi-AZ for high availability
- Added App3 – a Java application that connects to the database
- Enhanced the bastion host with MySQL client tools
- Simplified DNS to use one domain for all apps
- Improved security with separate security groups for each layer