AWS Lambda Running FastAPI using Lambda Web Adapter
Lambda Web Adapter has drastically accelerated application development which uses AWS Lambda. It enables us to develop backend services on AWS Lambda using docker which most developers are accustomed to.
This post guides you through how to develop API backends with FastAPI using Lambda Web Adapter. You can clone the example from my GitHub repository.
Overview
Merits
I think that merits of Lambda Web Adapter are:
Comparison Items | Lambda Web Adapter | Traditional Lambda |
---|---|---|
Testing locally | Easy | Hard |
Migrating to other services like Fargate | Easy | Hard |
Number of AWS resources | Less 1 | More |
Cost | Low (because of overhead) | Very low |
- There are only one Lambda function and one API Gateway route.
AWS Design
Traditional
The traditional AWS design using API Gateway and Lambda functions is like the following.
It might become chaos if you add many APIs.
Using Lambda Web Adapter
Lambda Web Adapter simplifies the AWS design like the following.
You do not need to take care of many API Gateway routes and Lambda functions.
Project Structure
This post creates the following project structure.
/
|-- .venv/
|-- cdk/
| |-- bin/
| | `-- cdk.ts
| |-- lib/
| | `-- cdk-stack.ts
| |-- node_modules/
| |-- test/
| | `-- cdk.test.ts
| |-- .gitignore
| |-- .npmignore
| |-- biome.json
| |-- cdk.json
| |-- jest.config.js
| |-- package.json
| |-- package-lock.json
| |-- README.md
| `-- tsconfig.json
|-- docker/
| |-- Dockerfile-web
| `-- compose.yaml
|-- node_modules/
|-- src/
| |-- main.py
| `-- requirements.txt
|-- .gitignore
|-- package.json
`-- package-lock.json
Getting Started
Bootstrapping AWS CDK Environment
First of all, bootstrap your AWS CDK environment.
In this post, we install AWS CDK locally to avoid polluting the global npm environment.
npm i -D aws-cdk
npx cdk bootstrap aws://<AWS_ACCOUNT_ID>/<AWS_REGION>
Initializing CDK Project
To initialize your CDK project, run the following command.
mkdir cdk && cd cdk
npx cdk init app --language typescript
Setting up FastAPI
To install FastAPI, run the following command.
python -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]"
mkdir src
pip freeze > ./src/requirements.txt
Backend API using FastAPI
Writing API
In this post, we use the FastAPI example as it is.
Write the following code and save it to ./src/main.py
.
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
Testing API
You can start the server with the following command.
fastapi dev ./src/main.py
You would see the server running.
...
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [73992] using WatchFiles
INFO: Started server process [73997]
INFO: Waiting for application startup.
INFO: Application startup complete.
Then, hit the API using tools which you like. You should see the response returned.
curl "http://127.0.0.1:8000/"
{"Hello":"World"}
curl "http://127.0.0.1:8000/items/1?q=keyword"
{"item_id":1,"q":"keyword"}
Containerizing using Docker
Dockerfile
To containerize the FastAPI backend, write the following code and save it to ./docker/Dockerfile-web
.
The key points here are:
- Using the base image
public.ecr.aws/docker/library/python:3.12-alpine
provided by AWS (line 1). - Copying Lambda Web Adapter (line 15).
- Listening the port 8080 which Lambda Web Adapter uses by default (line 16).
By default, Lambda Web Adapter assumes the web app is listening on port 8080. If not, you can specify the port via configuration.
FROM public.ecr.aws/docker/library/python:3.12-alpine AS base
ENV APP_ROOT=/code
COPY ./src/requirements.txt $APP_ROOT/
RUN pip install --no-cache-dir --upgrade -r $APP_ROOT/requirements.txt
FROM base AS dev
ENV ENV=dev
EXPOSE 8000
CMD ["sh", "-c", "fastapi run $APP_ROOT/main.py --port 8000"]
FROM base
ENV ENV=prod
EXPOSE 8080
COPY ./src $APP_ROOT
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
CMD ["sh", "-c", "fastapi run $APP_ROOT/main.py --port 8080"]
Docker Compose for Local Development
Although the databases are not used in this post, they are almost always used, for example, DynamoDB, MySQL, etc.
Thus, it would be useful for local development to prepare ./docker/compose.yaml
.
services:
api:
build:
context: ../
dockerfile: ./docker/Dockerfile-web
target: dev
ports:
- "8000:8000"
volumes:
- ../src:/code
You can run the backend service with the following command.
cd docker
docker compose up
...
api-1 | INFO: Started server process [1]
api-1 | INFO: Waiting for application startup.
api-1 | INFO: Application startup complete.
api-1 | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Deploying to AWS
Defining AWS Resources using AWS CDK
What you need to do is only to define AWS resources using AWS CDK.
Paste the following into cdk/bin/cdk.ts
.
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkStack } from '../lib/cdk-stack';
const app = new cdk.App();
new CdkStack(app, 'App');
Paste the following into cdk/lib/cdk-stack.ts
.
You may be surprised about the less code.
Note that the following:
memorySize
(line 19) larger than 128 should be better to avoid timeout.platform: Platform.LINUX_AMD64
(line 22) is needed if you are using Apple Silicon; otherwise you would see the error sayingError: fork/exec /opt/extensions/lambda-adapter: exec format error Extension.LaunchError
.
import * as cdk from 'aws-cdk-lib';
import type { Construct } from 'constructs';
import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway';
import {
DockerImageCode,
DockerImageFunction,
LoggingFormat,
} from 'aws-cdk-lib/aws-lambda';
import * as path from 'node:path';
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Dockerized Lambda Function
const lambda = new DockerImageFunction(this, 'function', {
functionName: 'fast-api-app-function',
loggingFormat: LoggingFormat.JSON,
memorySize: 512, // To avoid timeout
code: DockerImageCode.fromImageAsset(path.join(__dirname, '..', '..'), {
file: path.join('docker', 'Dockerfile-web'),
platform: Platform.LINUX_AMD64, // If you are using Apple Silicon
exclude: ['*', '!src', '!docker'],
}),
});
// API Gateway REST API
new LambdaRestApi(this, 'api', {
handler: lambda,
deploy: true,
});
}
}
Deploying App
Finally, hit the following command. It may take up to a few minutes.
cd cdk
npx cdk deploy
...
Do you wish to deploy these changes (y/n)? y
App: deploying... [1/1]
App: creating CloudFormation changeset...
✅ App
✨ Deployment time: 52.67s
Outputs:
App.apiEndpoint9349E63C = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:<AWS_REGION>:<AWS_ACCOUNT_ID>:stack/App/<UUID>
✨ Total time: 55.42s
Testing App
You can test your APIs using tools which you like.
curl "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/"
{"Hello":"World"}
curl "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/items/1?q=keyword"
{"item_id":1,"q":"keyword"}
Conclusion
When developing APIs with AWS Lambda, Lambda Web Adapter may be the first choice.
Before its introduction, developers faced challenges in testing locally and managing numerous API Gateway routes and Lambda functions. However, you can now test backend APIs as usual, with only one API Gateway route and one Lambda function to manage.
I hope you will find this post useful.