2.7.16 与 AWS Lambda 配合使用

原文链接: https://docs.astral.sh/uv/guides/integration/aws-lambda/

2.7.16 与 AWS Lambda 配合使用

AWS Lambda 是一项无服务器计算服务,让你无需预置或管理服务器即可运行代码。

你可以把 uv 与 AWS Lambda 配合使用,管理 Python 依赖、构建部署包并部署 Lambda 函数。

提示

关于用 uv 把应用部署到 AWS Lambda 的最佳实践示例,请查看 uv-aws-lambda-example 项目。

快速开始

首先,假设我们有一个结构如下的最小 FastAPI 应用:

1
2
3
4
5
project
├── pyproject.toml
└── app
    ├── __init__.py
    └── main.py

其中 pyproject.toml 内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# pyproject.toml
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI 是用于构建 Python API 的现代 Web 框架。
    "fastapi",
    # Mangum 是把 ASGI 应用适配到 AWS Lambda 与 API Gateway 的库。
    "mangum",
]

[dependency-groups]
dev = [
    # 在开发模式下,包含 FastAPI 开发服务器。
    "fastapi[standard]>=0.115",
]

main.py 文件内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# app/main.py
import logging

from fastapi import FastAPI
from mangum import Mangum

logger = logging.getLogger()
logger.setLevel(logging.INFO)

app = FastAPI()
handler = Mangum(app)


@app.get("/")
async def root() -> str:
    return "Hello, world!"

我们可以这样在本地运行该应用:

1
$ uv run fastapi dev

接着在浏览器中打开 http://127.0.0.1:8000/ 会显示 “Hello, world!”

部署 Docker 镜像

要部署到 AWS Lambda,我们需要构建一个把应用代码和依赖包含在同一个输出目录中的容器镜像。

我们会遵循 Docker 指南中所述的原则(特别是多阶段构建),以确保最终镜像尽可能小且对缓存友好。

在第一阶段,我们把所有应用代码和依赖放入同一个目录。在第二阶段,我们把这个目录复制到最终镜像,省略构建工具和其他不必要的文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Dockerfile
FROM ghcr.io/astral-sh/uv:0.12.19 AS uv

# 首先,把依赖打包任务根目录。
FROM public.ecr.aws/lambda/python:3.13 AS builder

# 启用字节码编译以改善冷启动性能。
ENV UV_COMPILE_BYTECODE=1

# 禁用安装器元数据,以创建确定性的层。
ENV UV_NO_INSTALLER_METADATA=1

# 启用复制模式以支持绑定挂载缓存。
ENV UV_LINK_MODE=copy

# 通过 `uv pip install --target` 把依赖打包到 Lambda 任务根目录。
#
# 省略任何本地包(`--no-emit-workspace`)和开发依赖(`--no-dev`)。
# 这样可以确保只有在 `pyproject.toml` 或 `uv.lock` 文件变化时才会失效 Docker 层缓存,
# 同时又能适应应用代码的变化。
RUN --mount=from=uv,source=/uv,target=/bin/uv \
    --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv export --frozen --no-emit-workspace --no-dev --no-editable -o requirements.txt && \
    uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

FROM public.ecr.aws/lambda/python:3.13

# 从构建阶段复制运行时依赖。
COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}

# 复制应用代码。
COPY ./app ${LAMBDA_TASK_ROOT}/app

# 设置 AWS Lambda 处理程序。
CMD ["app.main.handler"]

提示

要部署到基于 ARM 的 AWS Lambda 运行时,请把 public.ecr.aws/lambda/python:3.13 替换为 public.ecr.aws/lambda/python:3.13-arm64。

我们可以这样构建镜像:

1
2
$ uv lock
$ docker build -t fastapi-app .

这个 Dockerfile 结构的核心优势如下:

  1. 镜像体积最小。 通过多阶段构建,我们可以确保最终镜像只包含应用代码和依赖。例如,uv 二进制文件本身不会出现在最终镜像中。
  2. 缓存复用最大化。 通过把应用依赖与应用代码分开安装,我们可以确保只有在依赖变化时才会失效 Docker 层缓存。

