Deploy Scalable Docker Service with Amazon ECS on EC2 instances

Evgeniia
Towards AWS
Published in
17 min readDec 16, 2022

--

This tutorial will teach you how to deploy scalable Docker service with Amazon ECS on EC2 instances.

Introduction

This tutorial will teach you how to deploy scalable Docker service with Amazon ECS on EC2 instances. Deploying on EC2 instead of Fargate gives you more flexibility around your servers, which can be helpful in many cases.

Objectives

By the end of this tutorial, you will be able to:

  1. Explain what Terraform is and why it is essential for deployment
  2. Push custom Docker image to the ECR
  3. Define AWS architecture through Terraform
  4. Deploy a Docker-based app and make it scalable

App Description

For this tutorial, we will look at the scenario of video generation API, which takes a photo of a character and a script and creates a video of this character acting out the script. To be more specific, we also define the requirements of the AWS architecture for this app:

  1. It must be easy to scale the web servers up and down automatically.
  2. Even if the user terminates the request, the video generation process should be finished.
  3. The servers responsible for video generation cannot be accessed directly by the rest of the world.
  4. There must be no single points of failure.
  5. The videos should not be created twice.
  6. All uploaded character’s photos and generated videos should be stored in the database in the cloud.

Basic App Interface

To imitate the application we discussed above, I create a boilerplate code using FastAPI in Python. A few methods that I defined are as follows:

from fastapi import FastAPI  
from typing import Dict

app = FastAPI()

@app.get("/")
async def root() -> Dict[str, str]:
return {"message": "Welcome to Fake Video Studio"}

@app.get("/create-video")
async def create_video(character: str, script: str) -> str:
return "s3_link"

As you can see, we have two dummy methods, which we will use to test that the API is correctly deployed and responsive.

Terraform

Terraform is an open-source provisional tool for Infrastructure as Code (IaC) development. You can define your entire platform architecture using terraform in a cloud-agnostic way. It means you can define your architecture, for example, on AWS and Azure, simultaneously. Terraform uses beginner-friendly language to declare architecture. It is called Hashicorp Configuration Language (HCL). Its style is very declarative and easy to understand, so even people who do not know how to code can grasp the main aspects of the future architecture while looking at the configuration files. The benefits of declaring your architecture with the IaC tool include increased consistency and speed of deployments, easier team collaboration, error reduction, and enhanced security.

Architecture Overview

This is the chart that summarises the architecture that will be built during this tutorial.

Architecture Diagram

There are a few details on design choices that make this architecture suitable for the scenario of our app.

  1. No access to the servers for generation or database through private subnets and security groups.
  2. No single point of failure due to distributed database + EC2 instances in 2 availability zones.
  3. Created videos will be stored in S3 and assessed through the table in the database containing the script, character name, and the link to the video in S3.
  4. Autoscaling group to scale the EC2 instances in case of high demand.

ECR Creation and Image Push

Initially, we need to create ECR (Elastic Container Repository) on AWS. You can also use Docker Hub as your Docker image storage, but it is much easier to integrate ECR with ECS so we will stick to that. This part of the tutorial will be done using the AWS interface.

  1. Open the ECR panel in your AWS console and click on Repositories.

2. Click on Create repository.

3. Now, you need to open your new repository.

4. To push your Docker image, you need to follow the set of instructions that you can get from clicking on the View push commands button.

5. Follow the obtained instructions to push your image.

Now, we have a Docker image uploaded to your ECR repository, so we can start implementing the terraform architecture.

Architecture Implementation

To develop terraform infrastructure, we create terraform folder in the project directory. I prefer to write each block of resources in separate files rather than writing the whole architecture in main.tf file.

Let’s start with defining providers. The provider is a plugin in Terraform that allows communication with an API. Providers cover vendors of cloud computing and software as a service. The providers are listed in the code for Terraform configuration. They specify to Terraform which services it should communicate with.

For our purpose, we will use AWS provider. We create providers.tf file and put there the commands below. To authenticate with AWS, we will supply the access key id and secret access key. Moreover, we will define the desired region for our infrastructure.

# providers.tf

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
access_key = var.aws_access_key_id
secret_key = var.aws_secret_access_key
}

