How to build a microservices infrastructure on AWS using Terraform, ECS Fargate, and CloudMap | part two
This is the second part of this post: https://medium.com/aws-infrastructure/how-to-build-a-microservices-infrastructure-on-aws-using-terraform-ecs-fargate-and-cloudmap-f20ac7b82d73.
If you haven’t read it yet, I recommend you follow the link and prepare your infrastructure to permit the single service creation as described on this page.
Let’s begin with downloading the repo:
https://github.com/stech-world/build-a-microservices-infrastructure-on-aws-using-terraform-ecs-fargate-and-cloudmap
Here we can find the following folder structure:
This post will describe how to deploy ECR Repos, and services code and repos.
ECR
resource "aws_ecr_repository" "fgms-uno" {
name = "fgms-uno"
}
resource "aws_ecr_repository" "fgms-due" {
name = "fgms-due"
}
resource "aws_ecr_repository" "fgms-tre" {
name = "fgms-tre"
}
The aws_ecr_repository
resource type creates an Elastic Container Registry (ECR) repository in AWS. In this case, three ECR repositories are being defined: fgms-uno
, fgms-due
, and fgms-tre
.
Each repository is defined by a block of code with the following format:
resource "aws_ecr_repository" "REPOSITORY_NAME" {
name = "REPOSITORY_NAME"
}
The name
argument specifies the name of the ECR repository. This must be unique within the region and account specified in the provider
block.
Once the Terraform code is applied, these three ECR repositories will be created in the specified AWS account and region. They can be used to store Docker images and manage their lifecycle.
SERVICES
This is what our simple application will do:
— uno-service receives an HTTP request from the load balancer and, using Cloud Map as internal DNS routes the request to due-service and tre-service;
— uno-service prepares the response to the load balancer using responses from due-service and tre-service.
You can imagine uno-service as a frontend app that gathers info from the underlying microservices and responds to requests from the outside.
Apps Code
The code is a basic nodejs express app that returns a static response. The only difference is in the uno-service; it has a proxy role so, the code contains due HTTP requests with Axios to due-service and tre-service.
uno-service code
const express = require('express')
const fetch = require('node-fetch');
const app = express()
const port = 3000
app.get('/', async (req, res) => {
const dueResponse = await fetch(`${process.env.DUE_SERVICE_API_BASE}:3000`)
const treResponse = await fetch(`${process.env.TRE_SERVICE_API_BASE}:3000`)
const dueData = await dueResponse.json();
const treData = await treResponse.json();
res.json({
msg: "Hello world! (from uno)",
due:{
url: process.env.DUE_SERVICE_API_BASE,
data: dueData,
},
uno:{
url: process.env.TRE_SERVICE_API_BASE,
data: treData,
}
})
})
app.get('/healthcheck', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
due/tre-service code
const express = require('express')
const fetch = require('node-fetch');
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.json({
msg: "Hello world! (from tre)",
})
})
app.get('/healthcheck', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Apps Dockerfile
FROM node:8-alpine
WORKDIR /usr/app
COPY package.json .
RUN npm i --quiet
COPY . .
RUN npm install -g pm2@4.2.1
CMD ["pm2-runtime", "./index.js"]
FROM node:8-alpine
is a Dockerfile directive that specifies the base image for the Docker image that is being built. In this case, it specifies that the image should be based on the node:8-alpine
image, which is a lightweight version of the Node.js runtime environment that is based on the Alpine Linux distribution.
WORKDIR /usr/app
is a Dockerfile directive that sets the working directory for subsequent instructions in the Dockerfile. In this case, it sets the working directory to /usr/app
.
COPY package.json .
is a Dockerfile directive that copies the package.json
file from the context (the directory containing the Dockerfile) to the working directory (/usr/app
).
RUN npm i --quiet
is a Dockerfile directive that runs the command npm i --quiet
in the container. This command installs all of the dependencies specified in the package.json
file. The --quiet
flag suppresses npm's progress output.
COPY . .
is a Dockerfile directive that copies all files from the context (the directory containing the Dockerfile) to the working directory (/usr/app
).
RUN npm install -g pm2@4.2.1
is a Dockerfile directive that installs the pm2
package (version 4.2.1) globally in the container. pm2
is a process manager for Node.js applications.
CMD ["pm2-runtime", "./index.js"]
is a Dockerfile directive that specifies the default command to run when the container is started. In this case, it specifies that the pm2-runtime
command should be run with the ./index.js
argument. This will start the Node.js application in ./index.js
using pm2
.
Upload every docker image in the respective ECR repo
The file ecr-deploy-script.sh
contains bash command with AWS-CLI to upload container images on ECR.
You must run the following commands for every apps (uno, due, tre):
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com
docker build -t uno ./uno
# docker buildx build --platform=linux/amd64 -t uno ./uno !!In case you use apple m1 chip use this command instead of the first!!
docker tag uno <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com/fgms-uno:v1
docker push <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com/fgms-uno:v1
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com
is a command that retrieves an authentication token from AWS Identity and Access Management (IAM) and then uses it to log in to the Amazon Elastic Container Registry (ECR) in the eu-west-1
region. The |
symbol pipes the output of the aws
command into the docker login
command.
docker build -t uno ./uno
is a command that builds a Docker image from the Dockerfile in the ./uno
directory and tags the image with the name uno
.
docker tag uno <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com/fgms-uno:v1
is a command that tags the uno
image with a new name that includes the specified Amazon ECR registry and repository name.
docker push <YOUR AWS ACCOUNT ID>.dkr.ecr.eu-west-1.amazonaws.com/fgms-uno:v1
is a command that pushes the uno
image to the specified Amazon ECR repository.
Service Terraform Stack
Inside the folder stacks/services there are all the services AWS Resources needed to create the infrastructure.
resource "aws_ecs_task_definition" "fgms_uno_td" {
family = "fgms_uno_td"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.fgms_uno_task_role.arn
container_definitions = jsonencode(
[
{
cpu : 256,
image : "248581660709.dkr.ecr.eu-west-1.amazonaws.com/fgms-uno:v1",
memory : 512,
name : "fgms-uno",
networkMode : "awsvpc",
environment : [
{
name : "DUE_SERVICE_API_BASE",
value : "http://${data.terraform_remote_state.services-due.outputs.fgms_due_service_namespace}.${data.terraform_remote_state.dns.outputs.fgms_private_dns_namespace}"
},
{
name : "TRE_SERVICE_API_BASE",
value : "http://${data.terraform_remote_state.services-tre.outputs.fgms_tre_service_namespace}.${data.terraform_remote_state.dns.outputs.fgms_private_dns_namespace}"
}
],
portMappings : [
{
containerPort : 3000,
hostPort : 3000
}
],
logConfiguration : {
logDriver : "awslogs",
options : {
awslogs-group : "/ecs/fgms_log_group",
awslogs-region : "eu-west-1",
awslogs-stream-prefix : "uno"
}
}
}
]
)
}
resource "aws_ecs_service" "fgms_uno_td_service" {
name = "fgms_uno_td_service"
cluster = data.terraform_remote_state.ecs_cluster.outputs.fgms_ecs_cluster_id
task_definition = aws_ecs_task_definition.fgms_uno_td.arn
desired_count = "1"
launch_type = "FARGATE"
network_configuration {
security_groups = ["${aws_security_group.ecs_tasks_sg.id}"]
subnets = ["${data.terraform_remote_state.vpc.outputs.fgms_private_subnets_ids[0]}"]
}
load_balancer {
target_group_arn = aws_alb_target_group.fgms_uno_tg.id
container_name = "fgms-uno"
container_port = 3000
}
service_registries {
registry_arn = aws_service_discovery_service.fgms_uno_service.arn
}
}
resource "aws_security_group" "ecs_tasks_sg" {
name = "ecs_tasks_sg"
description = "allow inbound access from the ALB only"
vpc_id = data.terraform_remote_state.vpc.outputs.fgms_vpc_id
ingress {
protocol = "tcp"
from_port = "3000"
to_port = "3000"
security_groups = ["${data.terraform_remote_state.alb.outputs.fgms_alb_sg_id}"]
}
ingress {
protocol = -1
from_port = 0
to_port = 0
self = true
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_alb_target_group" "fgms_uno_tg" {
name = "fgms-uno-tg"
port = 80
protocol = "HTTP"
vpc_id = data.terraform_remote_state.vpc.outputs.fgms_vpc_id
target_type = "ip"
health_check {
path = "/healthcheck"
}
}
resource "aws_alb_listener" "fgms_uno_tg_listener" {
load_balancer_arn = data.terraform_remote_state.alb.outputs.fgms_alb_id
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = aws_alb_target_group.fgms_uno_tg.id
type = "forward"
}
}
The aws_ecs_task_definition
resource creates an ECS task definition, which is a blueprint for an ECS task. The task definition specifies the Docker image to use for the task, the number of CPU and memory units to use, and the command to run when the task is launched. The task definition also specifies the IAM role that the task should use and the environment variables that should be passed to the container when it is started.
The aws_ecs_service
resource creates an ECS service, which is a long-running task that is typically used to host a web application. The service is associated with the ECS cluster and task definition defined in the code block, and it specifies the desired number of tasks that should be running and the launch type (Fargate). The service also specifies the network configuration and load balancer settings. The service is also registered with a service discovery service, which allows it to be discovered by other services.
The aws_security_group
resource creates an Amazon Elastic Compute Cloud (Amazon EC2) security group, which is a virtual firewall that controls inbound and outbound traffic to and from the ECS tasks. The security group only allows inbound traffic on port 3000 from the specified security group and allows all outbound traffic.
The aws_alb_target_group
resource creates an Amazon Elastic Load Balancer (ALB) target group, which is used to route traffic to the ECS tasks. The target group is associated with the specified security group and port.
The aws_alb_listener_rule
resource creates an ALB listener rule, which specifies how traffic should be routed to the target group. The listener rule routes traffic from the specified ALB listener to the target group based on the specified host header and path pattern.
Regarding Cloud Map, we declared the following resource with DNS stack as a dependency:
resource "aws_service_discovery_service" "fgms_uno_service" {
name = var.fgms_uno_service_namespace
dns_config {
namespace_id = data.terraform_remote_state.dns.outputs.fgms_dns_discovery_id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 2
}
}
The aws_service_discovery_service
resource creates an Amazon Route 53 service discovery service, which allows services to discover each other through DNS. The service is given a name, which is specified using the name
parameter and is derived from a variable.
The dns_config
block specifies the DNS configuration for the service discovery service. It specifies the ID of the Amazon Route 53 namespace that the service should use, as well as the DNS records and routing policy for the service. The DNS records specify the Time To Live (TTL) value and type for the service.
The health_check_custom_config
block specifies custom health check configurations for the service discovery service. It specifies the number of consecutive failures required before the health check status is considered a failure.
Deploy
Let’s deploy every service (after uploading the docker image on ECR) with Terraform commands described in the part one of this tutorial:
$ cd stacks/dns
$ terraform init
$ terraform plan
$ terraform apply
The services deploy order is:
- due
- tre
- uno
Uno needs cloud map DNS record value of due and tre so, it must be the last one.
The following image represents all records inside the private Cloud Map hosted zone:
TEST ALL THE SYSTEM
Once every stack has been deployed, it’s time for testing our application!
Go to the load balancer section inside the EC2 console:
Under the DNS name there’s the load balancer URL to copy and paste into the browser.
Copy the URL in the search bar of the browser and you should receive the following response.
If you get an output like this you successfully complete this tutorial; otherwise, check this page to find if something missing.
Bye 😘