There is an alternative to run Flask on AWS Elastic Beanstalk that allow numerous customization options - run Flask on ECS Fargate. This serverless (you don’t have to manage a cluster of EC2) solution runs Docker images and can run Flask web server. There is a lot of AWS resources involved to make it work. I’m sharing CloudFormation templates that will create them automatically.Source code
Here are the details of these templates:
Resources
flask_resources.yaml
- is a CloudFormation template that will create services that won’t change much. It consists of three parts.
DNS
The input for that template is a domain name. I’m using flask.com
as an example. Route53::HostedZone
is created for that domain to host DNS records. It will also contain NS records to be used when you need to set up your domain registrar. Route53::RecordSet
is a DNS A record then points flask.com
to application load balancer (ALB).
VPC
A virtual private cloud (VPC) is a logical isolation of the resources. It’s necessary for ALB to work. I’m following this example that creates a pair of private subnets (where our containers will live), a pair of public subnets (in different AZs) and all the wiring - routing tables and default routes, internet and NAT gateways.
LoadBalancer
This one consists of a ElasticLoadBalancingV2::LoadBalancer
that resides in 2 public subnets. There is one config parameter I have in the template, which is idle_timeout.timeout_seconds
. More in it here.LoadBalancer drive traffic to ElasticLoadBalancingV2::Listener
which drives further to ElasticLoadBalancingV2::TargetGroup
. There could be another additional listener for HTTPS. The listener is a very powerful thing. It can do SSL off-load and even Cognito authentication. ElasticLoadBalancingV2::TargetGroup
config will define how often it will call out Flask health_check
endpoint to decide if the container is alive and available for accepting requests.
ECS
ECR::Repository
will hold Docker images. I’m building them locally, but it’s also possible to use CodeBuild and have a full CI/CD pipeline for that (Maybe another post?)ECS::Cluster
is a placeholder for the Flask stack.flask_resources.yaml
stack export some variables to be used in flask_stack.yaml
. The former will create a ECS task. I’ve tried to move as name resources to the first stack and leave only essentials in the second one.
Stack
flask_stack.yaml
contains a ECS::Service
. It run and maintains a number if running Docker containers (tasks) and associates load balancers with them.ECS::TaskDefinition
is an instruction on how to run Docker container. Environment variables can be specified there as well as CPU and Memory that will be used by a task. See this doc for a reference. TaskDefinition also specifies where to put stdout&stderr logs. I’m putting them into Cloudwatch. The inputs for that stack are CPU, Memory and tag. See Building image for tag.
IAM roles
There are a couple of roles created by both stacks: - TaskExecutionRole - a role that containers in this task can assume. Its uses build in (managed) AmazonECSTaskExecutionRolePolicy - TaskRole - a role that grants containers in the task permission to call AWS APIs. I’ve left a single policy that grants access to all DynamoDB tables. But of course, these policies should be as restrictive as possible (only necessary actions and resources/tables) If Flask will be using RDS/Aurora, then these resources should be placed in the same VPC. There is no need for an additional role.
Enabling SSL
First, the certificate is needed:
Certificate:
Type: "AWS::CertificateManager::Certificate"
Properties:
DomainName: !Sub "*.${Domain}"
SubjectAlternativeNames:
- !Ref Domain
Then this certificate is used by additional load balancer listener:
HTTPSListener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
Certificates:
- CertificateArn: !Ref Certificate
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
Then a redirect should be added in nginx.conf
inside a location section:
if ($http_x_forwarded_proto != "https") {
rewrite ^(.*)$ https://$host$request_uri permanent;
}
Building image
I’m building a Docker image locally and pushing it to AWS ECR.
1. Login to ERC $(aws ecr get-login --region eu-west-1 --no-include-email)
2. Build image locally docker build . -t flask_image
See a folder flask.com
in the repo.
3. Get the image ID by issuing docker images
4. Tag image. For that you’ll need you AWS account number and region. Put the tag that will be passed to CF template.
docker tag <image_id> <aws_account_number>.dkr.ecr..amazonaws.com/:<my_tag>
5. Upload the image docker push <aws_account_number>.dkr.ecr.<region>.amazonaws.com/<domain>
Bringing it all together
1. Create a stack from resources CloudFormation template
2. Build and push the image to ECR
3. Create a stack from stack.yaml CloudFormation template
4. In Route53 locate a newly created hosted zone and find a A record with ALIAS. Open load balancer hostname in browser - you’ll see a response from Flask
5. Rewrite NS records in registrar to use the ones from the Hosted zone. (So example flask.com
will lead to the deployed server)
Further thoughts
The stacks are missing some important things:
- Scaling. In ECS::Service
a DesiredCount attribute can be specified to define the number of simultaneous running containers. But it doesn’t change based on the load.
- Alarms. There are logs on CloudWatch. An alarm can be created based on these logs, like a number of excessive 4XX, 5XX HTTP responses, to long load balance response
- CI/CD. Images are built locally, CodePipeline+CodeBuild can be used for that.