Furthermore, we need to declare variables for our terraform code. Variables are helpful to avoid repetitions in your terraform configuration code. Moreover, you can supply the values for them during infrastructure creation; thus, you can, for example, never expose your secrets. We create vars.tf file and provide the region, AWS credentials, desired AWS zones, and ECR repository name as our variables. If you know the values you want to use (such as AWS zones in the following example), declare the default attribute for your variable.

# vars.tf

variable "aws_region" {
type = string
description = "Region to use"
default = "us-west-2"
}
variable "aws_access_key_id" {
type = string
}
variable "aws_secret_access_key" {
type = string
}
variable "aws_zones" {
type = list(string)
description = "List of availability zones to use"
default = ["us-west-2a", "us-west-2b"]
}
variable "aws_ecr_repo" {
type = string
}

We must also declare the backend where the terraform configuration file will be stored. As we are working with AWS, our tfstate file will be held in the S3 bucket on AWS.

# backend.tf

terraform {
backend "s3" {
bucket = "fake-video-studio"
key = "shared/terraform.tfstate"
region = "us-west-2"
encrypt = false
dynamodb_table = "fake-video-studio-db"
}
}

Now, we will implement the architecture above using terraform. Specifically, we will create the following resources:

  • VPC
  • AMI
  • IAM roles
  • Private and Public Subnets
  • Internet and NAT gateways
  • Security Groups for ALB, ECS, and RDS
  • Route Tables
  • Application Load Balancer
  • Autoscaling Group
  • RDS Database and Database Subnet
  • ECS Cluster and Service with EC2 Instances
  • ElastiCache cluster with Redis
  • CloudWatch Log Stream

VPC

First, we will create a VPC (Virtual Private Cloud) in vpc.tf file. AWS VPC is a virtual network, a secure and isolated private cloud hosted within a public cloud. We need to declare a CIDR block, a block of IP addresses that follow the IPv4 standard. We also enable DNS support even though it is out of the scope of this tutorial.

# vpc.tf

resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "vpc-cloud-tutorial"
Env = "production"
}
}

As you can see, we also add tags for resources. It is essential as the Name tag is used to name the resource in AWS, and the Env tag can be used to query resources.

AMI

To launch EC2 (Elastic Compute Cloud) instances, we need to define AMI (Amazon Machine Image) in ami.tf file. As we are using ECS (Elastic Container Service) and want the most recent AMI, we ask AWS for the most recent ECS-optimized AMI.

# ami.tf

data "aws_ami" "ecs_ami" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn-ami-*-amazon-ecs-optimized"]
}
}

IAM Roles

The next step is to define multiple IAM (Identity and Access Management) roles for ECS Host, ECS Service, and ECS instances in iam_role.ecs.tf file. We need to create a folder called policies, where we will store the JSON files for the role definitions.

First, we define the IAM role of the ECS Host.

/* ecs-role.json */

{
"Version": "2008-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": [
"ecs.amazonaws.com",
"ec2.amazonaws.com"
]
},
"Effect": "Allow"
}
]
}
# iam_role.ecs.tf

resource "aws_iam_role" "ecs-role" {
name = "ecs_host_role"
assume_role_policy = file("policies/ecs-role.json")
}
resource "aws_iam_role" "ecs-service-role" {
name = "ecs_service_role"
assume_role_policy = file("policies/ecs-role.json")
}
resource "aws_iam_instance_profile" "ecs" {
name = "ecs_instance_profile"
path = "/"
role = aws_iam_role.ecs-role.name
}

Second, we define IAM role of the ECS service.

/* ecs-service-role.json */

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"elasticloadbalancing:Describe*",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
"ec2:Describe*",
"ec2:AuthorizeSecurityGroupIngress",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DeregisterTargets"
],
"Resource": [
"*"
]
}
]
}
# iam_role.ecs.tf

resource "aws_iam_role_policy" "ecs-service-role-policy" {
name = "ecs_service_role_policy"
policy = file("policies/ecs-service-role-policy.json")
role = aws_iam_role.ecs-service-role.id
}
resource "aws_iam_instance_profile" "ecs" {
name = "ecs_instance_profile"
path = "/"
role = aws_iam_role.ecs-role.name
}

Lastly, we define the IAM role of the ECS’s EC2 instances.

/* ecs-instance-role-policy.json */

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:*",
"ec2:*",
"elasticloadbalancing:*",
"ecr:*",
"cloudwatch:*",
"s3:*",
"rds:*",
"logs:*"
],
"Resource": "*"
}
]
}
# iam_role.ecs.tf

