AWS Load Balancer and ASG with Terraform and GitHub Actions

I am a Salafiyyah who has a passion for creative and technological innovations. Currently, I am focused on DevOps and project management. With my skills and experience, I am committed to delivering quality results in every project that I work on.
Introduction
In today's world, where technology has become an essential aspect of our daily lives, it is important to have a reliable and scalable infrastructure to support your applications. The ability to quickly and easily provision and manage infrastructure is critical to the success of any modern application. In this tutorial, we will explore how to provision an AWS Load balancer and Auto Scaling Group (ASG) using Terraform and deploy it using GitHub Actions (GHA).

Prerequisites
The prerequisites for the article are:
A basic understanding of AWS services such as Elastic Load Balancer (ELB), Auto Scaling Groups (ASG), and S3 buckets.
A basic understanding of Terraform and its syntax.
An AWS account with permission to create resources.
A GitHub account and a repository where the Terraform code will be stored.
Familiarity with GitHub Actions and how to set up workflows.
Basic knowledge of YAML syntax for defining workflows.
Key Terms
Here are some definitions of key terms used in the article for absolute beginners:
Terraform - An open-source infrastructure as code (IaC) tool used for building, changing, and versioning infrastructure.
AWS (Amazon Web Services) - A comprehensive, evolving cloud computing platform provided by Amazon that includes a mix of infrastructure as a service (IaaS), platform as a service (PaaS), and software as a service (SaaS) offerings.
Load balancer - A device or software that acts as a reverse proxy and distributes network or application traffic across multiple servers or instances.
ASG (Auto Scaling Group) - A group of instances that are created from a common Amazon Machine Image (AMI) and are automatically scaled based on the defined policies.
GHA (GitHub Actions) - A tool provided by GitHub to automate tasks within the software development workflow.
IaC (Infrastructure as Code) - A practice of managing and provisioning data center infrastructure using machine-readable definition files, rather than physical hardware configuration or interactive configuration tools.
AWS CLI (Command Line Interface) - A unified tool provided by AWS to manage AWS services from the command line.
IAM (Identity and Access Management) - A web service that helps securely control access to AWS resources.
S3 (Simple Storage Service) - An object storage service offered by AWS that provides industry-leading scalability, data availability, security, and performance.
EC2 (Elastic Compute Cloud) - A web service offered by AWS that provides resizable compute capacity in the cloud.
GitHub - A web-based version control system used for software development and collaboration.
Workflow - A set of automated actions that run on GitHub when specific events occur, such as a pull request or a code push.
Provisioning AWS Load balancer and ASG with Terraform
Terraform is an open-source infrastructure as a code tool that enables you to write, plan, and create infrastructure as code. Terraform can provision resources across various cloud providers, including Amazon Web Services (AWS). With Terraform, you can manage your infrastructure in a declarative way, where you define the desired state of your infrastructure and let Terraform handle the rest.
Step 1: Create a new directory and initialize Terraform
Create a new project directory with the name terraform-lb-asg . Then create two other folders for the GHA (.github )and Infrastructure (infra )respectively inside terraform-lb-asg. Change the directory to infra and touch five files inside the directory; main.tf, and provider.tf , user-data.tpl, output.tf by running the following commands:
$ mkdir terraform-lb-asg
$ cd terraform-load-balancer
$ mkdir .github
$ mkdir infra
$ cd infra
$ touch main.tf
$ touch provider.tf
$ touch user-data.tpl
$ touch output.tf
Step 2: Create the Terraform configuration file
Next, we will create a Terraform configuration file to provision the ALB and ASG. Open the file main.tf and paste the following code into it:
# Data source declaration for all necessary fetch
data "aws_vpc" "default_vpc" {
default = true
}
data "aws_subnets" "subnets" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default_vpc.id]
}
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
owners = ["099720109477"] # Canonical
}
data "template_file" "nginx_data_script" {
template = file("./user-data.tpl")
vars = {
server = "nginx"
}
}
# General Security group declaration
resource "aws_security_group" "terraform-sg" {
egress = [{
cidr_blocks = ["0.0.0.0/0"]
description = ""
from_port = 0
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "-1"
security_groups = []
self = false
to_port = 0
}]
ingress = [{
cidr_blocks = ["0.0.0.0/0"]
description = "allow ssh"
from_port = 22
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "tcp"
security_groups = []
self = false
to_port = 22
},
{
cidr_blocks = ["0.0.0.0/0"]
description = "allow http"
from_port = 80
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "tcp"
security_groups = []
self = false
to_port = 80
}]
}
# Provision the ec2 instance for APACHE
resource "aws_instance" "apache-server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
key_name = "<key_pair_name>"
vpc_security_group_ids = [aws_security_group.general-sg.id]
user_data = base64encode(data.template_file.apache_data_script.rendered)
tags = {
"Name" = "apache-server"
}
}
# Load balancer, Target Group and ASG Declaration
# Load Balancers and component declaration
resource "aws_lb_target_group" "terraform-tg" {
name = "terraform-tg"
port = 80
protocol = "HTTP"
target_type = "instance"
vpc_id = data.aws_vpc.default_vpc.id
health_check {
path = "/"
port = "traffic-port"
protocol = "HTTP"
matcher = "200-399"
interval = 10
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
enabled = true
}
}
resource "aws_lb" "terraform-lb" {
name = "terraform-lb"
ip_address_type = "ipv4"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.general-sg.id]
subnets = data.aws_subnets.subnets.ids
}
resource "aws_lb_listener" "terraform-lbl" {
load_balancer_arn = aws_lb.terraform-lb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.terraform-tg.arn
}
}
resource "aws_lb_target_group_attachment" "apache-server" {
target_group_arn = aws_lb_target_group.terraform-tg.arn
target_id = aws_instance.apache-server.id
port = 80
}
# ASG and component declaretion
resource "aws_launch_template" "nginx-lt" {
name = "nginx-lt"
image_id = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
key_name = "<key_pair_name>"
vpc_security_group_ids = [aws_security_group.general-sg.id]
user_data = base64encode(data.template_file.nginx_data_script.rendered)
tag_specifications {
resource_type = "instance"
tags = {
Name : "nginx-lt"
}
}
}
resource "aws_autoscaling_group" "terraform-asg" {
name = "terraform-asg"
vpc_zone_identifier = aws_lb.terraform-lb.subnets
max_size = 10
min_size = 2
desired_capacity = 2
health_check_grace_period = 300
health_check_type = "ELB"
target_group_arns = [aws_lb_target_group.terraform-tg.arn]
launch_template {
id = aws_launch_template.nginx-lt.id
version = "$Latest"
}
}
Now, let's move on to the explanations of the code:
- Data Sources: The code starts with three AWS data sources:
aws_vpc.default_vpc: This data source retrieves the default VPC (Virtual Private Cloud) for the AWS account.aws_subnets.subnets: This data source retrieves a list of subnets for the VPC identified by theaws_vpc.default_vpcdata source.aws_ami.ubuntu: This data source retrieves the latest Ubuntu Server AMI (Amazon Machine Image) from the AWS Marketplace.
General Security Group Declaration: The next resource is an AWS security group declaration named
aws_security_group.terraform-sg. It defines two ingress rules: one for SSH traffic on port 22 and one for HTTP traffic on port 80. It also defines an egress rule that allows all outbound traffic.Provisioning EC2 instance for Apache: The next resource is an AWS EC2 instance declaration named
aws_instance.apache-server. It uses the latest Ubuntu Server AMI retrieved by theaws_ami.ubuntudata source and an instance type oft2.micro. The instance is assigned a key pair for SSH access and a security group that allows inbound SSH and HTTP traffic. Theuser_dataparameter contains the base64-encoded contents of theuser-data.tplfile, which is a template for a script that installs Apache web server on the instance.Load Balancer, Target Group, and ASG Declaration: The next set of resources declare an AWS Application Load Balancer, Target Group, and Auto Scaling Group.
aws_lb_target_group.terraform-tg: This resource defines an AWS Application Load Balancer target group namedterraform-tg. It listens on port 80 for HTTP traffic and forwards it to instances registered with the target group.aws_lb.terraform-lb: This resource defines an AWS Application Load Balancer namedterraform-lb. It is configured to listen on port 80 for HTTP traffic and forward it to instances registered with theterraform-tgtarget group. The load balancer is assigned a security group that allows inbound HTTP traffic and is associated with the subnets retrieved by theaws_subnets.subnetsdata source.aws_lb_listener.terraform-lbl: This resource defines an AWS Application Load Balancer listener namedterraform-lbl. It listens on port 80 for HTTP traffic and forwards it to theterraform-tgtarget group.aws_lb_target_group_attachment.apache-server: This resource attaches theapache-serverinstance to theterraform-tgtarget group.aws_launch_template.nginx-lt: This resource defines an AWS Launch Template namednginx-lt. It is used to launch instances for theterraform-asgAuto Scaling Group. It uses the latest Ubuntu Server AMI retrieved by theaws_ami.ubuntudata source, an instance type oft2.micro, a key pair for SSH access, and a security group that allows inbound SSH and HTTP traffic. Theuser_dataparameter contains the base64-encoded contents of theuser-data.tplfile, which is a template for a script that installs Nginx web server on the instance.resource "aws_autoscaling_group" "terraform-asg": This resource block is used to create an AWS Auto Scaling Group named"terraform-asg". Auto Scaling Groups are used to automatically launch and terminate EC2 instances based on demand. This block includes several attributes such as name,vpc_zone_identifier,max_size,min_size,desired_capacity,health_check_grace_period,health_check_type,target_group_arns, andlaunch_template.
This Terraform code declares AWS infrastructure resources for an autoscaling group behind a load balancer. The code provisions a virtual private cloud (VPC), subnets, Amazon Machine Images (AMIs) for Ubuntu, and security groups. The code then defines a launch template and an autoscaling group with scaling policies, minimum and maximum sizes, and desired capacity. The code also defines a load balancer, a target group, and a listener to distribute traffic to instances in the autoscaling group. Finally, the code deploys NGINX on the instances in the launch template, using a different instance launch script.
Next, paste the following code in the provider.tf file:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>4.0"
}
}
backend "s3" {
bucket = "<your_buck_name>"
key = "aws/terraform1/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = "us-east-1"
}
This is a Terraform configuration file that uses the AWS provider to manage AWS resources and also configures the Terraform backend to store the state file in an S3 bucket. Here's what each part does:
terraform { ... }: This block is used to configure Terraform itself and specifies the required providers and backend.required_providers { ... }: This block is used to specify the provider(s) that this configuration requires. In this case, it specifies the"aws"provider with a version constraint of"~>4.0",which means any version in the 4.x series should work.backend "s3" { ... }: This block configures the Terraform backend to useS3to store the state file. Thebucketparameter specifies the name of an already created bucket in"aws"while thekeyparameter specifies the S3 object key where the state file will be stored, and theregionparameter specifies the AWS region where the S3 bucket is located.provider "aws" { ... }: This block configures the AWS provider with the region where the AWS resources will be created and managed.
Next, paste the following code in the output.tf file:
output "load_balancer_dns_name" {
value = aws_lb.lb-asg-lb.dns_name
}
This code defines an output named "load_balancer_dns_name" that will return the DNS name of an AWS load balancer resource named "terraform-lb".
Next, paste the following code in the user-data.tpl file:
#!/bin/bash
sudo apt-get update
sudo apt-get install ${server} -y
sudo systemctl start ${server}
sudo systemctl enable ${server}
Using GHA to deploy the infrastructure
GitHub Actions (GHA) is a popular CI/CD tool that allows you to automate your workflow. In this tutorial, we will look at how to use GHA to deploy infrastructure created with Terraform. We will set up four jobs to lint, initialize & plan, apply, and destroy infrastructure on different triggers.
Now navigate to the .github directory we created earlier. Create a subdirectory named workflows and create a deploy.yml file inside the workflows. You can follow the below step if you're currently in the infra directory.
$ cd ../.github
$ mkdir workflows && cd workflows && touch deploy.yml
Next, we will create a GHA workflow. Open the file deploy.yml and paste the following code into it:
name: Deploy Terraform ALB-ASG Infra
on:
push:
branches:
- feature-a
pull_request:
types: [closed]
branches:
- main
- feature-b
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.9
- name: Format Terraform code
run: terraform fmt -check -diff -recursive
working-directory: ./infra
plan:
needs: lint
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/feature-a'
steps:
- name: Code Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
- name: Terraform init
run: terraform init
working-directory: ./infra
- name: Terraform plan
working-directory: ./infra
run: |
terraform plan \
-out=tf.plan
apply:
needs: lint
runs-on: ubuntu-latest
if: |
github.ref == 'main' &&
github.event.pull_request.merged == true
steps:
- name: Code Checkout
uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
- name: Terraform init
run: terraform init
working-directory: ./infra
- name: Terraform Apply
working-directory: ./infra
run: |
terraform apply \
-auto-approve \
destroy:
runs-on: ubuntu-latest
if: |
github.ref == 'feature-b' &&
github.event.pull_request.merged == true
steps:
- name: Code Checkout
uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
- name: Terraform init
run: terraform init
working-directory: ./infra
- name: Terraform Destroy
working-directory: ./infra
run: terraform destroy -auto-approve
Now, let's move on to the explanations of the code:
The values for these environment variables are stored in GitHub Secrets, which are encrypted and can only be accessed by authorized users or workflows. You can set the values for these secrets in the repository's Settings tab, under Secrets. Take the image below as a guide:

