跳到内容

配合 AWS Lambda 使用 uv

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

您可以将 uv 与 AWS Lambda 结合使用来管理 Python 依赖项、构建部署包和部署 Lambda 函数。

提示

查看 uv-aws-lambda-example 项目,了解使用 uv 将应用程序部署到 AWS Lambda 时的最佳实践示例。

快速入门

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

project
├── pyproject.toml
└── app
    ├── __init__.py
    └── main.py

其中 pyproject.toml 包含

pyproject.toml
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI is a modern web framework for building APIs with Python.
    "fastapi",
    # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway.
    "mangum",
]

[dependency-groups]
dev = [
    # In development mode, include the FastAPI development server.
    "fastapi[standard]>=0.115",
]

并且 main.py 文件包含

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!"

我们可以使用以下命令在本地运行此应用程序

$ uv run fastapi dev

从那里,在 Web 浏览器中打开 http://127.0.0.1:8000/ 将显示“Hello, world!”

部署 Docker 镜像

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

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

在第一阶段,我们将用所有应用程序代码和依赖项填充单个目录。 在第二阶段,我们将此目录复制到最终镜像,省略构建工具和其他不必要的文件。

Dockerfile
FROM ghcr.io/astral-sh/uv:0.8.0 AS uv

# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder

# Enable bytecode compilation, to improve cold-start performance.
ENV UV_COMPILE_BYTECODE=1

# Disable installer metadata, to create a deterministic layer.
ENV UV_NO_INSTALLER_METADATA=1

# Enable copy mode to support bind mount caching.
ENV UV_LINK_MODE=copy

# Bundle the dependencies into the Lambda task root via `uv pip install --target`.
#
# Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`).
# This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock`
# files change, but remains robust to changes in the application code.
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 the runtime dependencies from the builder stage.
COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}

# Copy the application code.
COPY ./app ${LAMBDA_TASK_ROOT}/app

# Set the AWS Lambda handler.
CMD ["app.main.handler"]

提示

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

我们可以使用以下命令构建镜像,例如

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

此 Dockerfile 结构的核心优势如下

  1. 最小镜像大小。 通过使用多阶段构建,我们可以确保最终镜像仅包含应用程序代码和依赖项。 例如,uv 二进制文件本身不包含在最终镜像中。
  2. 最大程度的缓存重用。 通过将应用程序依赖项与应用程序代码分开安装,我们可以确保仅当依赖项更改时 Docker 层缓存才会失效。

具体来说,修改应用程序源代码后重新构建镜像可以重用缓存的层,从而实现毫秒级的构建

 => [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),例如

$ 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,例如

$ 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

其中 执行角色 通过以下方式创建

$ 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"}]}'

或者,使用以下命令更新现有函数

$ 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 调用它,例如

$ 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 函数的事件有效负载

event.json
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}

并且 response.json 包含来自 Lambda 函数的响应

response.json
{
  "statusCode": 200,
  "headers": {
    "content-length": "14",
    "content-type": "application/json"
  },
  "multiValueHeaders": {},
  "body": "\"Hello, world!\"",
  "isBase64Encoded": false
}

有关详细信息,请参见 AWS Lambda 文档

工作区支持

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

我们将首先扩展上面的示例,以包含对名为 library 的本地开发库的依赖。

首先,我们将创建库本身

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

project 目录中运行 uv init 将自动将 project 转换为工作区并将 library 添加为工作区成员

pyproject.toml
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI is a modern web framework for building APIs with Python.
    "fastapi",
    # A local library.
    "library",
    # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway.
    "mangum",
]

[dependency-groups]
dev = [
    # In development mode, include the FastAPI development server.
    "fastapi[standard]",
]

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

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

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

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()

我们可以使用以下命令在本地运行修改后的应用程序

$ uv run fastapi dev

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

最后,我们将更新 Dockerfile 以将本地库包含在部署包中

Dockerfile
FROM ghcr.io/astral-sh/uv:0.8.0 AS uv

# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder

# Enable bytecode compilation, to improve cold-start performance.
ENV UV_COMPILE_BYTECODE=1

# Disable installer metadata, to create a deterministic layer.
ENV UV_NO_INSTALLER_METADATA=1

# Enable copy mode to support bind mount caching.
ENV UV_LINK_MODE=copy

# Bundle the dependencies into the Lambda task root via `uv pip install --target`.
#
# Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`).
# This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock`
# files change, but remains robust to changes in the application code.
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}"

# If you have a workspace, copy it over and install it too.
#
# By omitting `--no-emit-workspace`, `library` will be copied into the task root. Using a separate
# `RUN` command ensures that all third-party dependencies are cached separately and remain
# robust to changes in the workspace.
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 the runtime dependencies from the builder stage.
COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}

# Copy the application code.
COPY ./app ${LAMBDA_TASK_ROOT}/app

# Set the AWS Lambda handler.
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 的本地目录中

$ 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 中

$ cd packages
$ zip -r ../package.zip .
$ cd ..

最后,我们可以将应用程序代码添加到 zip 归档中

$ zip -r package.zip app

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

$ 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

其中 执行角色 通过以下方式创建

$ 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"}]}'

或者,使用以下命令更新现有函数

$ 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 调用它,例如

$ 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 函数的事件有效负载

event.json
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}

并且 response.json 包含来自 Lambda 函数的响应

response.json
{
  "statusCode": 200,
  "headers": {
    "content-length": "14",
    "content-type": "application/json"
  },
  "multiValueHeaders": {},
  "body": "\"Hello, world!\"",
  "isBase64Encoded": false
}

使用 Lambda 层

AWS Lambda 还支持在使用 zip 归档时部署多个组合的 Lambda 层。 这些层在概念上类似于 Docker 镜像中的层,使您可以将应用程序代码与依赖项分开。

特别是,我们可以为应用程序依赖项创建一个 lambda 层,并将其附加到 Lambda 函数,与应用程序代码本身分开。 这种设置可以提高应用程序更新的冷启动性能,因为可以在部署之间重用依赖项层。

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

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

$ 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 层的预期布局压缩依赖项

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

提示

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

并发布 Lambda 层

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

然后,我们可以像在前面的示例中一样创建 Lambda 函数,省略依赖项

$ # Zip the application code.
$ zip -r app.zip app

$ # Create the Lambda function.
$ 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 函数

$ 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 函数配置来独立于应用程序更新层

$ # Update the dependencies in the layer.
$ aws lambda publish-layer-version --layer-name dependencies-layer \
   --zip-file fileb://layer_content.zip \
   --compatible-runtimes python3.13 \
   --compatible-architectures "x86_64"

$ # Update the Lambda function configuration.
$ 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"