resource "aws_iam_role_policy" "ecs-instance-role-policy" {
name = "ecs_instance_role_policy"
policy = file("policies/ecs-instance-role-policy.json")
role = aws_iam_role.ecs-role.id
}

Private and Public Subnets

As our service must be accessible from the internet, while some of its resources should be hidden from the public, we must define public and private subnets for our VPC. A subnet is a range of IP addresses in the VPC. A public subnet is a subnet that has a route to internet gateway. Thus, you can connect to it outside your VPC. A private subnet does not have internet gateway route, so it does not accept the traffic outside of your VPC.

We will create private and public subnets for each of the availability zones. To make it more dynamic, we are using count keyword. It will run the resource creation multiple times (once for each availability zones).

First, we define private subnets in subnet.private.tf.

# subnet.private.tf

resource "aws_subnet" "private_subnet" {
count = length(var.aws_zones)
vpc_id = aws_vpc.vpc.id
availability_zone = element(var.aws_zones, count.index)
cidr_block = "10.0.${count.index + 3}.0/24"
tags = {
Name = "private-${element(var.aws_zones, count.index)}"
Env = "production"
}
}

Second, we define public subnets in subnet.public.tf.

# subnet.public.tf

resource "aws_subnet" "public_subnet" {
count = length(var.aws_zones)
vpc_id = aws_vpc.vpc.id
map_public_ip_on_launch = true
availability_zone = element(var.aws_zones, count.index)
cidr_block = "10.0.${count.index + 1}.0/24"
tags = {
Name = "public-${element(var.aws_zones, count.index)}"
Env = "production"
}
}

Internet and NAT gateways

We need our app to communicate with the world. Thus, we need to define the internet gateway that will accept traffic from outside of the VPC in the internet_gateway.tf file.

# internet_gateway.tf

resource "aws_internet_gateway" "internet_gateway" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "internet-gateway"
Env = "production"
}
}

Now, as our application will be able to receive outside traffic, we need to make sure that the resources within the VPC will be able to interact. We can do so with the NAT (Network Address Translation) gateway. To initiate a NAT gateway, we must create an Elastic IP address first. We do all of this in the nat_gateway.tf file

# nat_gateway.tf

resource "aws_eip" "gateway" {
vpc = true
associate_with_private_ip = "10.0.0.5"
depends_on = [aws_internet_gateway.internet_gateway]
}
resource "aws_nat_gateway" "nat_gateway" {
subnet_id = aws_subnet.public_subnet[0].id
allocation_id = aws_eip.gateway.id
depends_on = [aws_eip.gateway]
}

Security Groups for ALB, ECS, and RDS

Next, we define the security groups for ALB (Application Load Balancer), ECS, and RDS.

The security group for ALB supports traffic from ports 80 (HTTP internet traffic) and 443 (HTTPS internet traffic) and is declared in the security_group.alb.tf file.

# security_group.alb.tf

resource "aws_security_group" "alb" {
name = "security-group-alb"
description = "security-group-alb"
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Env = "production"
Name = "security-group-alb"
}
vpc_id = aws_vpc.vpc.id
}

The security group for RDS supports traffic from port 5432 (Postgres traffic) and is declared in the security_group.db_instance.tf file.

# security_group.db_instance.tf

resource "aws_security_group" "rds_sg" {
name = "security-group-db-instance"
description = "security-group-db-instance"
egress {
cidr_blocks = ["0.0.0.0/0"]
from_port = 0
protocol = "-1"
to_port = 0
}
ingress {
from_port = 5432
protocol = "tcp"
to_port = 5432
security_groups = [aws_security_group.ecs.id]
}
tags = {
Name = "security-group-db-instance"
Env = "production"
}
vpc_id = aws_vpc.vpc.id
}

The security group for ECS supports traffic from ports 80 (HTTP internet traffic), 22 (ssh traffic), and 0 (localhost traffic) and is declared in the security_group.ecs.tf file.

# security_group.ecs.tf 

resource "aws_security_group" "ecs" {
name = "security-group-ec2"
description = "security-group-ecs"
ingress {
from_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
to_port = 80
}
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.alb.id]
}
ingress {
from_port = 22
to_port = 22
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"]
}
tags = {
Name = "security-group-ec2"
Env = "production"
}
vpc_id = aws_vpc.vpc.id

