Split web traffic between Amazon EC2 and AWS Fargate
CloudFormation example of how to setup an Application Load Balancer that distributes web traffic across an ECS service running on both EC2 and Fargate.
Nathan Peck
Senior Developer Advocate at AWS
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.
constaxios=require('axios');constexpress=require('express');constapp=express();constport=process.env.PORT||80;constmetadataUrl=`${process.env.ECS_CONTAINER_METADATA_URI_V4}/task`;app.get('*',asyncfunction(req,res){constmetadataResponse=awaitaxios.get(metadataUrl);constformattedResponse=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();});
File: package.jsonLanguage: json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{"name":"ecs-metadata","version":"1.0.0","description":"Simple microservice that echoes the ECS metadata","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1","start":"node index.js"},"author":"","license":"MIT","dependencies":{"express":"^4.16.3","axios":"1.6.0"}}
# Build stage, includings NPM and tools necessary for the buildFROMpublic.ecr.aws/docker/library/node:20asbuildWORKDIR/srv# Install dependencies based on the `package.json` and `package-lock.json`# files in the host folderRUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev# Production stage, only includes what is needed for productionFROMpublic.ecr.aws/docker/library/node:20-slimENV NODE_ENV productionUSERnodeCOPY --from=build /srv .ADD . .# Specify the command to run when launching the containerEXPOSE3000CMD node index.js
You should have the following folder structure:
app - Folder containing the application code
app/index.js - The actual code for the sample application
app/package.json - A manifest files that lists some open source packages from NPM that the application depends on
app/Dockerfile - Instructions on how to build the application and package it up into a container image.
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 bothAmazon EC2 and AWS FargateParameters:ImageURI:Type:StringDescription:The URI of the image to deployCluster:Type:StringDescription:The name of the ECS cluster to deploy intoEc2CapacityProvider:Type:StringDescription:The name of an EC2 capacity provider in the cluster.ServiceName:Type:StringDefault:ecs-metadataDescription:Name of the serviceVpcId:Type:AWS::EC2::VPC::IdDescription:The virtual private network into which to launch all resourcesSubnetIds:Type:List<AWS::EC2::Subnet::Id>Description:List of subnet IDs where the EC2 instances will be launchedResources:# This task definition has settings which allow it to# be used on both AWS Fargate and Amazon EC2 capacitySampleTaskDefinition:Type:AWS::ECS::TaskDefinitionProperties:Family:ecs-metadataRequiresCompatibilities:- EC2- FARGATEExecutionRoleArn:!GetAtt TaskExecutionRole.ArnNetworkMode:awsvpcCpu:256Memory:512ContainerDefinitions:- Name:ecs-metadataImage:!Ref ImageURIPortMappings:- ContainerPort:3000Environment:- Name:PORTValue:3000LogConfiguration:LogDriver:'awslogs'Options:mode:non-blockingmax-buffer-size:25mawslogs-group:!Ref LogGroupawslogs-region:!Ref AWS::Regionawslogs-stream-prefix:!Ref ServiceName# Deploy the task definition as a service on EC2 capacityEc2Service:Type:AWS::ECS::ServiceProperties:ServiceName:!Sub '${ServiceName}-on-ec2'Cluster:!Ref 'Cluster'DeploymentConfiguration:MaximumPercent:200MinimumHealthyPercent:75CapacityProviderStrategy:- Base:0CapacityProvider:!Ref Ec2CapacityProviderWeight:1NetworkConfiguration:AwsvpcConfiguration:SecurityGroups:- !Ref ServiceSecurityGroupSubnets:- !Select [ 0, !Ref SubnetIds ]- !Select [ 1, !Ref SubnetIds ]DesiredCount:1TaskDefinition:!Ref 'SampleTaskDefinition'LoadBalancers:- ContainerName:ecs-metadataContainerPort:80TargetGroupArn:!Ref Ec2TargetGroup# Deploy the task definition as a service on AWS Fargate capacityFargateService:Type:AWS::ECS::ServiceProperties:ServiceName:!Sub '${ServiceName}-on-fargate'Cluster:!Ref 'Cluster'LaunchType:FARGATEDeploymentConfiguration:MaximumPercent:200MinimumHealthyPercent:75NetworkConfiguration:AwsvpcConfiguration:AssignPublicIp:ENABLEDSecurityGroups:- !Ref ServiceSecurityGroupSubnets:- !Select [ 0, !Ref SubnetIds ]- !Select [ 1, !Ref SubnetIds ]DesiredCount:1TaskDefinition:!Ref 'SampleTaskDefinition'LoadBalancers:- ContainerName:ecs-metadataContainerPort:80TargetGroupArn:!Ref FargateTargetGroup# Keeps track of the list of tasks running on EC2 instancesEc2TargetGroup:Type:AWS::ElasticLoadBalancingV2::TargetGroupProperties:HealthCheckIntervalSeconds:6HealthCheckPath:/HealthCheckProtocol:HTTPHealthCheckTimeoutSeconds:5HealthyThresholdCount:2TargetType:ipName:'ecs-metdata-on-ec2'Port:3000Protocol:HTTPUnhealthyThresholdCount:2VpcId:!Ref VpcId# Keeps track of the list of tasks running in AWS FargateFargateTargetGroup:Type:AWS::ElasticLoadBalancingV2::TargetGroupProperties:HealthCheckIntervalSeconds:6HealthCheckPath:/HealthCheckProtocol:HTTPHealthCheckTimeoutSeconds:5HealthyThresholdCount:2TargetType:ipName:'ecs-metadata-on-fargate'Port:3000Protocol:HTTPUnhealthyThresholdCount:2VpcId:!Ref VpcId# A public facing load balancer, this is used for accepting traffic from the public# internetPublicLoadBalancerSG:Type:AWS::EC2::SecurityGroupProperties:GroupDescription:Access to the public facing load balancerVpcId:!Ref VpcIdSecurityGroupIngress:# Allow access to ALB from anywhere on the internet- CidrIp:0.0.0.0/0IpProtocol:-1PublicLoadBalancer:Type:AWS::ElasticLoadBalancingV2::LoadBalancerProperties:Scheme:internet-facingLoadBalancerAttributes:- Key:idle_timeout.timeout_secondsValue:'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 PublicLoadBalancerSGPublicLoadBalancerListener:Type:AWS::ElasticLoadBalancingV2::ListenerProperties: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 twoForwardConfig:TargetGroups:- TargetGroupArn:!Ref Ec2TargetGroupWeight:50- TargetGroupArn:!Ref FargateTargetGroupWeight:50LoadBalancerArn:!Ref 'PublicLoadBalancer'Port:80Protocol:HTTP# Security group that limits network access# to the taskServiceSecurityGroup:Type:AWS::EC2::SecurityGroupProperties:GroupDescription:Security group for serviceVpcId:!Ref VpcId# The services' security group allows inbound# traffic from the public facing ALBServiceIngressFromPublicALB:Type:AWS::EC2::SecurityGroupIngressProperties:Description:Ingress from the public ALBGroupId:!Ref 'ServiceSecurityGroup'IpProtocol:-1SourceSecurityGroupId:!Ref 'PublicLoadBalancerSG'# This role is used to setup the execution environment for# the task, in this case to connect to the Elastic File SystemTaskExecutionRole:Type:AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Statement:- Effect:AllowPrincipal: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::AccountIdManagedPolicyArns:- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy# This log group stores the stdout logs from this service's containersLogGroup: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.
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 CloudFormationaws cloudformation delete-stack --stack-name service-across-ec2-and-fargate
# Empty and delete the Amazon ECR container registry we createdaws ecr delete-repository --repository-name sample-app-repo --force
You should also delete the container image that you uploaded to Amazon ECR.
🎓
New Workshop Series!
Join our upcoming container workshop series and learn best practices for Amazon ECS, AWS Fargate, and more.