具体来说,修改应用源代码后重新构建镜像可以复用缓存层,从而在毫秒级完成构建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 => [internal] load build definition from Dockerfile                                                                 0.0s
 => => transferring dockerfile: 1.31kB                                                                               0.0s
 => [internal] load metadata for public.ecr.aws/lambda/python:3.13                                                   0.3s
 => [internal] load metadata for ghcr.io/astral-sh/uv:latest                                                         0.3s
 => [internal] load .dockerignore                                                                                    0.0s
 => => transferring context: 106B                                                                                    0.0s
 => [uv 1/1] FROM ghcr.io/astral-sh/uv:latest@sha256:ea61e006cfec0e8d81fae901ad703e09d2c6cf1aa58abcb6507d124b50286f  0.0s
 => [builder 1/2] FROM public.ecr.aws/lambda/python:3.13@sha256:f5b51b377b80bd303fe8055084e2763336ea8920d12955b23ef  0.0s
 => [internal] load build context                                                                                    0.0s
 => => transferring context: 185B                                                                                    0.0s
 => CACHED [builder 2/2] RUN --mount=from=uv,source=/uv,target=/bin/uv     --mount=type=cache,target=/root/.cache/u  0.0s
 => CACHED [stage-2 2/3] COPY --from=builder /var/task /var/task                                                     0.0s
 => CACHED [stage-2 3/3] COPY ./app /var/task                                                                        0.0s
 => exporting to image                                                                                               0.0s
 => => exporting layers                                                                                              0.0s
 => => writing image sha256:6f8f9ef715a7cda466b677a9df4046ebbb90c8e88595242ade3b4771f547652d                         0.0

构建完成后,我们可以把镜像推送到 Elastic Container Registry (ECR):

1
2
3
$ aws ecr get-login-password --region region | docker login --username AWS --password-stdin aws_account_id.dkr.ecr.region.amazonaws.com
$ docker tag fastapi-app:latest aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest
$ docker push aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest

最后,我们可以用 AWS 管理控制台或 AWS CLI 把镜像部署到 AWS Lambda,例如:

1
2
3
4
5
$ aws lambda create-function \
   --function-name myFunction \
   --package-type Image \
   --code ImageUri=aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest \
   --role arn:aws:iam::111122223333:role/my-lambda-role

其中执行角色通过下面的命令创建:

1
2
3
$ aws iam create-role \
   --role-name my-lambda-role \
   --assume-role-policy-document '{"Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

或者这样更新已有函数:

1
2
3
4
$ aws lambda update-function-code \
   --function-name myFunction \
   --image-uri aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest \
   --publish

要测试该 Lambda,我们可以通过 AWS 管理控制台或 AWS CLI 调用它,例如:

1
2
3
4
5
6
7
8
9
$ aws lambda invoke \
   --function-name myFunction \
   --payload file://event.json \
   --cli-binary-format raw-in-base64-out \
   response.json
{
  "StatusCode": 200,
  "ExecutedVersion": "$LATEST"
}

其中 event.json 包含传给 Lambda 函数的事件负载:

1
2
3
4
5
6
7
// event.json
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}

response.json 包含 Lambda 函数返回的响应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// response.json
{
  "statusCode": 200,
  "headers": {
    "content-length": "14",
    "content-type": "application/json"
  },
  "multiValueHeaders": {},
  "body": "\"Hello, world!\"",
  "isBase64Encoded": false
}

细节请参阅 AWS Lambda 文档。

工作区支持

如果项目包含本地依赖(例如通过工作区),这些依赖也必须包含在部署包中。

我们先扩展上面的示例,加入对名为 library 的本地开发库的依赖。

首先创建该库:

1
2
$ uv init --lib library
$ uv add ./library

在 project 目录中运行 uv init 会自动把 project 转换为工作区,并把 library 添加为工作区成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# pyproject.toml
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI 是用于构建 Python API 的现代 Web 框架。
    "fastapi",
    # 一个本地库。
    "library",
    # Mangum 是把 ASGI 应用适配到 AWS Lambda 与 API Gateway 的库。
    "mangum",
]

[dependency-groups]
dev = [
    # 在开发模式下,包含 FastAPI 开发服务器。
    "fastapi[standard]",
]

[tool.uv.workspace]
members = ["library"]

