ECS (Fargate) with ALB Deployment Using Terraform — Part 3

Uzairyuseph
The Cloud Journal
Published in
8 min readOct 10, 2023

This article is a continuation of a larger demonstration on how to create a robust pattern consisting of a highly secured ECS container running a Flask application with connections to a DynamoDB database.

In Part 1 of this series, we delved deeply into the architecture of this project, outlining the strategy to ensure high availability and security of our application.

Architecture diagram

In Part 2 of this series, we focused the terraform implementation of the core services of this application: ECS container, ECR repo and the DynamoDB database.

In this final Part 3, we will continue with the terraform implementation of the supporting resources as well as focus on the deployment of the application.

Step 3.4: VPC

The main.tf file is where the VPC, subnets, route tables and more networking resources are defined. This sets the foundation on which we can now add the other required resources.

main.tf file

main.tf

###################################################################################
# This file describes the vpc, internet gateway, nat gateway and route tables
###################################################################################

resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
}

resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
}

resource "aws_nat_gateway" "ngw" {
allocation_id = aws_eip.nateip.id
subnet_id = aws_subnet.public_subnet_1.id
depends_on = [ aws_internet_gateway.igw ]
}

resource "aws_eip" "nateip" {
domain = "vpc"
}

resource "aws_subnet" "private_subnet_1" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.private_subnet_1
availability_zone = var.availibilty_zone_1
}

resource "aws_subnet" "private_subnet_2" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.private_subnet_2
availability_zone = var.availibilty_zone_2
}

resource "aws_subnet" "public_subnet_1" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.public_subnet_1
availability_zone = var.availibilty_zone_1
map_public_ip_on_launch = true
}

resource "aws_subnet" "public_subnet_2" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.public_subnet_2
availability_zone = var.availibilty_zone_2
map_public_ip_on_launch = true
}

resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id

}

resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}

resource "aws_route_table_association" "public1" {
subnet_id = aws_subnet.public_subnet_1.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public2" {
subnet_id = aws_subnet.public_subnet_2.id
route_table_id = aws_route_table.public.id
}



resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
}

resource "aws_route" "private" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.ngw.id
}

resource "aws_route_table_association" "private1" {
subnet_id = aws_subnet.private_subnet_1.id
route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private2" {
subnet_id = aws_subnet.private_subnet_2.id
route_table_id = aws_route_table.private.id
}

Step 3.5 : ALB

There are three parts needed to configure the ALB.

  1. First is the actual Load Balancer. As seen in the configuration below, the ALB is an external Load balancer and also has a security group attached, this will be explained later.
  2. Next is the target group. This the group of resources that will receive traffic from the ALB. A health check is also included as a measure of availability and reliability of the target groups.
  3. Finally we have the listener. This resource specifies the protocol and port the ALB should use to process and deliver traffic. In this project only HTTP will be used. It is recommended to include HTTPS for secure traffic.
alb.tf file

alb.tf

#################################################################################################
# This file describes the Load Balancer resources: ALB, ALB target group, ALB listener
#################################################################################################

#Defining the Application Load Balancer
resource "aws_alb" "application_load_balancer" {
name = "test-alb"
internal = false
load_balancer_type = "application"
subnets = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id]
security_groups = [aws_security_group.alb_sg.id]
}

#Defining the target group and a health check on the application
resource "aws_lb_target_group" "target_group" {
name = "test-tg"
port = var.container_port
protocol = "HTTP"
target_type = "ip"
vpc_id = aws_vpc.vpc.id
health_check {
path = "/health"
protocol = "HTTP"
matcher = "200"
port = "traffic-port"
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 10
interval = 30
}
}

#Defines an HTTP Listener for the ALB
resource "aws_lb_listener" "listener" {
load_balancer_arn = aws_alb.application_load_balancer.arn
port = "80"
protocol = "HTTP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.arn
}
}

Step 3.6 : IAM

IAM (Identity and Access Management) roles are used to give resources and services permissions and restrict what can be done by using the principle of least privilege.

iam.tf file

iam.tf

#################################################################################################
# This file describes the IAM resources: ECS task role, ECS execution role
#################################################################################################