Route Tables

The destination of network traffic from your subnet or gateway is determined by a set of rules called routes that are included in a route table. Simply described, a route table instructs network packets in which direction to go to reach their destination.

We need to implement two empty route tables for our private and public subnets in route_table.private.tf and route_table.public.tf files.

# route_table.private.tf

resource "aws_route_table" "private_route_table" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "private-route-table"
Env = "production"
}
}
# route_table.public.tf
resource "aws_route_table" "internet_route_table" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "internet-route-table"
Env = "production"
}
}

Then, we need to associate the route table with the subnets themselves through route table associationsin route_table_association.private.tf and route_table_association.public.tf.

# route_table_association.private.tf

resource "aws_route_table_association" "private_route" {
count = length(var.aws_zones)
subnet_id = element(aws_subnet.private_subnet.*.id, count.index)
route_table_id = aws_route_table.private_route_table.id
}
# route_table_association.public.tf
resource "aws_route_table_association" "public_route" {
count = length(var.aws_zones)
subnet_id = element(aws_subnet.public_subnet.*.id, count.index)
route_table_id = aws_route_table.internet_route_table.id
}

The only thing that needs to be done is to add the “rules” — routes. We have an internet gateway for the public subnets, so we create the route to it in the internet route table, and a nat gateway for the private subnets, so we create the route to it in the private route table. We do all of these in route.tf file.

# route.tf

resource "aws_route" "internet_access_route" {
route_table_id = aws_route_table.internet_route_table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.internet_gateway.id
}
resource "aws_route" "nat_gateway_route" {
route_table_id = aws_route_table.private_route_table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.nat_gateway.id
}

Application Load Balancer

Load balancers are needed to distribute incoming network traffic across EC2 instances. The load balancer in our example is essential because we want to be able to autoscale based on demand, so the distribution of traffic is an important issue. When a request comes to the API, it should be allocated to the free EC2 instance or be put in the queue until one of the EC2 instances will not become free.

To create a load balancer, you need to put the following code to the alb.tf file.

# alb.tf

resource "aws_lb" "alb" {
name = "alb"
load_balancer_type = "application"
internal = false
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public_subnet.*.id
}

It is vital to allocate our public subnets to the load balancer as the requests from the outside world will only come through them. Because of that, we set the internal parameter to be false as we need an external ALB (external because of the traffic outside of the VPC). Moreover, we should use the security group defined for the ALB we created in the previous steps.

Now, we define the target group in the alb_target_group.tf file. Requests are routed to one or more registered targets (EC2 instances) using a Target Group. We give the VPC identifier to the target group, so it can determine which EC2s to use. We also define health check, so the target group can check that the EC2 to which the request from ALB will be sent is responsive.

# alb_target_group.tf

resource "aws_alb_target_group" "default" {
name = "alb-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
health_check {
path = "/"
}
}

Lastly, we define the listener for the ALB in the alb_listener.tf file. ALB “listens” for the specific port, and if requests are coming to it, they are directed to the target group. Here, we will use port 80, our standard API port.

# alb_listener.tf

resource "aws_alb_listener" "http" {
load_balancer_arn = aws_lb.alb.id
port = "80"
protocol = "HTTP"
depends_on = [aws_alb_target_group.default]
default_action {
target_group_arn = aws_alb_target_group.default.arn
type = "forward"
}
}

Autoscaling Group

As discussed in our requirements, we must be ready for the high demand. Thus, we need to implement autoscaling. With autoscaling, the number of EC2 containers in the ECS cluster can be increased or decreased based on the current demand.

First, we need to create a user_data.sh shell script with the command that will generate the EC2 instances inside the needed ECS cluster.

# user_data.sh

#!/bin/bash
echo ECS_CLUSTER="${ecs_cluster_name}" >> /etc/ecs/ecs.config

We also need to define key pair for the EC2, so we can access them after creation. We do that in key_pair.tf file. To obtain your key pair, run the following set of command in terminal:

KEY_PATH=~/.ssh/fake-video-studio
EMAIL=evgeniia@uni.minerva.edu

