├── app ├── __init__.py ├── api │ └── v1 │ │ ├── __init__.py │ │ ├── endpoints │ │ ├── __init__.py │ │ └── users.py │ │ └── api.py ├── requirements.txt └── main.py ├── terraform ├── provider.tf ├── ecr.tf ├── variables.tf ├── api_gateway.tf └── lambda.tf ├── .gitignore ├── Dockerfile ├── readme.md └── Makefile /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/v1/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | mangum 3 | uvicorn -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | lambda.zip 4 | 5 | .terraform 6 | .terraform.* 7 | terraform.* 8 | 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/python:3.8 2 | 3 | COPY ./app ./app 4 | 5 | RUN pip install -r ./app/requirements.txt 6 | 7 | CMD [ "app.main.handler" ] 8 | -------------------------------------------------------------------------------- /terraform/ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "serverless_fastapi_repository" { 2 | name = local.image_name 3 | image_tag_mutability = "MUTABLE" 4 | } 5 | -------------------------------------------------------------------------------- /app/api/v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .endpoints import users 4 | 5 | router = APIRouter() 6 | router.include_router(users.router, prefix="/users", tags=["Users"]) 7 | -------------------------------------------------------------------------------- /app/api/v1/endpoints/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/") 7 | async def get_users(): 8 | return {"message": "Get users!"} 9 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # constant settings 2 | locals { 3 | image_name = "serverless_fastapi_container" 4 | image_version = "latest" 5 | 6 | lambda_function_name = "serverless_fastapi_function" 7 | 8 | api_name = "serverless_fastapi" 9 | } 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Serverless + FastAPI (Mangum) + Docker + Terraform 2 | 3 | ## How to try this? 🤔 4 | 5 | ### Requirement 6 | 7 | You must have logged in AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html 8 | 9 | ### Create an Elastic Container Registry (ECR) 🏗 10 | 11 | 1. make ecr 12 | 13 | ### Lets deploy Lambda and ApiGateway 🚀 14 | 15 | 1. make deploy 16 | 17 | ### References 18 | 19 | 1. https://towardsdatascience.com/building-a-serverless-containerized-machine-learning-model-api-using-aws-lambda-api-gateway-and-a73a091ff82e 20 | 21 | 2. https://www.deadbear.io/simple-serverless-fastapi-with-aws-lambda/ 22 | 23 | 3. https://www.youtube.com/watch?v=wlVcso4Ut5o 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | AWS_REGION := $(shell eval "aws --profile default configure get region") 2 | IMAGE_TAG := latest 3 | ECR_NAME := serverless_fastapi_repository 4 | 5 | IMAGE_NAME := serverless_fastapi_container 6 | 7 | REGISTRY_ID := $(shell aws ecr \ 8 | --profile default \ 9 | describe-repositories \ 10 | --query 'repositories[?repositoryName == `'$(IMAGE_NAME)'`].registryId' \ 11 | --output text) 12 | 13 | IMAGE_URI := $(REGISTRY_ID).dkr.ecr.$(AWS_REGION).amazonaws.com 14 | 15 | ecr: 16 | @echo "** Creating the ECR repository **" 17 | cd terraform && \ 18 | terraform init && \ 19 | terraform apply -target=aws_ecr_repository.$(ECR_NAME) -auto-approve 20 | 21 | deploy: 22 | @echo "** Login to AWS ECR **" 23 | aws ecr get-login-password --region us-east-1 | \ 24 | docker login --username AWS --password-stdin $(IMAGE_URI) 25 | 26 | @echo "** Building and pushing the container **" 27 | docker build -t $(IMAGE_URI)/$(IMAGE_NAME) . && \ 28 | docker push $(IMAGE_URI)/$(IMAGE_NAME):$(IMAGE_TAG) 29 | 30 | @echo "** Deploying API Gateway and Lambda" 31 | cd terraform && \ 32 | terraform apply -auto-approve 33 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from mangum import Mangum 3 | 4 | from app.api.v1.api import router as api_router 5 | 6 | ############################################################################### 7 | # Application object # 8 | ############################################################################### 9 | app = FastAPI() 10 | 11 | ############################################################################### 12 | # Routers configuration # 13 | ############################################################################### 14 | @app.get("/") 15 | def read_root(): 16 | return {"Hello": "World"} 17 | 18 | app.include_router(api_router, prefix="/api/v1") 19 | 20 | ############################################################################### 21 | # Handler for AWS Lambda # 22 | ############################################################################### 23 | handler = Mangum(app) 24 | 25 | ############################################################################### 26 | # Run the self contained application # 27 | ############################################################################### 28 | if __name__ == "__main__": 29 | uvicorn.run(app, host="0.0.0.0", port=5000) -------------------------------------------------------------------------------- /terraform/api_gateway.tf: -------------------------------------------------------------------------------- 1 | resource "aws_apigatewayv2_api" "lambda-api" { 2 | name = local.api_name 3 | protocol_type = "HTTP" 4 | } 5 | 6 | resource "aws_apigatewayv2_stage" "lambda-stage" { 7 | api_id = aws_apigatewayv2_api.lambda-api.id 8 | name = "$default" 9 | auto_deploy = true 10 | } 11 | 12 | resource "aws_apigatewayv2_integration" "lambda-integration" { 13 | api_id = aws_apigatewayv2_api.lambda-api.id 14 | integration_type = "AWS_PROXY" 15 | integration_method = "POST" 16 | integration_uri = aws_lambda_function.lambda_model_function.invoke_arn 17 | passthrough_behavior = "WHEN_NO_MATCH" 18 | } 19 | 20 | resource "aws_apigatewayv2_route" "lambda_route" { 21 | api_id = aws_apigatewayv2_api.lambda-api.id 22 | route_key = "GET /{proxy+}" 23 | target = "integrations/${aws_apigatewayv2_integration.lambda-integration.id}" 24 | } 25 | 26 | resource "aws_lambda_permission" "api-gateway" { 27 | statement_id = "AllowExecutionFromAPIGateway" 28 | action = "lambda:InvokeFunction" 29 | function_name = aws_lambda_function.lambda_model_function.arn 30 | principal = "apigateway.amazonaws.com" 31 | source_arn = "${aws_apigatewayv2_api.lambda-api.execution_arn}/*/*/*" 32 | } 33 | 34 | output "apigatewayv2_api_api_endpoint" { 35 | description = "The URI of the API" 36 | value = try(aws_apigatewayv2_api.lambda-api.api_endpoint, "") 37 | } 38 | -------------------------------------------------------------------------------- /terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "lambda_model_function" { 2 | function_name = local.lambda_function_name 3 | 4 | role = aws_iam_role.lambda_model_role.arn 5 | 6 | # tag is required, "source image ... is not valid" error will pop up 7 | image_uri = "${aws_ecr_repository.serverless_fastapi_repository.repository_url}:${local.image_version}" 8 | package_type = "Image" 9 | 10 | # we can check the memory usage in the lambda dashboard, sklearn is a bit memory hungry.. 11 | memory_size = 256 12 | 13 | # Uncomment the next line if you have an M1 processor 14 | # architectures = [ "arm64" ] 15 | } 16 | 17 | # as per https://learn.hashicorp.com/tutorials/terraform/lambda-api-gateway 18 | # provide role with no access policy initially 19 | resource "aws_iam_role" "lambda_model_role" { 20 | name = "my-lambda-model-role" 21 | 22 | assume_role_policy = <