resource "aws_iam_role" "ecsTaskExecutionRole" {
name = "test-app-ecsTaskExecutionRole"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

data "aws_iam_policy_document" "assume_role_policy" {
statement {
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}

resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" {
role = aws_iam_role.ecsTaskExecutionRole.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_iam_role" "ecsTaskRole" {
name = "ecsTaskRole"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecsTaskRole_policy" {
role = aws_iam_role.ecsTaskRole.name
policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
}

As seen in the ecs section, two roles are defined in the ecs.tf file, the execution_role and the task_role. The execution role refers to the role required for the underlying infrastructure on which the containers run. This role has the permissions of ‘AmazonECSTaskExecutionRolePolicy’.

The task role refers to permissions required for the actual application. Since the application will require connectivity to DynamoDB, an ‘AmazonDynamoDBFullAccess’ policy is attached to the task role.

Step 3.7 : Security Groups

Security groups in AWS act as virtual firewalls for services. Inbound and outbound traffic can be controlled using security groups.

Two security groups are created in the project, one for the ALB and the other for the ECS service.

securitygroup.tf

# ------------------------------------------------------------------------------
# Security Group for ECS app
# ------------------------------------------------------------------------------
resource "aws_security_group" "ecs_sg" {
vpc_id = aws_vpc.vpc.id
name = "demo-sg-ecs"
description = "Security group for ecs app"
revoke_rules_on_delete = true
}
# ------------------------------------------------------------------------------
# ECS app Security Group Rules - INBOUND
# ------------------------------------------------------------------------------
resource "aws_security_group_rule" "ecs_alb_ingress" {
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
description = "Allow inbound traffic from ALB"
security_group_id = aws_security_group.ecs_sg.id
source_security_group_id = aws_security_group.alb_sg.id
}
# ------------------------------------------------------------------------------
# ECS app Security Group Rules - OUTBOUND
# ------------------------------------------------------------------------------
resource "aws_security_group_rule" "ecs_all_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
description = "Allow outbound traffic from ECS"
security_group_id = aws_security_group.ecs_sg.id
cidr_blocks = ["0.0.0.0/0"]
}

# ------------------------------------------------------------------------------
# Security Group for alb
# ------------------------------------------------------------------------------
resource "aws_security_group" "alb_sg" {
vpc_id = aws_vpc.vpc.id
name = "demo-sg-alb"
description = "Security group for alb"
revoke_rules_on_delete = true
}
# ------------------------------------------------------------------------------
# Alb Security Group Rules - INBOUND
# ------------------------------------------------------------------------------
resource "aws_security_group_rule" "alb_http_ingress" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "TCP"
description = "Allow http inbound traffic from internet"
security_group_id = aws_security_group.alb_sg.id
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "alb_https_ingress" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "TCP"
description = "Allow https inbound traffic from internet"
security_group_id = aws_security_group.alb_sg.id
cidr_blocks = ["0.0.0.0/0"]
}
# ------------------------------------------------------------------------------
# Alb Security Group Rules - OUTBOUND
# ------------------------------------------------------------------------------
resource "aws_security_group_rule" "alb_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
description = "Allow outbound traffic from alb"
security_group_id = aws_security_group.alb_sg.id
cidr_blocks = ["0.0.0.0/0"]
}

Step 3.8 : Variables

Throughout the terraform files, variables have been referenced and these variables are created and stored separately from the other configuration files. The reason for this is to create an easily reusable template where the only values that need to be changed are the variables.

The variables section consists of two parts. The variables.tf file and the terraform.tfvars file. The first file declares all variables used in the project and the second file attaches a value to these variables.

variables file

terraform.tfvars

#These are the only value that need to be changed on implementation
region = "eu-west-1"
vpc_cidr = "10.0.0.0/16"
public_subnet_1 = "10.0.16.0/20"
public_subnet_2 = "10.0.32.0/20"
private_subnet_1 = "10.0.80.0/20"
private_subnet_2 = "10.0.112.0/20"
availibilty_zone_1 = "eu-west-1a"
availibilty_zone_2 = "eu-west-1b"
container_port = 8081
shared_config_files = "" # Replace with path
shared_credentials_files = "" # Replace with path
credential_profile = "" # Replace with what you named your profile

The terraform.tfvars file is the only file that needs to be changed when implementing this project.

The complete source for this application can be found here.

Step 4: Deployment

Its now time to deploy the application and test. Ensure that the folder structure of the directory looks something like the below:

Main Folder structure

The app folder will contain the flask application and the folder directory needs to be as shown below:

Flask app folder structure

The application is now ready to de deployed.

Open a CMD prompt in the directory of your terraform files.

Run:

terraform init

This will initialize the working directory for terraform and download all required providers.

Then:

terraform plan

The above command allows you to review the changes that will be made once applied. If there are no issues and you are happy with the configurations you can run the last step.

terraform apply

The above command deploys the infrastructure described in our code.

Step 5: Accessing the app

After step 4 is completed and terraform has deployed the required infrastructure, head over to the AWS console.
Locate the load balancer and find the ‘DNS name’ under the details section of the load balancer.

Copy the DNS name and paste it in your browser. You should now be able to access your application.

Application

We can also confirm that our health check works as expected:

Health check path

To summarize, we’ve embarked on an exciting journey of containerization with Amazon ECS. We’ve built resilient and highly available container tasks. We have also secured the containers in a private subnet and introduced an Application Load Balancer to add security and scalability.

Of course this isn’t the end but just a starting point. You can take this project to the next level.

A few considerations:

  • Add an SSL certificate for HTTPS requests
  • Security groups can be more secure
  • IAM roles can be more restrictive
  • ECR can also use a VPC endpoint to pull images

If you have more recommendations or if you would just like to share your experiences, please feel free to fill the comments below.

Once again Part 1 and Part 2 links are attached as well as the GitHub repo

Thank you!

--

--