AWS Lambda Container 方式部署 Flask 应用并通过 API Gateway 提供访问

发布于:2025-07-14 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言

一年前写过一篇 Lambda 运行 Flask 应用的博文:
https://lpwmm.blog.csdn.net/article/details/139756140

当时使用的是 ZIP 包方式部署应用代码, 对于简单的 API 开发用起来还是可以的, 但是如果需要集成到 CI/CD pipeline 里面就有点不太优雅. 本文将介绍使用容器方式部署 Flask 应用到 Lambda, 并实现通过 API Gateway 进行访问.

开发一个简单的 Flask 应用

使用 uv 作为项目管理工具, 如果你还不了解 uv, 可以参考之前的这篇文章:
https://lpwmm.blog.csdn.net/article/details/146774376

完整的项目代码开源在 Gitee:
https://gitee.com/lpwm/flask-on-lambda

主要涉及到以下常用的场景:

  • 静态文件访问, 模板中引入了自定义的 CSS 样式文件
  • 表单处理
  • 路由重定向

实现效果:
在这里插入图片描述

容器化封装

Dockerfile

# 使用 ECR 提供的 Alpine 环境的 Python 3.12
FROM public.ecr.aws/docker/library/python:3.12-alpine
# [重要] 添加 Lambda Web Adapter (LWA)
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter

# 使用清华源安装 uv
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories \
    && apk add --no-cache uv
    
# [重要] 配置 uv 的缓存文件夹路径, Lambda 中只有 /tmp 具有 RW 权限
ENV UV_CACHE_DIR="/tmp"
# 配置 uv 使用清华源
ENV UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple"

WORKDIR /var/task

# 先将 uv 项目相关的文件复制并初始化 .venv 和依赖
COPY pyproject.toml uv.lock .python-version ./
RUN uv sync

# 再将其他文件复制, 这样可以有效减少后面代码发生更新时重新 build 镜像所需要的操作时间
COPY static ./static
COPY templates ./templates
COPY app.py ./

# Lambda 执行时只能在一个运行环境中跑一个 Worker, 所以注意加参数 -w=1, 监听端口直接用 LWA 默认的 8080, 不用再改 LWA 的参数了
CMD ["uv", "run", "gunicorn", "-b=:8080", "-w=1", "app:app"]

测试容器

docker build -t flask-on-lambda .
docker run -it --rm -p 8080:8080 flask-on-lambda

AWS 资源创建

ECR & Lambda

REPO_NAME=flask-on-lambda
# 创建 ECR repository
aws ecr create-repository --repository-name $REPO_NAME

# 将 ECR repository 的 URI 存入变量, 方便后面调用
REPO_URI=$(aws ecr describe-repositories --repository-names $REPO_NAME --query 'repositories[0].repositoryUri' --output text)

# 从 URI 拆分出来 ECR 的主域名, 用于 Docker 登录访问
ECR_HOST=$(echo $REPO_URI | awk -F'/' '{print $1}')

# Docker 登录 ECR
aws ecr get-login-password --region cn-northwest-1 | docker login --username AWS --password-stdin $ECR_HOST

# 推送 Docker image 到 ECR
docker tag $REPO_NAME:latest $REPO_URI:latest
docker push $REPO_URI:latest

# [可选] 获取最新 Image 的哈希值
LATEST_DIGEST=$(aws ecr describe-images --repository-name $REPO_NAME --query 'sort_by(imageDetails,& imagePushedAt)[-1].imageDigest' --output text)

# [可选] 更新 Lambda
aws lambda update-function-code --function-name $REPO_NAME --image-uri $REPO_URI@$LATEST_DIGEST --no-cli-pager

# 创建 IAM Role
aws iam create-role \
  --role-name lambda-execution-role-$REPO_NAME \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}' \
&& aws iam attach-role-policy \
  --role-name lambda-execution-role-$REPO_NAME \
  --policy-arn arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# 获取 Role ARN
ROLE_ARN=$(aws iam get-role --role-name lambda-execution-role-$REPO_NAME --query 'Role.Arn' --output text)
 
# 创建和 REPO 相同名称的 Lambda
aws lambda create-function \
  --function-name $REPO_NAME \
  --package-type Image \
  --code ImageUri=$REPO_URI:latest \
  --role $ROLE_ARN

测试 Lambda 调用

aws lambda invoke \
  --function-name flask-on-lambda \
  --payload '{
    "httpMethod": "GET",
    "path": "/",
    "headers": {
      "Host": "example.com",
      "User-Agent": "curl/7.68.0"
    },
    "requestContext": {
      "resourcePath": "/",
      "httpMethod": "GET"
    },
    "body": null,
    "isBase64Encoded": false
  }' \
  --cli-binary-format raw-in-base64-out \
  /dev/stdout

预期响应:

{
	"statusCode": 200,
	"headers": {},
	"multiValueHeaders": {
		"server": ["gunicorn"],
		"date": ["Sun, 13 Jul 2025 12:02:04 GMT"],
		"connection": ["close"],
		"content-type": ["text/html; charset=utf-8"],
		"content-length": ["585"]
	},
	"body": "<html>\n\n<head>\n    <title>Flask on Lambda</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style.css\">\n</head>\n\n<body>\n    <section>\n        <h1>Welcome to the Flask on Lambda</h1>\n        <p>This is a simple Flask application powered by Lambda.</p>\n    </section>\n    <section>\n        <form action=\"\" method=\"post\">\n            <label for=\"name\">Name:</label>\n            <input type=\"text\" id=\"name\" name=\"name\" required placeholder=\"Enter your name\">\n            <br>\n            <button type=\"submit\">Submit</button>\n        </form>\n    </section>\n</body>\n\n</html>",
	"isBase64Encoded": false
}

API Gateway

这部分配置用 CLI 会很麻烦, 还是在 Console 操作吧

  • 创建 HTTP API
    注意, 一定要使用 HTTP API, REST API 会在 URL 中包含 Stage 名称, 导致后端的 Flask 无法正常处理路由. 踩坑过程归档在文章末尾了, 有兴趣继续看完
    在这里插入图片描述
    在这里插入图片描述
  • 添加 Lambda 集成
    在这里插入图片描述
  • 修改路由:
    Method: ANY
    Resource path: /{proxy+}
    在这里插入图片描述
  • 使用默认 Stage
    在这里插入图片描述
  • 完成创建
    在这里插入图片描述
  • 在 Deploy > Stages 中找到 Invoke URL
    在这里插入图片描述
  • 使用浏览器访问测试, 受到 Lambda 的 Cold start 机制的影响, 首次加载和交互的速度会有点慢.
    在这里插入图片描述
    后面刷新后再次交互速度就很快了.
    在这里插入图片描述

性能优化

为了保证用户能在首次访问的时候也有友好的体验, 我们可以为 Lambda 配置 Provisioned concurrency (额外收费的哟)

  • 首先为 Lambda function 创建 Version
    在这里插入图片描述
    在这里插入图片描述
  • 在 Version 视图中编辑 Provisioned concurrency
    在这里插入图片描述
    在这里插入图片描述
  • 此时 Status 为 In progress, 需要等几分钟
    在这里插入图片描述
    状态变成 Ready 就好了
    在这里插入图片描述
  • 复制当前 Version 界面的 Function ARN
    在这里插入图片描述
  • 回到 HTTP API 控制台修改 Integration, 将 Lambda function 对应的 ARN 更新为上面复制的带有 Version 信息的
    在这里插入图片描述
  • 确认目前使用的集成设置中 Lambda 包含了版本信息(后面多了 :1)
    在这里插入图片描述
    因为 HTTP API 默认开启了 Auto deploy 的选项, 所以这种修改都不需要手动重新 Deploy 操作. 再次使用浏览器访问测试, 速度嘎嘎的~

当然, 我们前面配置的 Provisioned concurrency = 1, 对于生产环境业务负载较高的场景, 可以酌情提升.

结尾

至此, 我们成功使用 Docker 容器的方式将一个 Flask 应用部署到了 Lambda 上, 并通过 API Gateway (HTTP API) 对外提供了可访问的 URL 地址, 实现了 Serverless 部署传统 Web 应用. 🎉🎉🎉
由于应用全部都封装在了 ECR 镜像, 所以在实际项目中, 也可以很方便的融入到 CI/CD pipeline 中.

关于之前撰稿期间使用 REST API 踩坑的经历, 有兴趣可以继续阅览. 😂

REST API 踩坑归档

  • 添加 Trigger
    在这里插入图片描述
  • 创建新的 REST API
    在这里插入图片描述
  • 打开自动创建好的 API
    在这里插入图片描述
  • 删除自动创建的资源路径
    在这里插入图片描述
  • 在根路径下创建资源
    在这里插入图片描述
  • 创建 Proxy 资源
    在这里插入图片描述
  • 编辑集成
    在这里插入图片描述
  • Execution role 可以留空
    在这里插入图片描述
  • 测试 GET 方法
    在这里插入图片描述
    在这里插入图片描述
  • 部署 API
    在这里插入图片描述

REST API 存在问题

完成上面的配置后, 如果从浏览器直接访问 Stage URL 根路径报错:
在这里插入图片描述
访问子路径 success/变量 可以加载出来页面
在这里插入图片描述
但是静态 CSS 文件加载失败, 因为请求路径中并没有包含 stage 的名称
在这里插入图片描述
先来解决直接访问 Stage 根路径报错的问题. 这是因为前面只给 /{proxy+} 创建了 ANY 方法和集成, 对于 / 来说, 还是空的设置. 再单独选中 / 资源路径, 创建 ANY 方法, 相同的方式配置 Lambda proxy 集成
在这里插入图片描述

重新部署后就可以访问到了:
在这里插入图片描述
当提交表单后, 重新定向的 URL 又出现了和 CSS 加载相同的问题, Stage 名称丢失了:
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到