Split web traffic between Amazon EC2 and AWS Fargate

Nathan Peck profile picture
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:

Application Load BalancerAmazon Elastic Compute Cloud (Amazon EC2)AWS Fargate50%50%

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
File: index.jsLanguage: js
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 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.

Now you can build and push the image to ECR using command like this (substitute your own ECR private repository URL):

Language: shell
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.

File: service-across-ec2-and-fargate.ymlLanguage: yml
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:

Language: sh
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:

Language: txt
Running on: EC2
DNS: ip-172-31-4-140.us-east-2.compute.internal
AvailabilityZone: us-east-2a
Language: txt
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:

Language: shell
# 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.