DEV Community

Cover image for Implementing Path based routing with Application Load Balancer ALB and EC2 Instances using Terraform
Chinmay Tonape
Chinmay Tonape

Posted on

Implementing Path based routing with Application Load Balancer ALB and EC2 Instances using Terraform

In this tutorial, we'll leverage Terraform to set up an architecture that includes an Application Load Balancer (ALB) with path-based routing to EC2 instances hosting different web services.

Path-based routing is a method of directing traffic to different backend services based on the URL path of the incoming request. This approach allows you to host multiple applications or services on the same domain or load balancer, each accessible via a unique path. For example, you could route example.com/app1 requests to one set of servers and example.com/app2 requests to another.

Architecture Overview

Before diving into the implementation details, let's outline the architecture we will be working with:

Architecture

Step 1: Create VPC

First, we'll define a Virtual Private Cloud (VPC) and networking components

################################################################################
# Create VPC and components
################################################################################

module "vpc" {
  source                        = "./modules/vpc"
  aws_region                    = var.aws_region
  vpc_cidr_block                = var.vpc_cidr_block
  enable_dns_hostnames          = var.enable_dns_hostnames
  vpc_public_subnets_cidr_block = var.vpc_public_subnets_cidr_block
  aws_azs                       = var.aws_azs
  common_tags                   = local.common_tags
  naming_prefix                 = local.naming_prefix
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Two Web Services on Separate EC2 Instances

Next, we'll provision two groups EC2 instances within our VPC, each running a distinct web service or application. These instances will serve as our backend servers behind the ALB.

First EC2 instance will be at default path and second instance will be at path pattern /other/, therefore keep the web files at /var/www/html/other/ for second EC2 instance.

################################################################################
# Get latest Amazon Linux 2 AMI
################################################################################
data "aws_ami" "amazon-linux-2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

################################################################################
# Create the Linux EC2 Web server
################################################################################
resource "aws_instance" "web" {
  ami             = data.aws_ami.amazon-linux-2.id
  instance_type   = var.instance_type
  key_name        = var.instance_key
  security_groups = var.security_group_ec2

  count     = length(var.public_subnets)
  subnet_id = element(var.public_subnets, count.index)


  user_data = <<-EOF
  #!/bin/bash
  yum update -y
  yum install -y httpd.x86_64
  systemctl start httpd.service
  systemctl enable httpd.service
  instanceId=$(curl http://169.254.169.254/latest/meta-data/instance-id)
  instanceAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)
  pubHostName=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
  pubIPv4=$(curl http://169.254.169.254/latest/meta-data/public-ipv4)
  privHostName=$(curl http://169.254.169.254/latest/meta-data/local-hostname)
  privIPv4=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)

  echo "<font face = "Verdana" size = "5">"                                       > /var/www/html/index.html
  echo "<center><h1>AWS Linux VM Deployed with Terraform</h1></center>"          >> /var/www/html/index.html
  echo "<center> <b>EC2 Instance Metadata</b> </center>"                         >> /var/www/html/index.html
  echo "<center> <b>Instance ID:</b> $instanceId </center>"                      >> /var/www/html/index.html
  echo "<center> <b>AWS Availablity Zone:</b> $instanceAZ </center>"             >> /var/www/html/index.html
  echo "<center> <b>Public Hostname:</b> $pubHostName </center>"                 >> /var/www/html/index.html
  echo "<center> <b>Public IPv4:</b> $pubIPv4 </center>"                         >> /var/www/html/index.html
  echo "<center> <b>Private Hostname:</b> $privHostName </center>"               >> /var/www/html/index.html
  echo "<center> <b>Private IPv4:</b> $privIPv4 </center>"                       >> /var/www/html/index.html
  echo "</font>"                                                                 >> /var/www/html/index.html
EOF

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-ec2-${count.index + 1}"
  })
}

################################################################################
# Create the Linux EC2 Web server - other instance
################################################################################
resource "aws_instance" "web_other" {
  ami             = data.aws_ami.amazon-linux-2.id
  instance_type   = var.instance_type
  key_name        = var.instance_key
  security_groups = var.security_group_ec2

  count     = length(var.public_subnets)
  subnet_id = element(var.public_subnets, count.index)


  user_data = <<-EOF
  #!/bin/bash
  yum update -y
  yum install -y httpd.x86_64
  systemctl start httpd.service
  systemctl enable httpd.service
  instanceId=$(curl http://169.254.169.254/latest/meta-data/instance-id)
  instanceAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)
  pubHostName=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
  pubIPv4=$(curl http://169.254.169.254/latest/meta-data/public-ipv4)
  privHostName=$(curl http://169.254.169.254/latest/meta-data/local-hostname)
  privIPv4=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)

  mkdir -p /var/www/html/other
  echo "<font face = "Verdana" size = "5">"                                                 > /var/www/html/other/index.html
  echo "<center><h1>AWS Linux VM Deployed with Terraform - Other Instance</h1></center>"   >> /var/www/html/other/index.html
  echo "<center> <b>EC2 Instance Metadata</b> </center>"                                   >> /var/www/html/other/index.html
  echo "<center> <b>Instance ID:</b> $instanceId </center>"                                >> /var/www/html/other/index.html
  echo "<center> <b>AWS Availablity Zone:</b> $instanceAZ </center>"                       >> /var/www/html/other/index.html
  echo "<center> <b>Public Hostname:</b> $pubHostName </center>"                           >> /var/www/html/other/index.html
  echo "<center> <b>Public IPv4:</b> $pubIPv4 </center>"                                   >> /var/www/html/other/index.html
  echo "<center> <b>Private Hostname:</b> $privHostName </center>"                         >> /var/www/html/other/index.html
  echo "<center> <b>Private IPv4:</b> $privIPv4 </center>"                                 >> /var/www/html/other/index.html
  echo "</font>"                                                                           >> /var/www/html/other/index.html
EOF

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-ec2-${count.index + 1}-other"
  })
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure ALB with Path-Based Routing

