FastAPI Container Deployment with AWS Lambda and Amazon ECS
Tags: aws, python, fastapi
Published: 2021-10-22 04:57:18


AWS container services provide many options for running containerized workloads in the cloud. In this post we will show how a Python application packaged into a container image can be executed on both AWS Lambda and Amazon ECS. 

The Python workload is a simple web service endpoint that adds numbers together and returns the result. It is developed using the popular [FastAPI](https://fastapi.tiangolo.com/) library. Applications deployed with this library are typically run containerized with a web server. The [Mangum](https://mangum.io/) library provides an adaptor that allows FastAPI applications to run in AWS Lambda behind Amazon API Gateway. The library translates the AWS Lambda proxy event to the Python ASGI standard. 

## Architecture
![Architecture Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zsmbir22ydoep0t8fn2i.jpg)
The ECS Task definition and Lambda function reference the same container image from ECR.

## Docker ENTRYPOINT and CMD
The ability to run the same container image with ECS or Lambda is achieved by taking advantage of the ability to override the default container settings for ENTRYPOINT and CMD.

ENTRYPOINT defines the executable that will run when the container is launched. CMD defines the parameters that are passed to the executable. For a detailed discussion of these Dockerfile instructions, see the AWS blog post “[Demystifying ENTRYPOINT and CMD in Docker](https://aws.amazon.com/blogs/opensource/demystifying-entrypoint-cmd-docker/)”.

## Setup and Code
The example FastAPI code and a CDK application that builds the container, uploads it to Amazon ECR, deploys to both AWS Lambda and an Amazon ECS Service can be found at https://github.com/eldritchideen/math-service.

The FastAPI service is very simple, it accepts 0 or more integers in a query parameter and returns the sum.
```python
from typing import List, Optional
from fastapi import FastAPI
from fastapi.param_functions import Query
from fastapi.responses import PlainTextResponse
from mangum import Mangum


app = FastAPI()


@app.get("/math/add", response_class=PlainTextResponse)
async def addition(i: Optional[List[int]] = Query([])):
    return str(sum(i))


@app.get("/")
async def health():
    return {"status": "OK"}


handler = Mangum(app)
```
  
```Dockerfile
FROM public.ecr.aws/lambda/python:3.8
RUN pip install fastapi "uvicorn[standard]" mangum
ADD main.py ${LAMBDA_TASK_ROOT}
CMD ["main.handler"]
```

The Dockerfile in the example above is based on the standard Lambda base image. The ENTRYPOINT executes a shell script that starts up the Lambda environment and loads the handler specified in CMD. This container can be pulled from ECR and run in Lambda directly. The key to allowing this image to be run up in Amazon ECS is the installation of Uvicorn. 
 
To run this same container in ECS, we must provide overrides for ENTRYPOINT and CMD. In the ECS Task definition we override the ENTRYPOINT with `[“uvicorn”]`. This sets the container to run the Uvicorn Python web server on startup. The CMD is overridden with `["main:app", "--host", "0.0.0.0", "--port", "80"]`, which defines the options for the web server to load the application and which ports to use. 

The CDK code used to define the ECS Task is:
```python
math_task = ecs.FargateTaskDefinition(
    self,
    "MathTask",
    memory_limit_mib=1024,
    cpu=512,
)
math_task.add_container(
    "MathServiceContainer",
    image=ecs.ContainerImage.from_ecr_repository(
        image.repository, image.asset_hash
    ),
    memory_limit_mib=1024,
    cpu=512,
    port_mappings=[{"containerPort": 80}],
    entry_point=["uvicorn"],
    command=["main:app", "--host", "0.0.0.0", "--port", "80"],
)
```

Hitting the ALB we get the following:
```shell
% http "http://xxxxx.ap-southeast-2.elb.amazonaws.com/math/add?i=5&i=10"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 2
Content-Type: text/plain; charset=utf-8
Date: Fri, 22 Oct 2021 04:24:35 GMT
server: uvicorn

15
```
Also the result from API Gateway and Lambda:
```shell
% http "https://xxxxx.execute-api.ap-southeast-2.amazonaws.com/math/add?i=4&i=6"
HTTP/1.1 200 OK
Apigw-Requestid: Hl6YIiA0ywMEPTA=
Connection: keep-alive
Content-Length: 2
Content-Type: text/plain; charset=utf-8
Date: Fri, 22 Oct 2021 04:28:01 GMT

10

```

## Summary

By planning ahead and packaging some extra dependencies into our container image, we have the ability to choose where to run the container. Using CDK to build the container image, store it in ECR, provision both the ECS Service and Lambda works very well and makes it quick to test out these ideas.