[tool.uv.sources]
lib = { workspace = true }

默认情况下,uv init --lib 会创建一个导出 hello 函数的包。我们修改应用源代码来调用该函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# app/main.py
import logging

from fastapi import FastAPI
from mangum import Mangum

from library import hello

logger = logging.getLogger()
logger.setLevel(logging.INFO)

app = FastAPI()
handler = Mangum(app)


@app.get("/")
async def root() -> str:
    return hello()

我们可以这样在本地运行修改后的应用:

1
$ uv run fastapi dev

并确认在浏览器中打开 http://127.0.0.1:8000/ 显示的是 “Hello from library!”(而不是 “Hello, World!”)。

最后,更新 Dockerfile,把本地库包含进部署包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Dockerfile
FROM ghcr.io/astral-sh/uv:0.12.19 AS uv

# 首先,把依赖打包任务根目录。
FROM public.ecr.aws/lambda/python:3.13 AS builder

# 启用字节码编译以改善冷启动性能。
ENV UV_COMPILE_BYTECODE=1

# 禁用安装器元数据,以创建确定性的层。
ENV UV_NO_INSTALLER_METADATA=1

# 启用复制模式以支持绑定挂载缓存。
ENV UV_LINK_MODE=copy

# 通过 `uv pip install --target` 把依赖打包到 Lambda 任务根目录。
#
# 省略任何本地包(`--no-emit-workspace`)和开发依赖(`--no-dev`)。
# 这样可以确保只有在 `pyproject.toml` 或 `uv.lock` 文件变化时才会失效 Docker 层缓存,
# 同时又能适应应用代码的变化。
RUN --mount=from=uv,source=/uv,target=/bin/uv \
    --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv export --frozen --no-emit-workspace --no-dev --no-editable -o requirements.txt && \
    uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

# 如果你有工作区,也把它复制过来并安装。
#
# 省略 `--no-emit-workspace` 后,`library` 会被复制到任务根目录。使用单独的
# `RUN` 命令可确保所有第三方依赖被单独缓存,并能适应工作区的变化。
RUN --mount=from=uv,source=/uv,target=/bin/uv \
    --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=library,target=library \
    uv export --frozen --no-dev --no-editable -o requirements.txt && \
    uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

FROM public.ecr.aws/lambda/python:3.13

# 从构建阶段复制运行时依赖。
COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}

# 复制应用代码。
COPY ./app ${LAMBDA_TASK_ROOT}/app

# 设置 AWS Lambda 处理程序。
CMD ["app.main.handler"]

提示

要部署到基于 ARM 的 AWS Lambda 运行时,请把 public.ecr.aws/lambda/python:3.13 替换为 public.ecr.aws/lambda/python:3.13-arm64。

接着,我们可以像之前一样构建并部署更新后的镜像。

部署 zip 归档

AWS Lambda 也支持通过 zip 归档部署。对简单应用来说,zip 归档可能比 Docker 镜像更直接、更高效;不过 zip 归档有 250 MB 的体积限制。

回到 FastAPI 示例,我们可以这样把应用依赖打包到本地目录以供 AWS Lambda 使用:

1
2
3
4
5
6
7
8
$ uv export --frozen --no-dev --no-editable -o requirements.txt
$ uv pip install \
   --no-installer-metadata \
   --no-compile-bytecode \
   --python-platform x86_64-manylinux2014 \
   --python 3.13 \
   --target packages \
   -r requirements.txt

提示

要部署到基于 ARM 的 AWS Lambda 运行时,请把 x86_64-manylinux2014 替换为 aarch64-manylinux2014。

按照 AWS Lambda 文档,我们可以这样把这些依赖打包为 zip:

1
2
3
$ cd packages
$ zip -r ../package.zip .
$ cd ..

最后,把应用代码加入 zip 归档:

1
$ zip -r package.zip app

然后就可以通过 AWS 管理控制台或 AWS CLI 把 zip 归档部署到 AWS Lambda,例如:

1
2
3
4
5
6
$ aws lambda create-function \
   --function-name myFunction \
   --runtime python3.13 \
   --zip-file fileb://package.zip \
   --handler app.main.handler \
   --role arn:aws:iam::111122223333:role/service-role/my-lambda-role

