Terraform - Otomatisasi setup EC2, Application Load Balancer, dan Auto Scaling

Pada tutorial kali ini, kita akan belajar implementasi Terraform untuk mengotomatisasi setup EC2 instance pada environment Auto Scaling dengan Application Load Balancer sebagai gateway aksesnya. Kita akan deploy sebuah aplikasi web sederhana pada setiap instance yang up.

Karena kita akan menggunakan fitur auto-scaling, maka proses deploy aplikasi harus dilakukan secara otomatis (tidak bisa deploy secara manual).

Aplikasi web yang kita akan pakai adalah aplikasi hello world sederhana yang siap pakai. Aplikasi tersebut ada di Github, nantinya kita akan clone.


1. Kebutuhan

1.1. Terraform CLI

Pastikan Terraform CLI tool tersedia. Jika belum, maka install terlebih dahulu.

1.2. User IAM AWS

Siapkan satu buah user IAM baru dengan programmatic access key aktif dan akses penuh ke EC2 management. Kita akan gunakan access_key dan secret_key-nya pada tutorial ini. Bisa ikuti guide berikut untuk cara buat user IAM baru: Membuat User IAM.

1.3. Command ssh-keygen dan ssh

Pastikan CLI tools ssh-keygen dan ssh tersedia.


2. Persiapan

Buat folder baru (dengan nama bebas), isinya satu buah file bernama infrastructure.tf. Kita akan gunakan file ini untuk pendefinisian kode infrastruktur. Semua kode setup resource akan dituliskan dalam bahasa HCL dalam file tersebut, meliputi:

  • Upload key pair (untuk keperluan SSH akses dari lokal ke EC2 instance).
  • Subnetting pada dua availability zones berbeda (tapi masih dalam satu region).
  • Setup Application Load Balancer, listener-nya, security group, dan juga target group.
  • Setup Auto-scaling dan launch config-nya.

Ok, mari kita mulai tutorialnya. Pertma siapkan folder dan file yang sudah disinggung di atas.

mkdir terraform-automate-aws-ec2-instance
cd terraform-automate-aws-ec2-instance
touch infrastructure.tf

Selanjutnya, buat public-key cryptography menggunakan CLI tool ssh-keygen. Dengan ini akan di-generate sebuah file public key id_rsa.pub dan private key id_rsa. Nantinya kita akan upload key public key-nya ke AWS dan menggunakan private key-nya untuk mengakses EC2 instance via ssh.

cd terraform-automate-aws-ec2-instance
ssh-keygen -t rsa -f ./id_rsa

Terraform - Otomatisasi setup EC2, Application Load Balancer, dan Auto Scaling - generate key pair


3. Kode Infrastruktur

Sekarang, mari kita mulai penulisan kode infrastruktur. Silakan buka file infrastructure.tf menggunakan editor apa saja bebas.

3.1. Set AWS sebagai provider

Definisikan blok kode provider, disini kita akan gunakan AWS sebagai cloud provider. Dalam blok kode provider, tulis informasi akses AWS seperti region, access_key, dan secret_key. Untuk keys nilainya kita isi menggunakan keys dari user IAM yang sudah dibuat (jadi silakan sesuaikan value-nya).

Ok, berikut adalah blok kode provider, silakan tulis pada file infrastructure.tf.

provider "aws" {
    region = "ap-southeast-1"
    access_key = "AKIAWLTS5CSXP7E3YLWG"
    secret_key = "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"
}

3.2. Generate key pair baru, lalu upload ke AWS

Definisikan blok kode resource aws_key_pair, namai blok tersebut dengan my_instance_key_pair, lalu tambahkan file public key yang sebelumnya sudah di-generate id_rsa.pub ke dalam blok kode ini.

resource "aws_key_pair" "my_instance_key_pair" {
    key_name = "terraform_learning_key_1"
    public_key = file("id_rsa.pub")
}

3.3. Book a VPC, and enable internet gateway on it

Book a VPC, name it my_vpc. Then enable internet gateway on it. Each part of the code below is self-explanatory.

# allocate a vpc named my_vpc.
resource "aws_vpc" "my_vpc" {
    cidr_block = "10.0.0.0/16"
    enable_dns_hostnames = true
}

# setup internet gateway for my_vpc.
resource "aws_internet_gateway" "my_vpc_igw" {
    vpc_id = aws_vpc.my_vpc.id
}

# attach the internet gateway my_vpc_igw into my_vpc.
resource "aws_route_table" "my_public_route_table" {
    vpc_id = aws_vpc.my_vpc.id
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = aws_internet_gateway.my_vpc_igw.id
    }
}

3.4. Allocate two different subnets on two different availability zones (within the same region)

Application Load Balancer or ALB requires two subnets setup on two availability zones (within the same region).

In this example, the region we used is ap-southeast-1, as defined in the provider block above (see 3.1). There are two zones available within this region, ap-southeast-1a and ap-southeast-1b. The ALB (not classic network load balancer) requires at least to be enabled on two different zones, so we will use those two.

