Microservices architecture deployed on ECS Fargate-based Cluster— Using CloudFormation
When I first tried to deploy microservices on ECS, exposing some of them to the internet and creating internal communications between them, I found it quite challenging.
This article is a step-by-step guide to help you deploy a microservices architecture and establish internal communications within an ECS
Fargate-based cluster using CloudFormation.
Requirements
- Basic knowledge of AWS services
- Basic knowledge of Docker and containerization
- Have you used or are willing to use CloudFormation
Architecture
To address the concern of communications between containers, AWS has provided a few options such as service discovery, internal load balancer, or service mesh.
In this article, I chose to solve the issue using an internal load balancer.
The global architecture is as follows.
Architecture explanation
As shown in the diagram, our architecture has 2 availability zones each containing a public subnet and a private subnet. In the public subnets, we have deployed the NAT Gateways and the internet-facing ALB to expose the frontend containers. The role of the NAT gateway is to allow components deployed in the private subnet to reach the internet without being reached or exposed.
In the private subnets, for security purposes, we have instantiated the backend containers and the frontend containers using services and Fargate.
The front-end service is exposed to the internet thanks to the internet-facing ALB + Internet Gateway. The backend service, however, is hidden and can be reached only by the frontend component. One thing to note here is that I could have deployed the front-end service in the public subnet, but it’s always safer to have your elements in the private subnet and expose them publicly using a load balancer or any similar service.
The communication between the two services Frontend -> backend happens with REST endpoints through the internal load balancer that routes traffic to the backend containers.
Example Scenario
- The user reaches the frontend service through the ALB URL.
- The frontend service calls the backend API “/api” to check connectivity with the backend service
- If the communication is successful we get a response
'request successful'
Code deployed in the containers
The frontend and backend containers are two simple express apps
Frontend code
const express = require('express')
const app = express()
const port = 5000
const requestify = require('requestify');
require('dotenv').config();
app.get('/', (req, res) => {
res.send('Hello from frontend!')
});
app.get('/api', (req, res) => {
requestify.get(process.env.backendurl)
.then(function(response) {
res.json('request successful')
}
);
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
As shown in the code, the frontend app has two endpoints, the default one is for accessing the app and the second one is to request the backend and check if the communication is established.
Backend code
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello again!')
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
The backend app has one single endpoint, the default one, just a way to reach it.
Push applications docker images to ECR
For this article, I am using Amazon Elastic Container Registry (ECR) as the docker container registry for the images used by the frontend and backend services.
Dockerfile
Since the backend app and frontend app are two express apps, we can use the same Dockerfile template and simply adjust the port exposure.
FROM node:16
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install
# If you are building your code for production
# Bundle app source
COPY . .
#I use the port 3000 to expose the backend app and the port 5000 for the frontend app
#You can use the ports that you want
EXPOSE 3000
CMD [ "node", "index.js" ]
Creating ECR repository
AWSTemplateFormatVersion: '2010-09-09'
Description: "Creating ecr repository"
Resources:
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: "test-ecs-repository"
Once you create the repository, you can build the docker images and push them to the ECR repository
Building the Docker images
docker build -t frontendapp <frontendapp_dockerfile_path>
docker build -t backendapp <backendapp_dockerfile_path>
Tagging the Docker images
Since I am going to use the same ECR repository for the two apps, tagging is then important so we can select the right image
docker tag backendapp:latest <your AWS account no>.dkr.ecr.<region>.amazonaws.com/test-ecs-repository:backend-app
docker tag frontendapp:latest <your AWS account no>.dkr.ecr.<region>.amazonaws.com/test-ecs-repository:frontend-app
Push the docker images
docker push <your AWS account no>.dkr.ecr.<region>.amazonaws.com/test-ecs-repository:frontend-app
docker push <your AWS account no>.dkr.ecr.<region>.amazonaws.com/test-ecs-repository:backend-app
CloudFormation Setup
- Setup VPC, Public/Private subnets, Nat Gateways, Internet Gateway, and routing tables
- Setup internet-facing load balancer, internal load balancer, and listeners
- Setup ECS cluster, ECS role, ECS services, and register with load balancers
- Complete code
- Deploy the CloudFormation code
- Checking results
Setup VPC, Nat Gateway, Internet Gateway, and routing tables
AWSTemplateFormatVersion: 2010-09-09
Description: Custom VPC Creation With Private and Public Subnet
Parameters:
VPCName:
Description: CIDR range for our VPC
Type: String
Default: DemoCustomVPC
VPCCidr:
Description: CIDR range for our VPC
Type: String
Default: 10.0.0.0/16
PrivateSubnetACidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.0.0/24
PrivateSubnetBCidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.1.0/24
PublicSubnetACidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.2.0/24
PublicSubnetBCidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.3.0/24
#choose whatever availability that suits you
AvailabilityZoneA:
Description: Avaibalbility Zone 1
Type: String
Default: eu-west-3a
AvailabilityZoneB:
Description: Avaibalbility Zone 2
Type: String
Default: eu-west-3b
Resources:
DemoVPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !Ref VPCCidr
Tags:
-
Key: Name
Value: !Ref VPCName
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PrivateSubnetACidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetA'
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PrivateSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetB'
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PublicSubnetACidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetA'
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PublicSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetB'
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref DemoVPC
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PublicSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
ElasticIPA:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
ElasticIPB:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGatewayA:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPA.AllocationId
SubnetId: !Ref PublicSubnetA
NATGatewayB:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPB.AllocationId
SubnetId: !Ref PublicSubnetB
PrivateRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteTableB:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteToInternetA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableA
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayA
PrivateRouteToInternetB:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableB
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayB
PrivateSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateRouteTableA
PrivateSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetB
RouteTableId: !Ref PrivateRouteTableB
Setup internet-facing load balancer, internal load balancer, and listeners
....
#Previous code
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetA
- !Ref PublicSubnetB
SecurityGroups:
- !Ref 'PublicLoadBalancerSecurityGroup'
PublicLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'DemoVPC'
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
internalLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internal
Subnets:
# The load balancer is placed into the private subnets
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroups:
- !Ref 'PrivateLoadBalancerSecurityGroup'
PrivateLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'DemoVPC'
#Allowing access to the internal loadbalancer only from the security group
# of the frontend containers
PrivateLoadBalancerSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Allowing access to the private ALB only from frontend containers SG
GroupId: !Ref PrivateLoadBalancerSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref ECSSecurityGroup
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
DummyTargetGroupPrivate:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
PrivateLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPrivate'
Type: 'forward'
LoadBalancerArn: !Ref 'internalLoadBalancer'
Port: 80
Protocol: HTTP
Setup ECS cluster, ECS role, ECS services, and register with load balancers
#Role that grants the Amazon ECS container and Fargate agents permission to make AWS API calls on your behalf
#The permissions treat the majority of usecases
ECSRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: ecs-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ecr:GetAuthorizationToken"
- "ecr:BatchCheckLayerAvailability"
- "ecr:GetDownloadUrlForLayer"
- "ecr:BatchGetImage"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: '*'
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public ECS containers
VpcId: !Ref 'DemoVPC'
ECSInternalSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS private containers
VpcId: !Ref 'DemoVPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromPrivateALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the private ALB
GroupId: !Ref 'ECSInternalSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PrivateLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSInternalSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSInternalSecurityGroup'
ECSSecurityGroupIngressFromSelfPublic:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
EcsFrontTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512 #basic utilization
Memory: 1024 #basic utilization
NetworkMode: awsvpc
ExecutionRoleArn: !GetAtt ECSRole.Arn
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: "ecs-front-container"
Image: "url of your ecr image repo"
PortMappings:
- ContainerPort: 5000
Environment:
#as you saw in the code of the frontend app, we need the backendurl environment variable
#so we can reach our backend
- Name: backendurl
Value: !Sub
- "http://${lb_url}"
- lb_url: !GetAtt internalLoadBalancer.DNSName
EcsBackendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512 #basic utilization
Memory: 1024 #basic utilization
NetworkMode: awsvpc
ExecutionRoleArn: !GetAtt ECSRole.Arn
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: "ecs-backend-container"
#URL of my ecr repository change it with yours
Image: "url of your ecr image repo"
PortMappings:
- ContainerPort: 3000
#By this time, we have internet- lets launch an instance
EcsFrontService:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
Cluster: !Ref ECSCluster
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSecurityGroup
Subnets:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
TaskDefinition: !Ref EcsFrontTaskDefinition
LoadBalancers:
- ContainerName: "ecs-front-container"
ContainerPort: 5000
TargetGroupArn: !Ref 'EcsServiceTargetGroup'
EcsServiceTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-frontend-service'
Port: 5000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
# Create a rule on the load balancer for routing traffic to the target group
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'EcsServiceTargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref PublicLoadBalancerListener
Priority: 1
EcsBackendService:
Type: AWS::ECS::Service
DependsOn: LoadBalancerInternalRule
Properties:
Cluster: !Ref ECSCluster
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSInternalSecurityGroup
Subnets:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
TaskDefinition: !Ref EcsBackendTaskDefinition
LoadBalancers:
- ContainerName: "ecs-backend-container"
ContainerPort: 3000
TargetGroupArn: !Ref 'EcsServiceInternalTargetGroup'
EcsServiceInternalTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-backend-service'
Port: 3000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
# Create a rule on the load balancer for routing traffic to the target group
LoadBalancerInternalRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'EcsServiceInternalTargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref PrivateLoadBalancerListener
Priority: 1
Complete code
AWSTemplateFormatVersion: 2010-09-09
Description: Custom VPC Creation With Private and Public Subnet
Parameters:
VPCName:
Description: CIDR range for our VPC
Type: String
Default: DemoCustomVPC
VPCCidr:
Description: CIDR range for our VPC
Type: String
Default: 10.0.0.0/16
PrivateSubnetACidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.0.0/24
PrivateSubnetBCidr:
Description: Private Subnet IP Range
Type: String
Default: 10.0.1.0/24
PublicSubnetACidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.2.0/24
PublicSubnetBCidr:
Description: Public Subnet IP Range
Type: String
Default: 10.0.3.0/24
AvailabilityZoneA:
Description: Avaibalbility Zone 1
Type: String
Default: eu-west-3a
AvailabilityZoneB:
Description: Avaibalbility Zone 2
Type: String
Default: eu-west-3b
Resources:
DemoVPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !Ref VPCCidr
Tags:
-
Key: Name
Value: !Ref VPCName
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PrivateSubnetACidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetA'
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PrivateSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PrivateSubnetB'
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneA
CidrBlock: !Ref PublicSubnetACidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetA'
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref DemoVPC
AvailabilityZone: !Ref AvailabilityZoneB
CidrBlock: !Ref PublicSubnetBCidr
Tags:
-
Key: Name
Value: !Sub '${VPCName}-PublicSubnetB'
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref DemoVPC
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PublicSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
ElasticIPA:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
ElasticIPB:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGatewayA:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPA.AllocationId
SubnetId: !Ref PublicSubnetA
NATGatewayB:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIPB.AllocationId
SubnetId: !Ref PublicSubnetB
PrivateRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteTableB:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref DemoVPC
PrivateRouteToInternetA:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableA
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayA
PrivateRouteToInternetB:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTableB
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGatewayB
PrivateSubnetRouteTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateRouteTableA
PrivateSubnetRouteTableAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetB
RouteTableId: !Ref PrivateRouteTableB
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetA
- !Ref PublicSubnetB
SecurityGroups:
- !Ref 'PublicLoadBalancerSecurityGroup'
internalLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internal
Subnets:
# The load balancer is placed into the private subnets
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroups:
- !Ref 'PrivateLoadBalancerSecurityGroup'
PrivateLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'DemoVPC'
#Allowing access to the internal loadbalancer only from the security group
# of the frontend containers
PrivateLoadBalancerSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Allowing access to the private ALB only from frontend containers SG
GroupId: !Ref PrivateLoadBalancerSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref ECSSecurityGroup
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
DummyTargetGroupPrivate:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
PrivateLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPrivate'
Type: 'forward'
LoadBalancerArn: !Ref 'internalLoadBalancer'
Port: 80
Protocol: HTTP
ECSRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: ecs-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ecr:GetAuthorizationToken"
- "ecr:BatchCheckLayerAvailability"
- "ecr:GetDownloadUrlForLayer"
- "ecr:BatchGetImage"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: '*'
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public ECS containers
VpcId: !Ref 'DemoVPC'
ECSInternalSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS private containers
VpcId: !Ref 'DemoVPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromPrivateALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the private ALB
GroupId: !Ref 'ECSInternalSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PrivateLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSInternalSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSInternalSecurityGroup'
ECSSecurityGroupIngressFromSelfPublic:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
EcsFrontTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Memory: 1024
NetworkMode: awsvpc
ExecutionRoleArn: !GetAtt ECSRole.Arn
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: "ecs-front-container"
Image: "url of your image in the ecr"
PortMappings:
- ContainerPort: 5000
Environment:
- Name: url
Value: !Sub
- "http://${lb_url}"
- lb_url: !GetAtt internalLoadBalancer.DNSName
EcsBackendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 512
Memory: 1024
NetworkMode: awsvpc
ExecutionRoleArn: !GetAtt ECSRole.Arn
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: "ecs-backend-container"
Image: "url of your image in the ecr"
PortMappings:
- ContainerPort: 3000
EcsFrontService:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
Cluster: !Ref ECSCluster
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSecurityGroup
TaskDefinition: !Ref EcsFrontTaskDefinition
LoadBalancers:
- ContainerName: "ecs-front-container"
ContainerPort: 5000
TargetGroupArn: !Ref 'EcsServiceTargetGroup'
EcsServiceTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-frontend-service'
Port: 5000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
# Create a rule on the load balancer for routing traffic to the target group
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'EcsServiceTargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref PublicLoadBalancerListener
Priority: 1
EcsBackendService:
Type: AWS::ECS::Service
DependsOn: LoadBalancerInternalRule
Properties:
Cluster: !Ref ECSCluster
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSInternalSecurityGroup
Subnets:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
TaskDefinition: !Ref EcsBackendTaskDefinition
LoadBalancers:
- ContainerName: "ecs-backend-container"
ContainerPort: 3000
TargetGroupArn: !Ref 'EcsServiceInternalTargetGroup'
EcsServiceInternalTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-backend-service'
Port: 3000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref DemoVPC
# Create a rule on the load balancer for routing traffic to the target group
LoadBalancerInternalRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'EcsServiceInternalTargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref PrivateLoadBalancerListener
Priority: 1
Outputs:
VPCId:
Description: vpc id
Value: !Ref DemoVPC
PublicSubnetA:
Description: SubnetId of public subnet A
Value: !Ref PublicSubnetA
PublicSubnetB:
Description: SubnetId of public subnet B
Value: !Ref PublicSubnetB
PrivateSubnetA:
Description: SubnetId of private subnet A
Value: !Ref PrivateSubnetA
PrivateSubnetB:
Description: SubnetId of private subnet B
Value: !Ref PublicSubnetB
PublicALBDns:
Description: Dns of the public ALB
Value: !GetAtt PublicLoadBalancer.DNSName
Deploy CloudFormation code
aws cloudformation deploy --stack-name vpc-ecs-cluster --template-file vpc-ecs.yaml --s3-bucket "your s3 bucket" --capabilities CAPABILITY_IAM
One thing to note here is the “ — capabilities CAPABILITY_IAM”. Since we are creating an IAM ROLE, we must specify the — — capabilities option.
If we did everything right and the deployment of our CloudFormation code was successful, we could grab the URL of the internet-facing ALB and check the results. We could do that by checking the outputs section of our CloudFormation template.
Checking results
By clicking on the DNS name provided in the outputs we should see this page
and if we type /api we should see this response
That definitely proves that communication between the frontend and the backend is successful.
Hope this article was helpful.
Don’t forget to subscribe and click the clap 👏 button below a few times before you leave.
See you soon in the upcoming articles to continue the talks about Cloud, DevOps, and system design