其中执行角色通过下面的命令创建:

1
2
3
$ aws iam create-role \
   --role-name my-lambda-role \
   --assume-role-policy-document '{"Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

或者这样更新已有函数:

1
2
3
$ aws lambda update-function-code \
   --function-name myFunction \
   --zip-file fileb://package.zip

注意

默认情况下,AWS 管理控制台假定 Lambda 入口点为 lambda_function.lambda_handler。如果你的应用使用其他入口点,需要在 AWS 管理控制台中修改它。例如上面的 FastAPI 应用使用 app.main.handler。

要测试该 Lambda,我们可以通过 AWS 管理控制台或 AWS CLI 调用它,例如:

1
2
3
4
5
6
7
8
9
$ aws lambda invoke \
   --function-name myFunction \
   --payload file://event.json \
   --cli-binary-format raw-in-base64-out \
   response.json
{
  "StatusCode": 200,
  "ExecutedVersion": "$LATEST"
}

其中 event.json 包含传给 Lambda 函数的事件负载:

1
2
3
4
5
6
7
// event.json
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}

response.json 包含 Lambda 函数返回的响应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// response.json
{
  "statusCode": 200,
  "headers": {
    "content-length": "14",
    "content-type": "application/json"
  },
  "multiValueHeaders": {},
  "body": "\"Hello, world!\"",
  "isBase64Encoded": false
}

使用 Lambda 层

处理 zip 归档时,AWS Lambda 还支持部署多个组合的 Lambda 层。这些层在概念上类似 Docker 镜像中的层,让你可以把应用代码与依赖分开。

特别是,我们可以为应用依赖创建一个 Lambda 层,并把它附加到 Lambda 函数上,与应用代码本身分离。这种安排可以改善应用更新时的冷启动性能,因为依赖层可以跨部署复用。

要创建 Lambda 层,我们遵循类似步骤,但创建两个独立的 zip 归档:一个用于应用代码,一个用于应用依赖。

首先创建依赖层。Lambda 层需要遵循略有不同的结构,因此我们使用 --prefix 而不是 --target:

1
2
3
4
5
6
7
8
$ uv export --frozen --no-dev --no-editable -o requirements.txt
$ uv pip install \
   --no-installer-metadata \
   --no-compile-bytecode \
   --python-platform x86_64-manylinux2014 \
   --python 3.13 \
   --prefix packages \
   -r requirements.txt

然后按照 Lambda 层期望的布局把这些依赖打包为 zip:

1
2
3
$ mkdir python
$ cp -r packages/lib python/
$ zip -r layer_content.zip python

提示

要生成确定性的 zip 归档,可以考虑给 zip 传入 -X 标志,以排除扩展属性和文件系统元数据。

并发布该 Lambda 层:

1
2
3
4
$ aws lambda publish-layer-version --layer-name dependencies-layer \
   --zip-file fileb://layer_content.zip \
   --compatible-runtimes python3.13 \
   --compatible-architectures "x86_64"

接着,我们可以像前面的示例那样创建 Lambda 函数,但不包含依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ # 打包应用代码。
$ zip -r app.zip app

$ # 创建 Lambda 函数。
$ aws lambda create-function \
   --function-name myFunction \
   --runtime python3.13 \
   --zip-file fileb://app.zip \
   --handler app.main.handler \
   --role arn:aws:iam::111122223333:role/service-role/my-lambda-role

最后,使用 publish-layer-version 步骤返回的 ARN,把依赖层附加到 Lambda 函数:

1
2
3
$ aws lambda update-function-configuration --function-name myFunction \
    --cli-binary-format raw-in-base64-out \
    --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:1"

当应用依赖发生变化时,可以重新发布该层并更新 Lambda 函数配置,从而独立于应用更新它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ # 更新层中的依赖。
$ aws lambda publish-layer-version --layer-name dependencies-layer \
   --zip-file fileb://layer_content.zip \
   --compatible-runtimes python3.13 \
   --compatible-architectures "x86_64"

$ # 更新 Lambda 函数配置。
$ aws lambda update-function-configuration --function-name myFunction \
    --cli-binary-format raw-in-base64-out \
    --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:2"
最后修改 September 25, 2026: 更新 (221c74c33)