October 23, 2019

Run Flask on AWS ECS (Fargate)

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_imageSee 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.

© Alexey Smirnov 2023