The pipeline is triggered on push events to the "feature-a" branch and closed pull requests targeting the "main" and "feature-b" branches. The pipeline also defines environment variables for AWS access credentials and the AWS region.
The pipeline is composed of three jobs: "lint", "plan", and "apply". Each job runs on an Ubuntu operating system and has a different set of steps.
The "lint" job runs the "terraform fmt" command to check and format the Terraform code in the "./infra" directory.
The "plan" job runs the "terraform plan" command to generate an execution plan for creating the infrastructure defined in the Terraform code. It creates a Terraform plan file that can be used later by the "apply" job. The job runs only if the pipeline is triggered by a push event to the "feature-a" branch. It checks out the code from the pull request head and sets up Terraform using version 1.0.9.
The "apply" job runs the "terraform apply" command to create or update the infrastructure defined in the Terraform code. The job runs only if the pipeline is triggered by a closed pull request targeting the "main" branch and the pull request was merged.
The "destroy" job runs the "terraform destroy" command to destroy the infrastructure created by the Terraform code. The job runs only if the pipeline is triggered by a closed pull request targeting the "feature-b" branch and the pull request was merged. It generates a Terraform plan and destroys the infrastructure defined in the Terraform code.
Conclusion
In conclusion, deploying infrastructure with Terraform and GitHub Actions is an efficient way of ensuring continuous integration and delivery. In this blog post, we have learned how to provision an AWS load balancer and ASG with Terraform and use GitHub Actions to deploy the infrastructure. We've seen how to lint the Terraform code, initiate Terraform, plan the infrastructure when the job is triggered with a push to the feature-a branch, and apply the changes when the job is triggered with a closed and merged pull request to the main branch. Additionally, we've demonstrated how to destroy the infrastructure when the job is triggered with a closed and merged pull request to the feature-b branch.
By implementing this process, developers can automate the infrastructure deployment process, which can save time and reduce the chance of errors. It ensures that the code deployed is consistent, and infrastructure is correctly configured across different environments. By utilizing GitHub Actions, developers can enjoy a seamless integration process while maintaining a robust infrastructure deployment.


