Split web traffic between Amazon EC2 and AWS Fargate
About
Amazon ECS can orchestrate your application across a range of different capacity types. In this pattern you will learn how to use Amazon ECS to setup an Application Load Balancer that distributes traffic across both Amazon EC2 capacity, and AWS Fargate capacity.
The following diagram shows what will be deployed:
Build the sample application
The Node.js sample application grabs information from the ECS Task Metadata endpoint, and returns it to the requester on port 80.
Create the following three files:
- app/index.js
- app/package.json
- app/Dockerfile
const axios = require('axios');
const express = require('express');
const app = express();
const port = process.env.PORT || 80;
const metadataUrl = `${process.env.ECS_CONTAINER_METADATA_URI_V4}/task`;
app.get('*', async function (req, res) {
const metadataResponse = await axios.get(metadataUrl);
const formattedResponse = JSON.stringify(metadataResponse.data, null, 2)
res.send(`<pre>
Running on: ${metadataResponse.data.LaunchType}
DNS: ${metadataResponse.data.Containers[0].Networks[0].PrivateDNSName}
AvailabilityZone: ${metadataResponse.data.AvailabilityZone}
</pre>
<br />
<br />
<pre style='height: 400px; overflow: scroll'>${formattedResponse}</pre>`);
});
app.listen(port, () => console.log(`Listening on port ${port}!`));
// This causes the process to respond to "docker stop" faster
process.on('SIGTERM', function () {
console.log('Received SIGTERM, shutting down');
app.close();
});
You should have the following folder structure:
app
- Folder containing the application codeapp/index.js
- The actual code for the sample applicationapp/package.json
- A manifest files that lists some open source packages from NPM that the application depends onapp/Dockerfile
- Instructions on how to build the application and package it up into a container image.
Now you can build and push the image to ECR using command like this (substitute your own ECR private repository URL):
REPO_URI=$(aws ecr create-repository --repository-name sample-app-repo --query 'repository.repositoryUri' --output text)
if [ -z "${REPO_URI}" ]; then
REPO_URI=$(aws ecr describe-repositories --repository-names sample-app-repo --query 'repositories[0].repositoryUri' --output text)
fi
docker build -t ${REPO_URI}:ecs-metadata ./app
docker push ${REPO_URI}:ecs-metadata
Deploy an ECS cluster and environment
In order to run this sample template you will need an ECS cluster with an EC2 capacity provider attached to it. You can follow the EC2 capacity provider pattern to get an example CloudFormation template that will deploy the cluster and the capacity provider.
Deploy the sample application
The following template will deploy the sample ecs-metadata
application (or any other image that you pass to it). The image will be deployed twice: once on EC2 and one of AWS Fargate. Finally an Application Load Balancer is provisioned which sends 50% of the traffic to the EC2 service, and 50% of the traffic to the AWS Fargate service.
AWSTemplateFormatVersion: '2010-09-09'
Description: An example task definition that can deployed onto both
Amazon EC2 and AWS Fargate
Parameters:
ImageURI:
Type: String
Description: The URI of the image to deploy
Cluster:
Type: String
Description: The name of the ECS cluster to deploy into
Ec2CapacityProvider:
Type: String
Description: The name of an EC2 capacity provider in the cluster.
ServiceName:
Type: String
Default: ecs-metadata
Description: Name of the service
VpcId:
Type: AWS::EC2::VPC::Id
Description: The virtual private network into which to launch all resources
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: List of subnet IDs where the EC2 instances will be launched
Resources:
# This task definition has settings which allow it to
# be used on both AWS Fargate and Amazon EC2 capacity
SampleTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: ecs-metadata
RequiresCompatibilities:
- EC2
- FARGATE
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
NetworkMode: awsvpc
Cpu: 256
Memory: 512
ContainerDefinitions:
- Name: ecs-metadata
Image: !Ref ImageURI
PortMappings:
- ContainerPort: 3000
Environment:
- Name: PORT
Value: 3000
LogConfiguration:
LogDriver: 'awslogs'
Options:
mode: non-blocking
max-buffer-size: 25m
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref ServiceName
# Deploy the task definition as a service on EC2 capacity
Ec2Service:
Type: AWS::ECS::Service
Properties:
ServiceName: !Sub '${ServiceName}-on-ec2'
Cluster: !Ref 'Cluster'
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 75
CapacityProviderStrategy:
- Base: 0
CapacityProvider: !Ref Ec2CapacityProvider
Weight: 1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ServiceSecurityGroup
Subnets:
- !Select [ 0, !Ref SubnetIds ]
- !Select [ 1, !Ref SubnetIds ]
DesiredCount: 1
TaskDefinition: !Ref 'SampleTaskDefinition'
LoadBalancers:
- ContainerName: ecs-metadata
ContainerPort: 80
TargetGroupArn: !Ref Ec2TargetGroup
# Deploy the task definition as a service on AWS Fargate capacity
FargateService:
Type: AWS::ECS::Service
Properties:
ServiceName: !Sub '${ServiceName}-on-fargate'
Cluster: !Ref 'Cluster'
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 75
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref ServiceSecurityGroup
Subnets:
- !Select [ 0, !Ref SubnetIds ]
- !Select [ 1, !Ref SubnetIds ]
DesiredCount: 1
TaskDefinition: !Ref 'SampleTaskDefinition'
LoadBalancers:
- ContainerName: ecs-metadata
ContainerPort: 80
TargetGroupArn: !Ref FargateTargetGroup
# Keeps track of the list of tasks running on EC2 instances
Ec2TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-metdata-on-ec2'
Port: 3000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref VpcId
# Keeps track of the list of tasks running in AWS Fargate
FargateTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: 'ecs-metadata-on-fargate'
Port: 3000
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref VpcId
# A public facing load balancer, this is used for accepting traffic from the public
# internet
PublicLoadBalancerSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref VpcId
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: '30'
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
- !Select [ 0, !Ref SubnetIds ]
- !Select [ 1, !Ref SubnetIds ]
SecurityGroups:
- !Ref PublicLoadBalancerSG
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: 'forward'
# Evenly split traffic across the app on EC2 and the app on Fargate
# Can adjust weights as needed to balance traffic between the two
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref Ec2TargetGroup
Weight: 50
- TargetGroupArn: !Ref FargateTargetGroup
Weight: 50
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
# Security group that limits network access
# to the task
ServiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for service
VpcId: !Ref VpcId
# The services' security group allows inbound
# traffic from the public facing ALB
ServiceIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ServiceSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG'
# This role is used to setup the execution environment for
# the task, in this case to connect to the Elastic File System
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Condition:
ArnLike:
aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# This log group stores the stdout logs from this service's containers
LogGroup:
Type: AWS::Logs::LogGroup
The template requires the following input parameters:
ImageURI
- The URI of the image to deploy. This should match the image that you built and pushed above.Cluster
- The name of an ECS cluster on this account. This cluster should have EC2 capacity available in it. All ECS clusters come with AWS Fargate support already built-in. For an example of how to deploy an ECS cluster with EC2 capacity there is a pattern for an ECS cluster using a EC2 capacity provider.Ec2CapacityProvider
- The name of an EC2 capacity provider on this cluster. Again see the ECS cluster with EC2 capacity provider pattern.VpcId
- A virtual private cloud ID. This can be the default VPC that comes with your AWS account. Example:vpc-79508710
SubnetIds
- A comma separated list of subnets from the VPC. Example:subnet-b4676dfe,subnet-c71ebfae
Deploy this template with a command like this:
aws cloudformation deploy \
--template-file service-across-ec2-and-fargate.yml \
--stack-name service-across-ec2-and-fargate \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
ImageURI=${REPO_URI}:ecs-metadata \
Cluster=capacity-provider-environment-BaseStack-18PANC6K9E7D8-ECSCluster-NNBNpIh5AkZO \
Ec2CapacityProvider=capacity-provider-environment-BaseStack-18PANC6K9E7D8-CapacityProvider-FI323ISAaRbn \
VpcId=vpc-79508710 \
SubnetIds=subnet-b4676dfe,subnet-c71ebfae
Next Steps
The services will initially deploy with only one of each task: one task on EC2 and one task on AWS Fargate. Try scaling up both services to launch additional tasks.
Note that the Weight
option inside of the AWS::ElasticLoadBalancingV2::Listener
forwarding configuration is controlling the balance of how traffic is distributed across the EC2 and Fargate versions of the service. It assumes an even 50% distribution to both.
You can also choose to scale up the AWS Fargate service higher than the Amazon EC2 service, and adjust the balance of traffic to send more traffic to the AWS Fargate version of the service.
Test out sending traffic to the single endpoint for the application. You should see response that look something like these samples:
Running on: EC2
DNS: ip-172-31-4-140.us-east-2.compute.internal
AvailabilityZone: us-east-2a
Running on: FARGATE
DNS: ip-172-31-41-78.us-east-2.compute.internal
AvailabilityZone: us-east-2c
By reloading the endpoint a few times you will see it flip back and forth between EC2
and FARGATE
as the load balancer distributes traffic evenly across both instances of the service.
Tear it down
You can use the following command to tear down the stack and delete the services:
# Tear down the CloudFormation
aws cloudformation delete-stack --stack-name service-across-ec2-and-fargate
# Empty and delete the Amazon ECR container registry we created
aws ecr delete-repository --repository-name sample-app-repo --force
You should also delete the container image that you uploaded to Amazon ECR.