前两年用 AWS Lambda 搭配 API Gateway 使用是为了省钱,因为没有请求时不花钱。又由于是 Rest API, 所以实现部分用了 FastAPI 的装饰器,但不实际启动 FastAPI 的 Web 服务,Lambda 的 handler 方法根据 routeKey 手动映射到 FastAPI 的装饰方法。大概实现是
def lambda_handler(event: dict, context):
fastapi_function = locate_fastapi_function(event['routeKey'])
return fastapi_function(<extract parameters from event>)
当时也思考着能不能把 Lambda 的请求与 FastAPI 的 Web 服务桥接起来,却又不能真正启动一个 Web 服务,否则 Lambda 调用不能结束。比如说 AWS Lambda 收到请求时快速启动 FastAPI 服务,该服务绑定到 TCP 端口或 Socket 文件都行,然后 Lambda 请求代理到 FastAPI 服务,最后关闭 FastAPI 服务,但是想来都不那么容易实现。
所以就在最近(前几周)把 Lambda + API Gateway 的应用迁移到了 ECS 上,这样带来的便利有
- 纯粹的 FastAPI 应用开发
- 方便使用 FastAPI 的 /docs Swagger UI
- 不再有 Lambda 的 Payload 6M 的限制。API Gateway 的 Payload 限制为 10M
而坏处是费钱,ELB 和 EC2 每月有数十美元的支出,Lambda 的调用次数极少,这部分费用可忽略不计。部署方面的复杂性差不了多少, Lambda + API Gateway 和 ECS 都可以使用自定义的域名。
其实被迫从 Lambda 迁移到 ECS 的一切一切的根源是太晚结识 Python 库 Mangum, 它最早的 Release 版本是 2020-05-09 的 v0.9.0。Mangum 是一个适配器,使得 AWS Lambda 运行 ASGI 应用能处理 Function URL, API Gateway, ALB 和 Lambda@Edge 事件。其中一个支持特性就是兼容 ASGI 应用框架,如 Starlette, FastAPI, Quart 和 Django。
下面就花点时间亲自体验一下 Mangum 部署 FastAPI 到 AWS Lambda 的功能
准备代码及部署 Lambda
在 Mangum 官方 FastAPI 应用示例中增加一个文件上传的功能,完整的操作步骤如下
$ mkdir fastapi-lambda
$ python3.9 -m venv .venv
$ source .venv/bin/activate
$ cd fastapi-lambda
$ pip install --target ./ fastapi python-multipart mangum
$ vi main.py
main.py 文件内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from fastapi import FastAPI, UploadFile from mangum import Mangum app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q} @app.post("/uploadfile") def upload_file(upload_file: UploadFile): return {"filename": upload_file.filename, "content": upload_file.file.read().decode()} handler = Mangum(app, lifespan="off") |
然后打成 zip 包,仍然在 fastapi-lambda
目录中
$ zip -r fastapi-lambda.zip -r *
现在可以部署一个 AWS Lambda 了,基本要素是
- Code:上传前面的 fastapi-lambda.zip 压缩包
- Runtime: Python 3.9
- Handler: main.handler
- 启用 Lambda 的 Function URL
部署完后我们可进行多种集成方式集成测试
通过 Lambda Function URL 访问
比如我们启用了 Function URL 之后得到一个链接 https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/,访问 https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/docs 打开了 FastAPI 的 Swagger UI
测试部署到 Lambda 中 FastAPI,可以直接用 Swagger UI 操作或用 curl 命令
➜ / curl https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/
{"Hello":"World"}%
➜ / curl https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/items/123\?q\=abc
{"item_id":123,"q":"abc"}%
➜ / curl https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/uploadfile \
-F "upload_file=@/Users/yanbin/demo/fastapi-lambda/main.py"
{"filename":"main.py","content":"from fastapi import FastAPI, UploadFile\nfrom mangum import Mangum\n\napp = FastAPI()\n\n\n@app.get(\"/\")\ndef read_root():\n return {\"Hello\": \"World\"}\n\n\n@app.get(\"/items/{item_id}\")\ndef read_item(item_id: int, q: str = None):\n return {\"item_id\": item_id, \"q\": q}\n\n\n@app.post(\"/uploadfile\")\ndef upload_file(upload_file: UploadFile):\n return {\"filename\": upload_file.filename, \"content\": upload_file.file.read().decode()}\n\nhandler = Mangum(app, lifespan=\"off\")\n"}%
本人所用的 shell 为 zsh, 所以 ?
和 =
会用 \?
和 \=
进行转义,上传时直接上传 main.py 文件,在服务端读出文件的内容。
API Gateway + HTTP 集成
创建 API Gateway 时选择 HTTP API,选择集成 HTTP, 然后创建一个 route
Route and method: ANY /{proxy+}
集成到 Lambda Function URL
/{path+} ANY -> HTTP URL
ANY https://emfb4cust6hbntfwuev5bz6jna0wyinr.lambda-url.us-east-1.on.aws/{proxy}
即任何 HTTP_METHOD 对任意路径的访问全都代理到 Lambda Function URL
部署了 $default Stage 后,产生一个 API Gateway 的 URL, 如 https://dd11zks36i.execute-api.us-east-1.amazonaws.com/, 访问
执行上面的 curl 命令,全部通过。
API Gateway + Lambda 集成
创建 API Gateway 同样选择 HTTP API,选择集成 Lambda,版本选择 2.0。创建一样的 route
route: ANY /{proxy+}
集成到 Lambda
/{proxy+} ANY -> Lambda function: fastapi-lambda
部署到 $default Stage 后产生 API Gateway URL, 如 https://2mgr5mplc1.execute-api.us-east-1.amazonaws.com/。 同样,可经由该 URL 访问 Swagger UI,通过相同的 curl 命令测试。
API Gateway REST API + Lambda 集成
创建 API Gateway 时选择 REST API, 创建 Resource/Action
/ ANY
-> Integration type: Lambda Function, Use Lambda proxy Integration, 选择 fastapi-lambda
/{proxy+} ANY
, proxy resource -> Integration type: Lambda Fucntion proxy, 选择 fastapi-lambda
完后部署到 test Stage, 产生一个 URL,如 https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test, 测试
➜ curl https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test
{"Hello":"World"}%
➜ curl https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test/items/123\?q\=abc
{"item_id":123,"q":"abc"}%
➜ curl https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test/uploadfile \
-F "upload_file=@/Users/yanbin/demo/fastapi-lambda/main.py"
{"filename":"main.py","content":"from fastapi import FastAPI, UploadFile\nfrom mangum import Mangum\n\napp = FastAPI()\n\n\n@app.get(\"/\")\ndef read_root():\n return {\"Hello\": \"World\"}\n\n\n@app.get(\"/items/{item_id}\")\ndef read_item(item_id: int, q: str = None):\n return {\"item_id\": item_id, \"q\": q}\n\n\n@app.post(\"/uploadfile\")\ndef upload_file(upload_file: UploadFile):\n return {\"filename\": upload_file.filename, \"content\": upload_file.file.read().decode()}\n\nhandler = Mangum(app, lifespan=\"off\")\n"}
但在浏览器中直接访问 https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test/docs 就有问题了
原因是虽然我们部署 API Gateway 在 /test Stage 上,但 openapi.json 的路径仍然在 / 根目录上。即使我们在 main.py
中指定 openapi_url
1 |
app = FastAPI(openapi_url="/test/openapi.json") |
也无济于事,因为 Swagger UI 加载 openapi.json 时变成了 https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test/openapi.json, 替换掉其中的 /test 访问的是 FastAPI 的 /openapi.json,而 FastAPI 产生的 openapi.json 的 URL 是 /test/openapi.json. Swgger UI 加载与 FastAPI 产生的 openjson.json 不能错位。
解决办法可以在本地启动 FastAPI Web 服务,把生成的 openapi.json 内容保存为 openapi.json 文件,然后在 main.app 中新增 API
1 2 3 4 |
@app.get("/openapi.json") def openapi_json(): with open('openapi.json') as f: return f.read() |
前面的指定 openapi_url="/test/openapi.json" 依然需要,/test 与 APIGateway 的 Stage 名相匹配,如有不同的 Stage 名可在 openapi_json() 函数中根据当前 URL 灵活处理。
部署后,访问 https://gos0owi9r4.execute-api.us-east-1.amazonaws.com/test/docs,熟悉的界面又回来了
这都是由于 REST API 与 Lambda 集成时没有默认的 Stage 名造成的。
FastAPI 的静态文件
最后测试一下 Lambda 中部署 FastAPI 的静态文件,在 fastapi-lambda 目录中创建目录及文件
static
├── index.html
├── main.css
└── main.js
index.html 的内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<html> <head> <link rel="stylesheet" href="main.css"> <script src="main.js"></script> <title>Test FastAPI static file in Lambda</title> </head> <body> <div style="text-align: center"> <h1>Hello FastAPI</h1> <button onclick="btn_click()">click me</button> </div> </body> </html> |
main.css 中定义了 button 的样式
1 2 3 |
button { background-color: lightblue } |
main.js 中定义了 btn_click 函数
1 2 3 |
function btn_click() { alert("button clicked"); } |
重新打包
$ zip -r fastapi-lambda.zip *
部署到 Lambda, 然后用 Lambda 的 Function URL 访问 https://onbfbzsxxrdsirutnrpbz32qfm0cxbgc.lambda-url.us-east-1.on.aws/static/index.html,符合预期
整个验证下来,Mangum + FastAPI + AWS Lambda 不仅能实现 RestAPI, 做个正常的网站都没问题。
总结:
Mangum + FastAPI 部署到 Lambda 使得 FastAPI 也是一个 Serverless 服务,同时具有了完备的功能,编程方式也未曾改变。
综上几种应用集成的方式,最简单的方式莫过于启用 Lambda Function URL, 然后用 API Gateway 通过 HTTP 与 Lambda Function URL 集成。使用了 API Gateway 的好处是可以给 API Gateway 使用自定义域名,使得 API 更友好。
另外,在使用 API Gateway 集成 AWS Lambda 请留意相应的访问权限配置,如果通过 AWS 控制台操作的话都是自动配置的,用 Terraform 等工具的会多一点工作,简单起见,能用通配符就用通配符,安全访问控制可在 Python 代码中处理。
如果能接受 Lambda Payload 6M 的限制和最大 30 秒的响应限制话,使用 API Gateway + Lambda (Mangum + FastAPI) 还是很划算的,部署后不用的话零成本,而且还能更简单的用上访问认证,配额监控,API Gateway 想公开或是私有(Regional)的都更随意。
本文链接 https://yanbin.blog/deploy-fastapi-on-aws-lambda/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。