ssh-keygen -t rsa -b 4096 -f $KEY_PATH -C $EMAIL
# Enter passphrase (empty for no passphrase): press Enter
# Enter same passphrase again: press Enter
chmod 600 $KEY_PATH
ssh-add $KEY_PATH
cat ${KEY_PATH}.pub

Copy the output from the last command and insert it into public key attribute. Don’t forget to put your email into the Name tag!

# key_pair.tf

resource "aws_key_pair" "default" {
key_name = "fake-video-studio"
public_key = "your_public_key"
tags = {
"Name" = "evgeniia@uni.minerva.edu"
}
}

Next, we define launch configuration in launch_configuration.tf file. Launch configuration will determine the parameters for EC2s that will be created during the autoscaling process. Therefore, we need to define AMI, instance type, security group, and IAM profile that will be used to create EC2 instances. We also need to give the user data file to the launch configuration.

# launch_configuration.tf

data "template_file" "user_data_template" {
template = file("user_data.sh")
vars = {
ecs_cluster_name = aws_ecs_cluster.production.name
}
}
resource "aws_launch_configuration" "aws_conf" {
image_id = data.aws_ami.ecs_ami.id
instance_type = "t2.micro"
security_groups = [aws_security_group.ecs.id]
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.ecs.name
name_prefix = "launch-configuration-"
key_name = aws_key_pair.default.key_name
user_data = data.template_file.user_data_template.rendered
lifecycle {
create_before_destroy = true
}
}

Now, we are ready to define the autoscaling group in autoscaling_group.tf. We are doing it in our private subnets with the launch configuration we wrote earlier. We can determine the minimum, maximum, and desired number of instances in the ECS. We also define the health checks and some of the parameters of the autoscaling group, such as cooldown period length or termination policy.

# autoscaling_group.tf

resource "aws_autoscaling_group" "asg" {
name = "as_group"
vpc_zone_identifier = aws_subnet.private_subnet.*.id
min_size = 1
max_size = 3
desired_capacity = 2
launch_configuration = aws_launch_configuration.aws_conf.id
health_check_type = "EC2"
health_check_grace_period = 120
default_cooldown = 30
termination_policies = ["OldestInstance"]
lifecycle {
create_before_destroy = true
}
tag {
key = "Env"
propagate_at_launch = true
value = "production"
}
tag {
key = "Name"
propagate_at_launch = true
value = "fake-video-studio"
}
}

RDS Database and Database Subnet

As we do not want to regenerate videos for the same characters and scripts, we need to store the links to the videos stored in s3. To do that, we will use the PostgreSQL database in RDS (Relational Database Service).

First, we define the database subnet group in db_subnet_group.tf. It will allow the EC2 instances in a private subnet to communicate with the database.

# db_subnet_group.tf

resource "aws_db_subnet_group" "db_subnet_group" {
name = "db-subnet-group"
subnet_ids = aws_subnet.private_subnet.*.id
tags = {
Name = "db-subnet-group"
Env = "production"
}
}

Next, we declare the database instance in rds.tf file. We need to supply the database subnet group and RDS security group to this resource. Here, we also give the resource many other parameters, such as database name, password, username, etc. But be careful; usually, you do not need to put your password this way; you should store it as a secret. You can also define the database’s backup or maintenance time windows, allocated memory, or instance class.

# rds.tf

resource "aws_db_instance" "production" {
backup_window = "03:00-04:00"
ca_cert_identifier = "rds-ca-2019"
db_subnet_group_name = aws_db_subnet_group.db_subnet_group.name
engine_version = "13.4"
engine = "postgres"
skip_final_snapshot = true
identifier = "production"
instance_class = "db.t3.micro"
maintenance_window = "sun:08:00-sun:09:00"
name = "fake_video_studio_db"
parameter_group_name = "default.postgres13"
password = "test12345"
username = "postgres"
allocated_storage = "10"
port = "5432"
vpc_security_group_ids = [aws_security_group.rds_sg.id]
multi_az = false
backup_retention_period = 7
}

ECS Cluster and Service with EC2 Instances

Now, we are ready to implement ECS (Elastic Container Service) cluster and service. First, we create the file ecs_cluster.tf and put a basic cluster declaration into it.

# ecs_cluster.tf

resource "aws_ecs_cluster" "production" {
name = "production"
lifecycle {
create_before_destroy = true
}
tags = {
Name = "production"
Env = "production"
}
}