The core of our setup involves configuring the ALB to route incoming requests based on their URL paths:

ALB Configuration: We'll define an ALB with multiple target groups, each associated with one of our target group of EC2 instances. This setup allows the ALB to route traffic to different sets of instances based on the URL path.

Make sure health checks for alb target groups are correctly configured.

################################################################################
# create application load balancer
################################################################################
resource "aws_lb" "aws-application_load_balancer" {
  internal           = false
  load_balancer_type = "application"
  security_groups    = [var.security_group_alb[0]]
  //subnets                    = [var.public_subnets[0],var.public_subnets[1] ,var.public_subnets[2],var.public_subnets[3]]
  subnets                    = tolist(var.public_subnets)
  enable_deletion_protection = false

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-alb"
  })
}

################################################################################
# create target groups for ALB - Default and Other
################################################################################
resource "aws_lb_target_group" "alb_target_group_default" {
  target_type = "instance"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id

  health_check {
    enabled             = true
    interval            = 30
    path                = "/"
    timeout             = 5
    matcher             = 200
    healthy_threshold   = 2
    unhealthy_threshold = 5
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-alb-tg-default"
  })
}

resource "aws_lb_target_group" "alb_target_group_other" {
  target_type = "instance"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id

  health_check {
    enabled             = true
    interval            = 30
    path                = "/other/"
    timeout             = 5
    matcher             = 200
    healthy_threshold   = 2
    unhealthy_threshold = 5
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-alb-tg-other"
  })
}

Enter fullscreen mode Exit fullscreen mode

Listener Rules: We'll create listener rules using Terraform to specify path-based routing logic. For instance, requests to /* will be forwarded to default target group, while requests to /other/* will be directed to another.

################################################################################
# create a listener on port 80 with redirect action - Default and Other
################################################################################
resource "aws_lb_listener" "alb_http_listener_default" {
  load_balancer_arn = aws_lb.aws-application_load_balancer.id
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_target_group_default.id

  }
}

################################################################################
# create a listener rule for other listener
################################################################################
resource "aws_lb_listener_rule" "alb_rule_other" {
  listener_arn = aws_lb_listener.alb_http_listener_default.arn
  priority     = 10
  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_target_group_other.arn
  }
  condition {
    path_pattern {
      values = ["/other/"]
    }
  }
}

################################################################################
# Target Group Attachment with Instance
################################################################################

resource "aws_alb_target_group_attachment" "alb_tg_attach_default" {
  count            = length(var.instance_ids)
  target_group_arn = aws_lb_target_group.alb_target_group_default.arn
  target_id        = element(var.instance_ids, count.index)
}


resource "aws_alb_target_group_attachment" "alb_tg_attach_other" {
  count            = length(var.instance_ids_other)
  target_group_arn = aws_lb_target_group.alb_target_group_other.arn
  target_id        = element(var.instance_ids_other, count.index)
}

Enter fullscreen mode Exit fullscreen mode

Steps to Run Terraform

Follow these steps to execute the Terraform configuration:

terraform init
terraform plan 
terraform apply -auto-approve
Enter fullscreen mode Exit fullscreen mode

Upon successful completion, Terraform will provide relevant outputs.

Apply complete! Resources: 23 added, 0 changed, 0 destroyed.

Outputs:

alb_dns_name = "tf-lb-20240724121109991900000006-1464435819.us-east-1.elb.amazonaws.com"
ec2_instance_ids = [
  "i-0c3ec981cbc105993",
  "i-034025ab95d436bcb",
]
public_subnets = [
  "subnet-0bf0fb37d7fe382ac",
  "subnet-086da92937739db51",
]
security_group_alb = [
  "sg-0c1110387f85745d4",
]
security_groups_ec2 = [
  "sg-0ea78de50cb1702e2",
]
Enter fullscreen mode Exit fullscreen mode

Testing

ALB with a listener with 2 rules:

ALB with listener

Listener rules Default and Other

Listener Rules

ALB Resource Map:

ALB Resource Map

Default route EC2 web instances:

EC2

EC2

Path based route EC2 web instances:

EC2

EC2

All running instances:

AllInstances

Cleanup

Remember to stop AWS components to avoid large bills.

terraform destroy -auto-approve
Enter fullscreen mode Exit fullscreen mode

Conclusion

By following this guide and using Terraform for infrastructure as code, you can easily implement path-based routing with ALB and EC2 instances on AWS. This approach enhances scalability and simplifies management by consolidating multiple services under a single load balancer while maintaining clear separation based on URL paths.

Resources

ALB Path Based Routing: https://repost.aws/knowledge-center/elb-achieve-path-based-routing-alb

Github Link: https://github.com/chinmayto/terraform-aws-alb-path-based-routing

Top comments (0)