Terraform: Deploy Application Load Balancer on AWS

Terraform: Deploy Application Load Balancer on AWS

Introduction

In the first Infrastructure as Code (IaC) tutorial using Terraform, we explored how to install the CLI and launch an EC2 instance with external input variables. The Terraform CLI version was v0.12.29 then, but the latest version at the time of writing is v.1.2.3; hence, some syntaxes will be deprecated. This tutorial will launch an Application Load Balancer (ALB) and Auto Scaling Group (ASG) in a VPC and security groups in an AWS environment. This example is adopted from the book Terraform: Up & Running, 2nd Edition by O’Reilly Media. Note that some resource syntaxes here are updated and may differ slightly from the book.

Prerequisites

  • Install any Terraform extension into the code editor for highlighting and auto-completion on *.tf files.
  • An active AWS account to test and deploy this Iac script using aws configure and Terraform CLI
  • A keen eye to google for solutions after encountering terraform apply errors because, by the time you attempt this Iac script, some syntax might be deprecated.
  • Last, REMEMBER tearing down all resources (terraform destroy) deployed in AWS because resources such as Application Load Balancer and EC2 instances in the Auto Scaling group are not free.

1. Codes

1.1. Launch Configuration and Auto Scaling Group

A launch configuration is a template that an Auto Scaling group (ASG) uses to launch EC2 instances. We will specify the Amazon Machine Image (AMI), instance type, key pair, and security group(s). For example, if an instance is terminated, ASG will launch a new instance and all future instances based on these configurations. Next, we define the minimum and maximum size of kept-alive EC2 instances in the ASG. The user_data installs Apache (httpd) and creates an index.html file, while the rest tags the EC2 instances to a security group and launches the ASG in a virtual private cloud (VPC).

# Launch configuration
resource "aws_launch_configuration" "example" {
    image_id = "ami-0c55b159cbfafe1f0"
    instance_type = "t2.micro"
    security_groups = [aws_security_group.instance.id]

    # Create a simple "Hello, World" HTML and install Apache (listen port 8080)
    user_data = <<-EOF
                #!/bin/bash
                echo "Hello, World" > index.html
                nohup busybox httpd -f -p ${var.server_port} &
                EOF

    # Required when using launch configuration with auto scaling group
    lifecycle {
        create_before_destroy = true
    }
}

# Auto Scaling group
resource "aws_autoscaling_group" "example" {
    launch_configuration = aws_launch_configuration.example.name
    vpc_zone_identifier = data.aws_subnets.default.ids

    target_group_arns = [aws_lb_target_group.asg.arn]
    health_check_type = "ELB"

    # Minimum and maximum size of Auto Scaling Group
    min_size = 2
    max_size = 10

    # Name the ASG (optional)
    tag {
        key = "Name"
        value = "terraform-asg-example"
        propagate_at_launch = true
    }
}

1.2. Load Balancer and Target Group

Here, we configure an application-based load balancer (ALB) and associate it with a subnet and security group. The load balancer will listen on HTTP port 80 and return a 404 Page not found error on status_code = 404. The ALB will then route all requests (“/”) to a target group with conditions such as healthy and unhealthy instances threshold.

# Application Load Balancer
resource "aws_lb" "example" {
    name = "terraform-asg-example"

    load_balancer_type = "application"
    subnets = data.aws_subnets.default.ids
    security_groups = [aws_security_group.alb.id]
}

# Listening port for Application Load Balancer
resource "aws_lb_listener" "http" {
    load_balancer_arn = aws_lb.example.arn
    port = 80
    protocol = "HTTP"

    # Default return 404 page
    default_action {
        type = "fixed-response"

        fixed_response {
            content_type = "text/plain"
            message_body = "404: page not found"
            status_code = 404
        }
    }
}

# Listener rule for Application Load Balancer
resource "aws_lb_listener_rule" "asg" {
    listener_arn = aws_lb_listener.http.arn
    priority = 100

    condition {
      path_pattern {
          values = ["*"]
      }
    }

    # Distribute requests to 1 or more target groups
    action {
        type = "forward"
        target_group_arn = aws_lb_target_group.asg.arn
    }
}

# Target group 1 for Application Load Balancer
resource "aws_lb_target_group" "asg" {
    name = var.alb_name

    port = var.server_port
    protocol = "HTTP"
    vpc_id = data.aws_vpc.default.id

    health_check {
      path = "/"
      protocol = "HTTP"
      matcher = "200"
      interval = 15
      timeout = 3

      # Number of consecutive health checks before consider target as unhealthy
      healthy_threshold = 2
      unhealthy_threshold = 2
    }
}

1.3. Security Groups

The EC2 instances will exchange traffic with the target group via Port 8080 (var.server_port) only and deny all others. The Application Load Balancer (ALB) listens to incoming traffic at Port 80 and imposes an any-to-any rule on outbound traffic.