As ECS operates based on Docker containers, we need to create a file container_definitions.json with container configuration, so multiple instances of it can be created inside the ECS service. Here, we declare basic parameters similar to docker-compose (name, command, image, port mappings) and more special ones (CPU and memory of each container or logging).

/* container_definitions.json */

[{
"name": "fake-video-studio",
"command": ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "80"],
"cpu": 10,
"memory": 512,
"image": "${ecr_repository}",
"essential": true,
"links": [],
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp",
"hostPort": 0
}
],
"environment": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/fake-video-studio",
"awslogs-region": "${region}",
"awslogs-stream-prefix": "fake-video-studio-log-stream"
}
}
}]

Now, we create resource for our container definition.

# ecs_task_definition.tf

data "template_file" "app" {
template = file("container_definitions.json")
vars = {
region = var.aws_region
ecr_repository = var.aws_ecr_repo
}
}

resource "aws_ecs_task_definition" "td" {
family = "fake-video-studio"
container_definitions = data.template_file.app.rendered
depends_on = [aws_db_instance.production]
}

Lastly, we define ECS service. We supply task definition, ECS IAM role, load balancer, and ECS cluster. Notice that the number of ECS services must equal the number of availability zones.

# ecs_service.tf

resource "aws_ecs_service" "service" {
name = "fake-video-studio"
cluster = aws_ecs_cluster.production.id
desired_count = length(var.aws_zones)
force_new_deployment = true
iam_role = aws_iam_role.ecs-service-role.arn
task_definition = aws_ecs_task_definition.td.arn
load_balancer {
target_group_arn = aws_alb_target_group.default.arn
container_name = "fake-video-studio"
container_port = 80
}
depends_on = [aws_alb_listener.http, aws_iam_role_policy.ecs-service-role-policy]
}

ElastiCache cluster with Redis

We also want to implement caching, so the same requests will obtain the same responses without running the algorithm. Thus, we define ElastiCache with Redis in elasticache.tf file. We again need to create a subnet group (as we have done for the database). Then, we declare the replication group required to distribute the cache and make it more reliable and fast. Lastly, we just create the ElastiCache cluster using this replication group.

resource "aws_elasticache_subnet_group" "elasticache_subnet" {  
name = "fake-video-studio-cache-subnet"
subnet_ids = aws_subnet.private_subnet.*.id
}

resource "aws_elasticache_replication_group" "rep_group" {
automatic_failover_enabled = true
availability_zones = var.aws_zones
replication_group_id = "fake-video-studio-rep-group"
replication_group_description = "Replication group"
node_type = "cache.t3.micro"
number_cache_clusters = 2
port = 6379
subnet_group_name = aws_elasticache_subnet_group.elasticache_subnet.name
lifecycle {
ignore_changes = [number_cache_clusters]
}
}
resource "aws_elasticache_cluster" "replica" {
count = 2
cluster_id = "fake-video-studio-rep-group-${count.index}"
replication_group_id = aws_elasticache_replication_group.rep_group.id
}

CloudWatch Log Stream

It is pretty valuable to collect logs of your application. It can help you see where exactly bugs appear in your problem or analyze what users usually do in your application. AWS has a CloudWatch app, which can help you to monitor application logs. To implement it through terraform, we declare the CloudWatch log group and log stream in the logs.tf file. We set the retention period to be 30 days, meaning that application’s logs will be stored for 30 days and then deleted.

# logs.tf

resource "aws_cloudwatch_log_group" "fake-video-studio-log-group" {
name = "/ecs/fake-video-studio"
retention_in_days = 30
}
resource "aws_cloudwatch_log_stream" "fake-video-studio-log-stream" {
name = "fake-video-studio-log-stream"
log_group_name = aws_cloudwatch_log_group.fake-video-studio-log-group.name
}

Results

Now, as we implemented all the required infrastructure, we can go to the load balancer and see the link, where our API will be hosted:

If you did everything correctly, you will see the “Hello World” response from the API.

Conclusion

In this tutorial, we learned what is terraform and how it can be used to define complex AWS infrastructure. You can find all the code from this tutorial in the github repository.

Thank you for reading this tutorial! I hope you found it helpful.

--

--

Data Science student at Minerva Schools at KGI. Aspiring Python Developer at B-rain Tech. Data Science and Machine Learning tutor.