Loading
Reza Chegini

Junior DevOps Engineer

Junior Cloud Engineer

Junior Site Reliability Engineer

Software Engineer

Backend Developer

Reza Chegini

Junior DevOps Engineer

Junior Cloud Engineer

Junior Site Reliability Engineer

Software Engineer

Backend Developer

Blog Post

Optimizing AWS ALB with RDS Integration

October 14, 2025 AWS, DevOps, Infrastructure, Terraform
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 /app1 or /app1/anything goes to target group mytg1
  • 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 /app2 or /app2/anything go to target group mytg2
  • 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 to mytg3
  • 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 /app1 in 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-tcp to 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 telnet

Why 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
  • telnet helps 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.com

New 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 page

App2 (Static HTML):

bash

curl https://dns-to-db.rezaops.com/app2/
# Shows teal background page

App3 (Java + Database):

bash

curl https://dns-to-db.rezaops.com/
# Shows User Management System login page

Database Connection (from bastion):

bash

ssh -i terraform-key.pem ec2-user@<bastion-ip>
mysql -h <rds-endpoint> -u dbadmin -p
# Password: dbpassword11

What I Learned

  1. Path-based routing is simpler: Users just visit different URLs, no need for custom headers
  2. RDS integration is straightforward: The Terraform RDS module handles most complexity
  3. Template files are powerful: Using templatefile() to inject the database endpoint keeps the code clean
  4. Security layers matter: Separate security groups for ALB, EC2, and RDS provide defense in depth
  5. 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 group
  • c7-06-ec2instance-private-app3.tf – App3 instances
  • c13-01-rdsdb-variables.tf – Database variables
  • c13-02-rdsdb.tf – RDS configuration
  • c13-03-rdsdb-outputs.tf – Database outputs
  • app3-ums-install.tmpl – App3 installation template
  • jumpbox-install.sh – Bastion with MySQL client
  • rdsdb.auto.tfvars – Database configuration
  • secrets.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

Tags:
Write a comment