ECS (Fargate) with ALB Deployment Using Terraform — Part 3
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.
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
###################################################################################
# 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.
- 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.
- 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.
- 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
#################################################################################################
# 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
#################################################################################################
# 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.
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:
The app folder will contain the flask application and the folder directory needs to be as shown below:
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.
We can also confirm that our health check works as expected:
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!