AWS Fargate Microservices with CloudFormation | single service template

Stefano Monti
AWS Infrastructure
Published in
7 min readDec 28, 2022

Hi! This article is linked to AWS Fargate Microservices with CloudFormation | basic infrastructure, if haven’t read it yet I recommend you to follow the link and prepare your infrastructure in order to permit the single service creation as described on this page.

Let’s begin by cloning the service repo you can find here!

The repo contains:
— infra subfolder with all the cloudformation resources for deploying the service and sh script with useful commands for running deploys from the command line (it uses AWS-CLI, if you don’t know what is it, check this);
— the source folder with the microservice code;
— a docker file for dockerizing our service and uploading it into the ECR repo.

First Microservices called ‘UNO’

ECR Repo deployment

First, we must create the ECR repo:

AWSTemplateFormatVersion: '2010-09-09'

Description: ECR Repositiories

Parameters:
ServiceName:
Type: String

Resources:

UnoRepo:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Sub stw-aws-ecs-microservices-${ServiceName}-service

… and deploy it:

aws cloudformation create-stack --stack-name cf-infra-aws-ecs-microservices-ecr-<YOUR SERVICE NAME>-repo-stack --template-body file://./infra/ecr-repo-stack.yml \
--parameters ParameterKey=ServiceName,ParameterValue=<YOUR SERVICE NAME>

Remember to choose a service name to substitute in the command provided; in this example, the first service name will be ‘uno’.

aws cloudformation create-stack --stack-name cf-infra-aws-ecs-microservices-ecr-uno-repo-stack --template-body file://./infra/ecr-repo-stack.yml \
--parameters ParameterKey=ServiceName,ParameterValue=uno

The Container Registry has been created:

Code

const express = require('express');
const axios = require('axios');

const app = express();

const serviceName = process.env.SERVICE_NAME
const loadBalancerUrl = process.env.LOADBALANCER_URL

app.get('/', async (req, res) => {
return res.send({ error: false, v: 5 });
});

app.get(`/${serviceName}/cities`, async (req, res) => {
return res.send({
error: false,
v: 5,
topic: 'cities',
form: serviceName,
});
});

app.get(`/${serviceName}/books`, async (req, res) => {
return res.send({
error: false,
v: 5,
topic: 'books',
form: serviceName,
});
});

app.get('/stat', async (req, res) => {
return res.send({ error: false });
});


const server = app.listen(process.env.PORT || 4567);

This simple express application returns a fixed response; we will test the ‘cities’ and ‘books’ API. The /stat API is used by the load balancer for the health check.

Dockerfile

FROM node:18-alpine

EXPOSE 80

WORKDIR /app
COPY package*.json ./
COPY . .

RUN npm i pm2 -g
RUN npm i
COPY . .

CMD ["pm2-runtime", "./src/index.js"]

The image will be built and run with the use of pm2. Check this link if you want to know more about docker and dockerfile.

$ docker build -t stw-aws-ecs-microservices-uno-service .

!! Important !! If your use apple chip m1 or m2 use this command instead:

$ docker buildx build --platform=linux/amd64 -t stw-aws-ecs-microservices-due-service . 

… and now let’s tag our image (remember to substitute YOUR SERVICE NAME, YOUR ACCOUNT ID, and YOUR REGION):

$ docker tag stw-aws-ecs-microservices-<YOUR SERVICE NAME>-service <YOUR ACCOUNT ID>.dkr.ecr.<YOUR REGION>.amazonaws.com/stw-aws-ecs-microservices-<YOUR SERVICE NAME>-service:v1

Docker image upload

Now you’ve created the service image in your local machine, the next step is to upload it on AWS ECR.

First, the login:

$ aws ecr get-login-password --region <YOUR REGION> | docker login --username AWS --password-stdin <YOUR ACCOUNT ID>.dkr.ecr.<YOUR REGION>.amazonaws.com

Maybe I could be a little tedious but remember to substitute YOUR SERVICE NAME, YOUR ACCOUNT ID, and YOUR REGION. 🥱
If you need a review of how AWS ECR works, check this doc.
Ready to upload our image!!

$ docker push <YOUR ACCOUNT ID>.dkr.ecr.<YOUR REGION>.amazonaws.com/stw-aws-ecs-microservices-<YOUR SERVICE NAME>-service:v1

If everything has worked with success you should see your image on AWS ECR

Service Deployment

Create the Task Definition:

  Task:
Type: AWS::ECS::TaskDefinition
Properties:
Family: stw-aws-ecs-microservices
Cpu: 256
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
Fn::ImportValue:
!Sub "${ClusterStackName}-ECSTaskExecutionRole"
ContainerDefinitions:
- Name: !Sub ${ServiceName}-service
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/stw-aws-ecs-microservices-${ServiceName}-service:v1
Cpu: 256
Memory: 512
PortMappings:
- ContainerPort: 4567
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: 'stw-aws-ecs-microservices-lg'
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Sub ${ServiceName}-service
Environment:
- Name: LOADBALANCER_URL
Value:
Fn::ImportValue:
!Sub "${ClusterStackName}-LoadBalancerDNS"
- Name: SERVICE_NAME
Value: !Ref ServiceName

Here we can find the repo from where to download the docker image, the port mapping, CPU and memory allocation, Env variable. Check this link for a complete overview of all the parameters you can specify in an ECS task definition.