# prepare a subnet for availability zone ap-southeast-1a.
resource "aws_subnet" "my_subnet_public_southeast_1a" {
    vpc_id = aws_vpc.my_vpc.id
    cidr_block = "10.0.0.0/24"
    availability_zone = "ap-southeast-1a"
}
# associate the internet gateway into newly created subnet for ap-southeast-1a
resource "aws_route_table_association" "my_public_route_association_for_southeast_1a" {
    subnet_id = aws_subnet.my_subnet_public_southeast_1a.id
    route_table_id = aws_route_table.my_public_route_table.id
}

# prepare a subnet for availability zone ap-southeast-1b
resource "aws_subnet" "my_subnet_public_southeast_1b" {
    vpc_id = aws_vpc.my_vpc.id
    cidr_block = "10.0.1.0/24"
    availability_zone = "ap-southeast-1b"
}
# associate the internet gateway into newly created subnet for ap-southeast-1b
resource "aws_route_table_association" "my_public_route_association_for_southeast_1b" {
    subnet_id = aws_subnet.my_subnet_public_southeast_1b.id
    route_table_id = aws_route_table.my_public_route_table.id
}

The internet gateway associated with two zones that we just created. In this example, it is required for the application hosted within instances on these zones to be able to connect to the internet.

3.5. Define ALB resource block, listener, security group, and target group

The ALB will be created with two subnets attached (subnets from ap-southeast-1a and ap-southeast-1b).

# create an Application Load Balancer.
# attach the previous availability zones' subnets into this load balancer.
resource "aws_lb" "my_alb" {
    name = "my-alb"
    internal = false # set lb for public access
    load_balancer_type = "application" # use Application Load Balancer
    security_groups = [aws_security_group.my_alb_security_group.id]
    subnets = [ # attach the availability zones' subnets.
        aws_subnet.my_subnet_public_southeast_1a.id,
        aws_subnet.my_subnet_public_southeast_1b.id 
    ]
}

The security group for our load balancer has only two rules.

  • Allow only incoming TCP/HTTP request on port 80.
  • Allow every kind of outgoing request.
# prepare a security group for our load balancer my_alb.
resource "aws_security_group" "my_alb_security_group" {
    vpc_id = aws_vpc.my_vpc.id
    ingress {
        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"]
    }
}

Next, we shall prepare the ALB listener. The load balancer will listen for every incoming request to port 80, and then the particular request will be directed towards port 8080 on the instance.

Port 8080 is chosen here because the application (that will be deployed later) will listen to this port.

# create an alb listener for my_alb.
# forward rule: only accept incoming HTTP request on port 80,
# then it'll be forwarded to port target:8080.
resource "aws_lb_listener" "my_alb_listener" {  
    load_balancer_arn = aws_lb.my_alb.arn
    port = 80  
    protocol = "HTTP"
    default_action {    
        target_group_arn = aws_lb_target_group.my_alb_target_group.arn
        type = "forward"  
    }
}

# my_alb will forward the request to a particular app,
# that listen on 8080 within instances on my_vpc.
resource "aws_lb_target_group" "my_alb_target_group" {
    port = 8080
    protocol = "HTTP"
    vpc_id = aws_vpc.my_vpc.id
}