# Security group for EC2 instances
resource "aws_security_group" "instance" {
    name = var.security_group_name

    # Destination:TCP/8080 | Source:Any
    ingress {
        from_port = var.server_port
        to_port = var.server_port
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

# Security group for Application Load Balancer
resource "aws_security_group" "alb" {
    name = var.alb_security_group_name

    # Allow inbound
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    # Allow outbound
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

1.4. VPC and Subnets

We will use the default VPC and subnet for the region to keep things simple. Lastly, we associate the default subnet to the VPC.

# Default VPCs
data "aws_vpc" "default" {
    default = true
}

# Default subnet for VPC
data "aws_subnets" "default" {
    filter {
        name   = "vpc-id"
        values = [data.aws_vpc.default.id]
    }
}

2. Putting All Together

2.1. main.tf

Below is the full Iac Terraform script (main.tf). The region in provider “aws” will overwrite the default region name settings in aws configure

# Choose a low-cost region near to your Terraform CLI server
provider "aws" {
    region = "us-east-2"
}

# Launch configuration
resource "aws_launch_configuration" "example" {
    image_id = "ami-0c55b159cbfafe1f0"
    instance_type = "t2.micro"
    security_groups = [aws_security_group.instance.id]

    # Create a simple "Hello, World" HTML and install Apache (listen port 8080)
    user_data = <<-EOF
                #!/bin/bash
                echo "Hello, World" > index.html
                nohup busybox httpd -f -p ${var.server_port} &
                EOF

    # Required when using launch configuration with auto scaling group
    lifecycle {
        create_before_destroy = true
    }
}

# Auto Scaling group
resource "aws_autoscaling_group" "example" {
    launch_configuration = aws_launch_configuration.example.name
    vpc_zone_identifier = data.aws_subnets.default.ids

    target_group_arns = [aws_lb_target_group.asg.arn]
    health_check_type = "ELB"

    # Minimum and maximum size of Auto Scaling Group
    min_size = 2
    max_size = 10

    # Name the ASG (optional)
    tag {
        key = "Name"
        value = "terraform-asg-example"
        propagate_at_launch = true
    }
}

# Application Load Balancer
resource "aws_lb" "example" {
    name = "terraform-asg-example"

    load_balancer_type = "application"
    subnets = data.aws_subnets.default.ids
    security_groups = [aws_security_group.alb.id]
}

# Listening port for Application Load Balancer
resource "aws_lb_listener" "http" {
    load_balancer_arn = aws_lb.example.arn
    port = 80
    protocol = "HTTP"

    # Default return 404 page
    default_action {
        type = "fixed-response"

        fixed_response {
            content_type = "text/plain"
            message_body = "404: page not found"
            status_code = 404
        }
    }
}

# Listener rule for Application Load Balancer
resource "aws_lb_listener_rule" "asg" {
    listener_arn = aws_lb_listener.http.arn
    priority = 100

    condition {
      path_pattern {
          values = ["*"]
      }
    }

    # Distribute requests to 1 or more target groups
    action {
        type = "forward"
        target_group_arn = aws_lb_target_group.asg.arn
    }
}

# Target group 1 for Application Load Balancer
resource "aws_lb_target_group" "asg" {
    name = var.alb_name

    port = var.server_port
    protocol = "HTTP"
    vpc_id = data.aws_vpc.default.id

    health_check {
      path = "/"
      protocol = "HTTP"
      matcher = "200"
      interval = 15
      timeout = 3

      # Number of consecutive health checks before consider target as unhealthy
      healthy_threshold = 2
      unhealthy_threshold = 2
    }
}

# Security group for EC2 instances
resource "aws_security_group" "instance" {
    name = var.security_group_name

    # Destination:TCP/8080 | Source:Any
    ingress {
        from_port = var.server_port
        to_port = var.server_port
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

# Security group for Application Load Balancer
resource "aws_security_group" "alb" {
    name = var.alb_security_group_name

    # Allow inbound
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    # Allow outbound
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

# Default VPCs
data "aws_vpc" "default" {
    default = true
}

# Default subnet for VPC
data "aws_subnets" "default" {
    filter {
        name   = "vpc-id"
        values = [data.aws_vpc.default.id]
    }
}

2.2. variables.tf

Port 8080 is repeated in both the security group and User Data configuration. The (variables.tf) enables us to make changes to the port easily and quickly and gives us an overall view of configuration parameters that might otherwise be clustered among other resource codes in main.tf

variable "server_port" {
    description = "HTTP request non-port 80"
    type = number
    default = 8080
}

variable "security_group_name" {
    description = "Name of security group"
    type = string
    default = "terraform-example-instance1"
}

variable "alb_name" {
    description = "ALB name"
    type = string
    default = "ALB-up-and-running"
}

variable "alb_security_group_name" {
    description = "ALB security group"
    type = string
    default = "security-group-up-and-running"
}

variable "vpc_id" {
    description = "VPC name"
    type = string
    default = "VPC-up-and-running"
}

3. AWS Console Screenshots after “terraform apply”

resource “aws_launch_configuration”
instance_type = “t2.micro”
resource “aws_autoscaling_group”
resource “aws_lb”
resource “aws_lb_target_group”
Load balancer: DNS name loads “Hello, World” index.html

Conclusion

It has been almost two years since we last checked in at Terraform. Many syntaxes such as resource aws_xxx have since been deprecated, but one can easily google for the up-to-date syntax on the web. IMO, Kubernetes is Terraform’s direct competitor, and there is strong demand for Kubernetes enablers in the IT industry now. Some companies are already starting to migrate from Terraform to Kubernetes. These days, if you are hunting for a DevOps role, 8 out of 10, you will be tested on Iac using Terraform. Therefore, this script that deploys AWS ASG, ALB, Target group, Security groups, VPC, etc., will give you a little head start.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *