Running a cluster of machines can be hard. Fargate removes the need to manage instances on which to run containers. In this article, I demonstrate how to create a service running on Amazon’s Elastic Container Service with Fargate using CloudFormation.
Fargate removes the need to think about running a container on a machine and instead leaves the user to only need to think about running applications on the container level. A lot more knowledge and understanding is required to manage your own instances; check out our previous on Auto Scaling with a SpotFleet Cluster if you’re interested in learning more about this.
Photo by Sam Solomon on Unsplash*
Architecture
For this example, I am going to create a Dockerised Python web server and deploy it to an ECS cluster which auto scales the number of Fargate containers. An Application Load Balancer (ALB) will be used to create an API which load balances the containers running the service.
The service
The service is an asynchronous Python web server running on port 5000 with CORS enabled. Note that the healthcheck endpoint is required for ECS to keep track of the service.
from aiohttp import web
import aiohttp_cors
import json
async def healthcheck(_):
headers = {
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
}
return web.Response(text=json.dumps("Healthy"), headers=headers, status=200)
async def helloworld(_):
return web.Response(text="<h1>HELLO WORLD</h1>", content_type='text/html', status=200)
app = web.Application()
cors = aiohttp_cors.setup(app)
app.router.add_get("/healthcheck", healthcheck)
app.router.add_get("/", helloworld)
cors = aiohttp_cors.setup(app, defaults={
"*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
allow_headers="*",
)
})
# Configure CORS on all routes.
for route in list(app.router.routes()):
cors.add(route)
if __name__ == "__main__":
print("Starting service")
web.run_app(app, host="0.0.0.0", port=(5000))
Deployment
Using AWS CLI to deploy CloudFormation is as simple as:
`aws cloudformation create-stack --stack-name service --template-body file://template.yml --capabilities CAPABILITY_NAMED_IAM`
The deployment is split into four templates:
Let’s Build! 🔩
VPC
I am building this service inside a VPC described in a previous article
Virtual Private Cloud on AWS — Quickstart with CloudFormation
*A Virtual Private Cloud is the foundation from which to build a new system. In this article, I demonstrate how to…*medium.com
It’s pretty standard. There are three public and three private (hybrid) subnets.
Load Balancer
The service requires a public facing load balancer which distributes HTTP requests to the machines running the web server.
LoadBalancerSecGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Load balancer only allow http port traffic
VpcId: !ImportValue VPCID
SecurityGroupIngress:
CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: TCP
ToPort: 80
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
SecurityGroups:
- !Ref LoadBalancerSecGroup
Subnets:
- !ImportValue PublicSubnetA
- !ImportValue PublicSubnetB
Cluster
The cluster orchestrates containers running on the machines. If you are unfamiliar with Docker, check out this article. Dockerising the Python web server can be done in few lines:
FROM python:3.7-slim
COPY requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
COPY src /app
EXPOSE 5000
CMD python app.py
In 2020, AWS introduced Capacity Providers to ECS, this includes Spot Fargate, which are a fraction of the price of standard Fargate containers.
The following template configures an ECS cluster using Fargate Spot, and ECR to store the Docker image of the Python web server:
ECSCluster:
Type: 'AWS::ECS::Cluster'
Properties:
ClusterName: ECSCluster
CapacityProviders:
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE_SPOT
Weight: 1
ClusterSettings:
- Name: containerInsights
Value: enabled
ECRRepository:
Type: AWS::ECR::Repository
Service
I’ve configured the load balancer to listen on port 80 for HTTP requests and send them to a Target Group — a reference we can use when defining the service to access traffic.
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 5000
Protocol: HTTP
VpcId: !ImportValue VPCID
HealthCheckIntervalSeconds: 60
HealthCheckTimeoutSeconds: 5
UnhealthyThresholdCount: 5
HealthCheckPath: /healthcheck
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 2
LoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !ImportValue LoadBalancerArn
Port: 80
Protocol: HTTP
ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
Conditions:
- Field: path-pattern
Values:
- '*'
ListenerArn: !Ref LoadBalancerListener
Priority: 1
ECS runs the Task Definition as a persistent service using the web server image in ECR. It’s as simple as defining it as Fargate.
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: AppTaskDefinition
TaskRoleArn: !GetAtt TaskRole.Arn
NetworkMode: awsvpc
ExecutionRoleArn: !Ref ExecutionRole
RequiresCompatibilities:
- FARGATE
Memory: 0.5Gb
Cpu: 256
ContainerDefinitions:
- Name: ServiceContainer
PortMappings:
- ContainerPort: 5000
Essential: true
Image: <your_image>
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: ecs
Service:
Type: AWS::ECS::Service
DependsOn:
- ListenerRule
Properties:
Cluster: !ImportValue ECSCluster
LaunchType: FARGATE
DesiredCount: 1
LoadBalancers:
- ContainerName: ServiceContainer
ContainerPort: 5000
TargetGroupArn: !Ref TargetGroup
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
HealthCheckGracePeriodSeconds: 30
TaskDefinition: !Ref TaskDefinition
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !ImportValue PublicSubnetA
- !ImportValue PublicSubnetB
SecurityGroups:
- !Ref ContainerSecurityGroup
Configuring an auto-scaling policy on the containers works in much the same way as the EC2 machines as they have defined memory and CPU so can be scaled based on those metrics, too:
AutoScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 3
MinCapacity: 2
ResourceId: !Join ["/", [service, !ImportValue ECSCluster, !GetAtt Service.Name]]
RoleARN: !ImportValue ECSServiceAutoScalingRoleArn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
AutoScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: ServiceAutoScalingPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref AutoScalingTarget
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
ScaleInCooldown: 10
ScaleOutCooldown: 10
TargetValue: 70
Usage
After a successful deployment, it is possible to access the DNS name of the ALB in the EC2 section of the AWS console which should look something like:
loadb-LoadB-R7RVQD09YC9O-1401336014.eu-west-1.elb.amazonaws.com
I am now able to view the response inside Post Man:
Use a Domain Name
loadb-LoadB-R7RVQD09YC9O-1401336014.eu-west-1.elb.amazonaws.com
But it’s quite simple to use a custom domain using AWS. You must first transfer your DNS management to Route 53 and then create a new record set which is aliased to the load balancer.
Thanks For Reading
I hope you have enjoyed this article. If you like the style, check out T3chFlicks.org for more tech focused educational content (YouTube, Instagram, Facebook, Twitter).
Resources:
Top comments (0)