3.6. Define launch config (and it's required dependencies) for auto-scaling

We are not going to simply create an instance then deploy the application into it. Instead, the instance creation and app deployment will be automated using AWS auto-scaling feature.

In the resource block below, we will set up the launch configuration for the auto-scaling. This launch config is the one that decides how the instance will be created.

  • The Amazon Linux 2 AMI t2.micro is used here.
  • The launched instance will have a public IP attached, this is better to be set to false, but in here we might need it for testing purposes.
  • The previously allocated key pair will also be used on the instance, to make it accessible through SSH access. This part is also for testing purposes.

Other than that, there is one point left that is very important, the user_data. The user data is a block of bash script that will be executed during instance bootstrap. We will use this to automate the deployment of our application. The whole script is stored in a file named deployment.sh, we will prepare it later.

# setup launch configuration for the auto-scaling.
resource "aws_launch_configuration" "my_launch_configuration" {

    # Amazon Linux 2 AMI (HVM), SSD Volume Type (ami-0f02b24005e4aec36).
    image_id = "ami-0f02b24005e4aec36"

    instance_type = "t2.micro"
    key_name = aws_key_pair.my_instance_key_pair.key_name # terraform_learning_key_2
    security_groups = [aws_security_group.my_launch_config_security_group.id]

    # set to false on prod stage.
    # otherwise true, because ssh access might be needed to the instance.
    associate_public_ip_address = true
    lifecycle {
        # ensure the new instance is only created before the other one is destroyed.
        create_before_destroy = true
    }

    # execute bash scripts inside deployment.sh on instance's bootstrap.
    # what the bash scripts going to do in summary:
    # fetch a hello world app from Github repo, then deploy it in the instance.
    user_data = file("deployment.sh")
}

Below is the launch config security group. In this block, we define the security group specifically for the instances that will be created by the auto scale launch config. Three rules defined here:

  • Allow incoming TCP/SSH access on port 22.
  • Allow TCP/HTTP access on port 8080.
  • Allow every kind of outgoing requests.
# security group for launch config my_launch_configuration.
resource "aws_security_group" "my_launch_config_security_group" {
    vpc_id = aws_vpc.my_vpc.id
    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    ingress {
        from_port = 8080
        to_port = 8080
        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"]
    }
}

Ok, the autoscale launch config is ready, now we shall attach it into our ALB.

# create an autoscaling then attach it into my_alb_target_group.
resource "aws_autoscaling_attachment" "my_aws_autoscaling_attachment" {
    alb_target_group_arn = aws_lb_target_group.my_alb_target_group.arn
    autoscaling_group_name = aws_autoscaling_group.my_autoscaling_group.id
}

Next, we shall prepare the auto-scaling group config. This resource is used to determine when or on what condition the scaling process run.

  • As per the below config, the auto-scaling will have a minimum of 2 instances alive, and 5 max.
  • The ELB health check is enabled.
  • The previous two subnets on ap-southeast-1a and ap-southeast-1b are applied.
# define the autoscaling group.
# attach my_launch_configuration into this newly created autoscaling group below.
resource "aws_autoscaling_group" "my_autoscaling_group" {
    name = "my-autoscaling-group"
    desired_capacity = 2 # ideal number of instance alive
    min_size = 2 # min number of instance alive
    max_size = 5 # max number of instance alive
    health_check_type = "ELB"

    # allows deleting the autoscaling group without waiting
    # for all instances in the pool to terminate
    force_delete = true

    launch_configuration = aws_launch_configuration.my_launch_configuration.id
    vpc_zone_identifier = [
        aws_subnet.my_subnet_public_southeast_1a.id,
        aws_subnet.my_subnet_public_southeast_1b.id 
    ]
    timeouts {
        delete = "15m" # timeout duration for instances
    }
    lifecycle {
        # ensure the new instance is only created before the other one is destroyed.
        create_before_destroy = true
    }
}

3.7. Print the ALB public DNS

Everything is pretty much done, except we need to print the ALB public DNS, so then we can do the testing.

# print load balancer's DNS, test it using curl.
#
# curl my-alb-625362998.ap-southeast-1.elb.amazonaws.com
output "alb-url" {
    value = aws_lb.my_alb.dns_name
}

4. App Deployment Script

We have done with the infrastructure code, next prepare the deployment script.

Create a file named deployment.sh in the same directory where the infra code is placed. It will contain bash scripts for automating app deployment. This file will be used by auto-scaling launcher to automate app setup during instance bootstrap.

The application is written in Go, and the AMI Amazon Linux 2 AMI t2.micro that used here does not have any Go tools ready, that's why we need to set it up.

Deploying app means that the app is ready (has been built into binary), so what we need is simply just run the binary.

However to make our learning process better, in this example, we are going to fetch the app source code and perform the build and deploy processes within the instance.

Ok, here we go, the bash script.

#!/bin/bash

# install git
sudo yum -y install git

# download go, then install it
wget https://dl.google.com/go/go1.14.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.14.linux-amd64.tar.gz

# clone the hello world app.
# The app is hosted on private repo,
# that's why the github token is used on cloning the repo
github_token=30542dd8874ba3745c55203a091c345340c18b7a
git clone https://$github_token:x-oauth-basic@github.com/novalagung/hello-world.git \
    && echo "cloned" \
    || echo "clone failed"

# export certain variables required by go
export GO111MODULE=on
export GOROOT=/usr/local/go
export GOCACHE=~/gocache
mkdir -p $GOCACHE
export GOPATH=~/goapp
mkdir -p $GOPATH

# create local vars specifically for the app
export PORT=8080
export INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`

# build the app
cd hello-world
/usr/local/go/bin/go env
/usr/local/go/bin/go mod tidy
/usr/local/go/bin/go build -o binary

# run the app with nohup
nohup ./binary &

5. Run Terraform

5.1. Terraform initialization

First, run the terraform init command. This command will do some setup/initialization, certain dependencies (like AWS provider that we used) will be downloaded.

cd terraform-automate-aws-ec2-instance
terraform init

5.2. Terraform plan

Next, run terraform plan, to see the plan of our infrastructure. This step is optional, however, might be useful for us to see the outcome from the infra file.

5.3. Terraform apply

Last, run the terraform apply command to execute the infrastructure plan.

cd terraform-automate-aws-ec2-instance
terraform apply -auto-approve

The -auto-approve flag is optional, it will skip the confirmation prompt during execution.

After the process is done, public DNS shall appear. Next, we shall test the instance.


6. Test Instance

Use the curl command to make an HTTP request to the ALB public DNS instance.

curl -X GET my-alb-613171058.ap-southeast-1.elb.amazonaws.com

Terraform - Otomatisasi setup EC2, Application Load Balancer, dan Auto Scaling - curl to load balancer

We can see from the image above, the HTTP response is different from one another across those multiple curl commands. The load balancer manages the traffic, sometimes we will get the instance A, B, etc.

In the AWS console, the instances that up and running are visible.

Terraform - Otomatisasi setup EC2, Application Load Balancer, dan Auto Scaling - aws console

results matching ""

    No results matching ""