Serverless public facing API hosted on AWS Fargate
About
Sometimes you want to create a public facing service, but you want stricter control over the networking of the service. This pattern is especially useful for the following usecases:
- An API which is public facing but needs an extra layer of security hardening by not even having a public IP address that an attacker could send a request directly to.
- An API service which needs to be massively horizontally scalable while not being constrained by number of IP addresses available.
- A web or API service which initiates outbound connections but to the public you want those connections to originate from a specific and limited set of IP addresses that can be whitelisted.
WARNING
This pattern is not suited for:
Services that have high throughput/bandwidth to other services on the internet. Because this pattern use a NAT gateway to provide internet to resources in a private subnet you will be sending a lot of bandwidth through the NAT gateway and paying extra per GB of outbound traffic. Instead consider using the public facing version of this pattern, which hosts public facing tasks with public IP addresses assigned by AWS Fargate
Architecture
At a high level the architecture looks like this:
Everything is deployed in an Amazon Virtual Private Cloud (VPC) which has two subnets:
- Public subnet: Has an attached internet gateway to allow resources launched in that subnet to accept connections from the internet, and initiate connections to the internet. Resources in this subnet have public IP addresses.
- Private subnet: For internal resources. AWS Fargate tasks in this subnet have no direct internet access, and only have private IP addresses that are internal to the VPC, not directly accessible by the public.
The public facing subnet hosts a couple resources:
- Public facing load balancer: Accepts inbound connections on specific ports, and forwards acceptable traffic to resources inside the private subnet.
- NAT gateway: A networking bridge to allow resources inside the private subnet to initiate outbound communications to the internet, while not allowing inbound connections.
TIP
Application Load Balancer has a constant hourly charge, which gives it a baseline cost, even if your application receives no traffic.
If you expect your API to receive very low traffic, or intermittent traffic, then you may prefer to use the API Gateway pattern for AWS Fargate. Amazon API Gateway charges per request, with no minimum fee. However, this can add up to a higher per request cost if you do receive large amounts of traffic.
Dependencies
This pattern requires that you have an AWS account, and that you use AWS Serverless Application Model (SAM) CLI. If not already installed then please install SAM CLI for your system.
Define the VPC
To deploy this pattern you will use the base pattern that defines a large VPC for an Amazon ECS cluster. This will deploy the public and private subnets as well as the NAT gateway that provides internet access to the private subnets. Download the vpc.yml
file from that pattern, but don't deploy it yet. You will deploy the VPC later as part of this pattern.
Define the cluster
The following AWS CloudFormation template creates a simple Amazon ECS cluster that is setup for serverless usage with AWS Fargate.
AWSTemplateFormatVersion: '2010-09-09'
Description: Empty ECS cluster that has no EC2 instances. It is designed
to be used with AWS Fargate serverless capacity
Resources:
# Cluster that keeps track of container deployments
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterSettings:
- Name: containerInsights
Value: enabled
# This is a role which is used within Fargate to allow the Fargate agent
# to download images, and upload logs.
ECSTaskExecutionRole:
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
Path: /
# This role enables basic features of ECS. See reference:
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECSTaskExecutionRolePolicy
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Outputs:
ClusterName:
Description: The ECS cluster into which to launch resources
Value: !Ref ECSCluster
ECSTaskExecutionRole:
Description: The role used to start up a task
Value: !Ref ECSTaskExecutionRole
Define the service
Next we need to define an Amazon ECS service that deploys a container using AWS Fargate. The following template deploys an Application Load Balancer into the VPC public subnet, while deploying the containers themselves into AWS Fargate in the private subnet:
AWSTemplateFormatVersion: '2010-09-09'
Description: An example service that deploys in AWS VPC networking mode
on AWS Fargate. Service runs with networking in private subnets
and with private IP addresses only.
Parameters:
VpcId:
Type: String
Description: The VPC that the service is running inside of
PublicSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: List of public subnet ID's to put the load balancer in
PrivateSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: List of private subnet ID's to put the tasks in
ClusterName:
Type: String
Description: The name of the ECS cluster into which to launch capacity.
ECSTaskExecutionRole:
Type: String
Description: The role used to start up an ECS task
ServiceName:
Type: String
Default: web
Description: A name for the service
ImageUrl:
Type: String
Default: public.ecr.aws/docker/library/nginx:latest
Description: The url of a docker image that contains the application process that
will handle the traffic for this service
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container
ContainerPort:
Type: Number
Default: 80
Description: What port that the application expects traffic on
DesiredCount:
Type: Number
Default: 2
Description: How many copies of the service task to run
Resources:
# The task definition. This is a simple metadata description of what
# container to run, and what resource requirements it has.
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref ServiceName
Cpu: !Ref ContainerCpu
Memory: !Ref ContainerMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Ref ECSTaskExecutionRole
ContainerDefinitions:
- Name: !Ref ServiceName
Cpu: !Ref ContainerCpu
Memory: !Ref ContainerMemory
Image: !Ref ImageUrl
PortMappings:
- ContainerPort: !Ref ContainerPort
HostPort: !Ref ContainerPort
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
# The service. The service is a resource which allows you to run multiple
# copies of a type of task, and gather up their logs and metrics, as well
# as monitor the number of running tasks and replace any that have crashed
Service:
Type: AWS::ECS::Service
# Avoid race condition between ECS service creation and associating
# the target group with the LB
DependsOn: PublicLoadBalancerListener
Properties:
ServiceName: !Ref ServiceName
Cluster: !Ref ClusterName
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- !Ref ServiceSecurityGroup
Subnets: !Ref PrivateSubnetIds
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 75
DesiredCount: !Ref DesiredCount
TaskDefinition: !Ref TaskDefinition
LoadBalancers:
- ContainerName: !Ref ServiceName
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref ServiceTargetGroup
# Security group that limits network access
# to the task
ServiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for service
VpcId: !Ref VpcId
# Keeps track of the list of tasks for the service
ServiceTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Port: !Ref ContainerPort
Protocol: HTTP
UnhealthyThresholdCount: 10
VpcId: !Ref VpcId
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 0
# A public facing load balancer, this is used as ingress for
# public facing internet traffic.
PublicLoadBalancerSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref VpcId
SecurityGroupIngress:
# Allow access to public facing ALB from any IP address
- 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: !Ref PublicSubnetIds
SecurityGroups:
- !Ref PublicLoadBalancerSG
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: 'forward'
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref ServiceTargetGroup
Weight: 100
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
# Open up the service's security group to traffic originating
# from the security group of the load balancer.
ServiceIngressfromLoadBalancer:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref ServiceSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG'
# This log group stores the stdout logs from this service's containers
LogGroup:
Type: AWS::Logs::LogGroup
Deploy it all
You should now have the following three files:
vpc.yml
- Template for the base VPC that you wish to host resources incluster.yml
- Template for the ECS cluster and its capacity providerservice.yml
- Template for the web service that will be deployed on the cluster
Use the following parent stack to deploy all three stacks:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Parent stack that deploys VPC, Amazon ECS cluster for AWS Fargate,
and a serverless Amazon ECS service deployment that hosts
the task containers on AWS Fargate
Resources:
# The networking configuration. This creates an isolated
# network specific to this particular environment
VpcStack:
Type: AWS::Serverless::Application
Properties:
Location: vpc.yml
# This stack contains the Amazon ECS cluster itself
ClusterStack:
Type: AWS::Serverless::Application
Properties:
Location: cluster.yml
# This stack contains the container deployment
ServiceStack:
Type: AWS::Serverless::Application
Properties:
Location: service.yml
Parameters:
VpcId: !GetAtt VpcStack.Outputs.VpcId
PublicSubnetIds: !GetAtt VpcStack.Outputs.PublicSubnetIds
PrivateSubnetIds: !GetAtt VpcStack.Outputs.PrivateSubnetIds
ClusterName: !GetAtt ClusterStack.Outputs.ClusterName
ECSTaskExecutionRole: !GetAtt ClusterStack.Outputs.ECSTaskExecutionRole
Use the following command to deploy all three stacks:
sam deploy \
--template-file parent.yml \
--stack-name serverless-api-environment \
--resolve-s3 \
--capabilities CAPABILITY_IAM
Test it out
Open the Amazon ECS cluster in the web console and verify that the service has been created with a desired count of two. You will observe the service create tasks that launch into AWS Fargate. Last but not least ECS will register the task's IP addresses with the Application Load Balancer so that it can send them traffic.
On the "Health & Metrics" tab of the service details page you can click on the load balancer name to navigate to the load balancer in the EC2 console. This will give you the load balancer's public facing CNAME that you can copy and paste into your browser to verify that the sample NGINX webserver is up and running.
Tear it down
You can tear down the entire stack with the following command:
sam delete --stack-name serverless-api-environment
See Also
- If you plan to run an extremely large workload that requires many vCPU's worth of compute resources, or you wish to have more control over the precise CPU generation used to power your application, then consider using Amazon ECS to host your containerized API service on EC2.
- This pattern does not configure scaling. Consider using the target tracking scaling pattern to add autoscaling.