How to Launch an Auto Scaling Group with Application Load Balancer using Terraform
Introduction
Infrastructure as code (IaC) has become increasingly popular as a means to manage and provision infrastructure in a more automated and repeatable way. Terraform is one such tool that enables you to define and deploy your infrastructure as code.
In this context, creating an Auto Scaling Group (ASG) and Application Load Balancer (ALB) using Terraform has a number of benefits over manually creating the infrastructure. Terraform allows you to define the desired state of your infrastructure in a declarative way, which means that you simply specify what you want your infrastructure to look like and Terraform takes care of the details of how to make it happen.
One of the key benefits of Terraform is its ability to automate infrastructure provisioning and management. With Terraform, you can create and manage your infrastructure in a repeatable and scalable way. You can define your infrastructure once and then deploy it multiple times across different environments, making it easier to manage and maintain consistency across your infrastructure.
Creating an ASG and ALB using Terraform provides additional benefits such as the ability to manage infrastructure as code, easily version and audit infrastructure changes, and to provision infrastructure in a more efficient and cost-effective manner. Additionally, Terraform provides a consistent and standardized way to manage infrastructure, which makes it easier for teams to collaborate and work together on infrastructure provisioning and management.
Objective
- Create a custom VPC that has 2 public subnets, 2 private subnets, a public route table and private route table, a NAT Gateway in the public subnet, and an Internet Gateway to allow outbound internet traffic.
- Create a security group that allows traffic from the internet and associates it with the Auto Scaling group instances.
- Create a security group for the ALB that allows traffic from the internet and associates it with the ALB.
- Create an Auto-Scaling group that deploys a minimum of 2 and a maximum of 5 EC2 instances.
- Include a script in the user data to launch an Apache web server.
- Verify everything is working by checking the public IP addresses of the two instances and manually terminating one instance to confirm another one spins up to meet the minimum requirement.
- Add an Application Load Balancer (ALB) in front of the Auto Scaling group.
- Launch the ALB in the public subnets and the Auto Scaling group in the private subnets.
- Output the public DNS of the ALB and verify the ability to reach the web server via the URL.
Instruction
All of the following code below can be found on my GitHub page
#main.tf
# Set AWS provider details
provider "aws" {
region = "us-east-1"
}
#AWS Availability Zones
data "aws_availability_zones" "available" {}
# Create a VPC
resource "aws_vpc" "custom_vpc" {
cidr_block = "192.168.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "custom-vpc"
}
}
# Create an Internet Gateway
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.custom_vpc.id
tags = {
Name = "custom-vpc-igw"
}
}
resource "aws_eip" "elastic_ip" {
vpc = true
depends_on = [aws_internet_gateway.gw]
}
#Create nat gateway
resource "aws_nat_gateway" "nat_gw" {
allocation_id = aws_eip.elastic_ip.id
connectivity_type = "public"
subnet_id = aws_subnet.public_subnet_1.id
}
# Create Public Subnets
resource "aws_subnet" "public_subnet_1" {
vpc_id = aws_vpc.custom_vpc.id
cidr_block = "192.168.1.0/24"
map_public_ip_on_launch = true
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "custom-vpc-public-subnet-1"
}
}
resource "aws_subnet" "public_subnet_2" {
vpc_id = aws_vpc.custom_vpc.id
cidr_block = "192.168.2.0/24"
map_public_ip_on_launch = true
availability_zone = data.aws_availability_zones.available.names[1]
tags = {
Name = "custom-vpc-public-subnet-2"
}
}
# Create Private Subnets
resource "aws_subnet" "private_subnet_1" {
vpc_id = aws_vpc.custom_vpc.id
cidr_block = "192.168.3.0/24"
map_public_ip_on_launch = true
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "custom-vpc-private-subnet-1"
}
}
resource "aws_subnet" "private_subnet_2" {
vpc_id = aws_vpc.custom_vpc.id
cidr_block = "192.168.4.0/24"
map_public_ip_on_launch = true
availability_zone = data.aws_availability_zones.available.names[1]
tags = {
Name = "custom-vpc-private-subnet-2"
}
}
# Create a Public Route Table
resource "aws_route_table" "public_route_table" {
vpc_id = aws_vpc.custom_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "custom-vpc-public-route-table"
}
}
# Create a Private Route Table
resource "aws_route_table" "private_route_table" {
vpc_id = aws_vpc.custom_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gw.id
}
tags = {
Name = "private_route_table"
}
}
resource "aws_route_table_association" "public_rt_1" {
subnet_id = aws_subnet.public_subnet_1.id
route_table_id = aws_route_table.public_route_table.id
}
resource "aws_route_table_association" "public_rt_2" {
subnet_id = aws_subnet.public_subnet_2.id
route_table_id = aws_route_table.public_route_table.id
}
resource "aws_route_table_association" "private_rt_1" {
subnet_id = aws_subnet.private_subnet_1.id
route_table_id = aws_route_table.private_route_table.id
}
resource "aws_route_table_association" "private_rt_2" {
subnet_id = aws_subnet.private_subnet_2.id
route_table_id = aws_route_table.private_route_table.id
}
# Security Group Resources
resource "aws_security_group" "alb_security_group" {
description = "ALB Security Group"
vpc_id = aws_vpc.custom_vpc.id
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
#Auto Scaling Group
resource "aws_security_group" "asg_security_group" {
name = "asg-security-group"
vpc_id = aws_vpc.custom_vpc.id
tags = {
Name = "asg security group"
}
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb_security_group.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_autoscaling_group" "asg" {
name = "asg"
vpc_zone_identifier = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id]
min_size = 2
max_size = 5
desired_capacity = 4
health_check_grace_period = 120
health_check_type = "EC2"
target_group_arns = [aws_lb_target_group.target_group.arn]
termination_policies = ["OldestInstance"]
launch_template {
id = aws_launch_template.launch_template.id
version = aws_launch_template.launch_template.latest_version
}
}
#Launch Template
resource "aws_launch_template" "launch_template" {
name = "launch_template"
image_id = "ami-006dcf34c09e50022"
instance_type = "t2.micro"
user_data = base64encode(var.user_data)
network_interfaces {
device_index = 0
security_groups = [aws_security_group.asg_security_group.id]
}
}
#Application Load Balancer
resource "aws_lb" "alb" {
name = "alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_security_group.id]
subnets = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id]
}
resource "aws_lb_target_group" "target_group" {
name = "alb-target-group"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.custom_vpc.id
health_check {
path = "/"
matcher = 200
}
}
resource "aws_lb_listener" "alb_listener" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.arn
}
}
#variable.tf
variable "public_subnet_cidr_blocks" {
description = "CIDR blocks for the public subnets"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnet_cidr_blocks" {
description = "CIDR blocks for the private subnets"
type = list(string)
default = ["10.0.3.0/24", "10.0.4.0/24"]
}
variable "az_names" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
variable "user_data" {
default = <<EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
systemctl status httpd
yum install -y firewalld
systemctl start firewalld
systemctl enable firewalld
systemctl status firewalld
# Allow HTTP traffic through firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --reload
sudo cat > /var/www/html/index.html << EOF
<html>
<head>
<title> Level Up in Tech </title>
</head>
<body>
<p> Auto Scaling Group made using Terraform
</body>
</html>
EOF
}
#provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.55.0"
}
}
required_version = "~> 1.3.0"
}
#output.tf
output "alb_public_url" {
description = "Public URL"
value = aws_lb.alb.dns_name
}
Terraform init
after making your directory or you will receive an error like this:
Terraform init
Terraform validate
to verify if there are any syntax errors
Terraform fmt
or format to make the line of code easier to read. It corrects spacing issues.
Terraform apply -auto-approve
to create our infrastructure
The URL for our ALB is produced from the output requested
An error occurred while verifying the URL
The issue was the security group of the auto-scaling group was set to default and not the one created.
You can also check it by using curl in the command line
aws autoscaling describe-auto-scaling-groups
this show us the list of EC2 instances created from the auto-scaling group
"AutoScalingGroups": [
{
"AutoScalingGroupName": "asg",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:097550552923:autoScalingGroup:f60d4f51-c459-4929-b716-25dfec6a8c22:autoScalingGroupName/asg",
"LaunchTemplate": {
"LaunchTemplateId": "lt-07ba624d88ae90520",
"LaunchTemplateName": "launch_template",
"Version": "1"
},
"MinSize": 2,
"MaxSize": 5,
"DesiredCapacity": 4,
"DefaultCooldown": 300,
"AvailabilityZones": [
"us-east-1a",
"us-east-1b"
],
"LoadBalancerNames": [],
"TargetGroupARNs": [
"arn:aws:elasticloadbalancing:us-east-1:097550552923:targetgroup/alb-target-group/b33053db8940fff0"
],
"HealthCheckType": "EC2",
"HealthCheckGracePeriod": 120,
"Instances": [
{
"InstanceId": "i-0529e4d9c1e4230e2",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-07ba624d88ae90520",
"LaunchTemplateName": "launch_template",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-05f5aaa13af237674",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-07ba624d88ae90520",
"LaunchTemplateName": "launch_template",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-06e47759f98dd83ff",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-07ba624d88ae90520",
"LaunchTemplateName": "launch_template",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0bf1734e674c80051",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-07ba624d88ae90520",
"LaunchTemplateName": "launch_template",
"Version": "1"
},
"ProtectedFromScaleIn": false
}
],
"CreatedTime": "2023-03-11T03:58:29.097Z",
"SuspendedProcesses": [],
"VPCZoneIdentifier": "subnet-0edfd18317f85e6dd,subnet-080923458f91e5dd8",
"EnabledMetrics": [],
"Tags": [],
"TerminationPolicies": [
"OldestInstance"
],
"NewInstancesProtectedFromScaleIn": false,
"ServiceLinkedRoleARN": "arn:aws:iam::097550552923:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"
}
]
}
Terraform destroy
to destroy all the infrastructure you created