Next comes the ECS Service. Inside this resource is specified in what vpc and subnet run tasks, how many tasks run, and what is the previously created load balancer.
Together with the ECS Service has also been defined the Load Balancer Listener and Target Group for this specific service (for every new service we will create a new load balancer listener and target group). Listener and Target Group are linked to the previously created Load Balancer by using the ImportValue function and the outputs added in the Load Balancer stack.

  Service:
Type: AWS::ECS::Service
DependsOn: ListenerRule
Properties:
ServiceName: !Sub ${ServiceName}-service
TaskDefinition: !Ref Task
Cluster:
Fn::ImportValue:
!Sub "${ClusterStackName}-Cluster"
LaunchType: FARGATE
DesiredCount: 2
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 70
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- Fn::ImportValue:
!Sub "${VPCStackName}-SubnetPrivate1-Id"
- Fn::ImportValue:
!Sub "${VPCStackName}-SubnetPrivate2-Id"
SecurityGroups:
- Fn::ImportValue:
!Sub "${ClusterStackName}-ContainerSecurityGroup"
LoadBalancers:
- ContainerName: !Sub ${ServiceName}-service
ContainerPort: 4567
TargetGroupArn: !Ref TargetGroup

TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${ServiceName}-tg
VpcId:
Fn::ImportValue:
!Sub "${VPCStackName}-Vpc-Id"
Port: 80
Protocol: HTTP
Matcher:
HttpCode: 200-299
HealthCheckIntervalSeconds: 10
HealthCheckPath: /stat
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 10
TargetType: ip

ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn:
Fn::ImportValue:
!Sub "${ClusterStackName}-Listener"
Priority: !Ref Priority
Conditions:
- Field: path-pattern
Values:
- !Sub /${ServiceName}*
Actions:
- TargetGroupArn: !Ref TargetGroup
Type: forward

Let’s proceed with the deployment:

aws cloudformation create-stack --stack-name cf-infra-aws-ecs-microservices-<YOUR SERVICE NAME>-service-stack --template-body file://./infra/service-stack.yml \
--parameters ParameterKey=VPCStackName,ParameterValue=cf-infra-aws-ecs-microservices-vpc-stack \
ParameterKey=ClusterStackName,ParameterValue=cf-infra-aws-ecs-microservices-cluster-stack \
ParameterKey=ServiceName,ParameterValue=<YOUR SERVICE NAME> \
ParameterKey=Priority,ParameterValue=<YOUR SERVICE PRIORITY>

Inside the ECS dashboard, you should see the following situation, two containers are running inside the cluster ‘stw-aws-ecs-microservices’:

Second Microservices called ‘DUE’

You can do the same procedure in order to deploy every service you want.

For example, let’s make a little change in the service code and deploy it with the name ‘due’.

const express = require('express');
const axios = require('axios');

const app = express();

const serviceName = process.env.SERVICE_NAME
const loadBalancerUrl = process.env.LOADBALANCER_URL

app.get('/', async (req, res) => {
return res.send({ error: false, v: 5 });
});

app.get(`/${serviceName}/cities`, async (req, res) => {
const response = await axios.get('http://' + loadBalancerUrl + '/uno/cities')
console.log('log:', response)
return res.send({
error: false,
v: 5,
topic: 'cities',
form: serviceName,
unoResponse: JSON.stringify(response.data),
});
});

app.get(`/${serviceName}/books`, async (req, res) => {
const response = await axios.get('http://' + loadBalancerUrl + '/uno/books')
console.log('log:', response)
return res.send({
error: false,
v: 5,
topic: 'books',
form: serviceName,
unoResponse: JSON.stringify(response.data),
});
});

app.get('/stat', async (req, res) => {
return res.send({ error: false });
});


const server = app.listen(process.env.PORT || 4567);

We added the following lines:

const response = await axios.get('http://' + loadBalancerUrl + '/uno/books')
console.log('log:', response)

It has been added an HTTP request to the first service deployed (‘uno’); the aim is to test the communication between services through the Load Balancer (in this situation the Load Balancer act as a Discovery Service).

After the code has been modified you can proceed exactly like the first microservice.

This should be the status of the cluster:

4 Services are in running status 2 ‘uno’ and 2 ‘due’.

Testing our infrastructure

It’s time for testing our infrastructure. Unfortunately, we cannot make requests to the internal LoadBalancer because is only accessible from inside the vpc. You have two options:
— create an EC2 in the public subnet, ssh into it, and use curl to make test requests… boring…👎
— follow this link that will show how to create an HTTP API gateway with the vpc link and integration for making requests from your local machine.

Cleaning everything

What we have just deployed are resources that you charge for the running time, so remember to destroy them if you don’t use them.

$ aws cloudformation delete-stack --stack-name cf-infra-aws-ecs-microservices-<YOUR SERVICE NAME>-service-stack 

This command will destroy the stack with the task definition, service, etc…

$ aws cloudformation delete-stack --stack-name cf-infra-aws-ecs-microservices-ecr-<YOUR SERVICE NAME>-repos-stack 

This command will destroy the AWS ECR Repository.
NOTE: The stack won’t be deleted if images are still in it, images must be deleted manually before sending the delete-stack command.

If you have questions feel free to send me a message,
Bye 😘

--

--