使用 uv Workspaces 和 AWS Lambda 管理 Python Monorepos
Source: Dev.to
抱歉,我无法直接访问外部链接或检索该页面的完整内容。如果您能提供需要翻译的文本(除代码块和 URL 之外),我很乐意帮您将其翻译成简体中文,并保持原有的 Markdown 格式和技术术语不变。
UV 工作区 – 快速概览
UV 工作区在开发相互关联的 Python 包时是一个 超级工具,尤其是在单仓库(mono‑repo)设置中。
如果你的仓库根目录有 pyproject.toml,并在子文件夹内运行 uv init,UV 将会:
-
创建一个包含所有项目依赖的单一虚拟环境。
对 IDE 很友好 —— 你不需要不断切换 venv。
⚠️ 重要提示: 请参见下文。 -
通过在
[tool.uv.sources]下添加本地项目来引用它。UV 会把它安装为 editable 包,这样每次修改后都不需要重新构建/重新安装。
[tool.uv.sources]
common_logging = { workspace = true }
注意事项
| 注意事项 | 描述 |
|---|---|
| 冲突的依赖 | 如果任何工作区成员需要不兼容的版本,uv sync 将会失败。对于微服务来说,最好保持依赖版本的兼容性。 |
| IDE 误报 | 由于只有一个 venv,IDE 可能会建议在某个服务的依赖中并未声明的导入。这在本地开发时可能不易察觉。在这种情况下,你可能更倾向于使用 路径依赖 而不是工作区条目: |
[tool.uv.sources]
common_logging = { path = "../common/logging", editable = true }
使用 UV 工作区于 CI / 生产
我一直在使用 UV 工作区,但我们有一个 CI 检查会在代码进入生产之前捕获 “冲突” 问题。
下面的示例假设 AWS Lambda 容器镜像,因此会出现类似 /var/task 和 /var/lang/lib 的路径。相同的方法也适用于非 Lambda 容器,只需相应地调整基础镜像和路径即可。
两个关键需求
- 选择性安装 – 并非每个工作区依赖都应为每个微服务安装。
- 层缓存 – 核心(共享)依赖应位于与服务特定(本地)依赖分开的 Docker 层中。这一点对 Lambda 镜像尤为重要,因为镜像大小和层缓存会直接影响冷启动性能。
Docker 构建 – 步骤详解
下面是一个已清理、可复现的 Dockerfile,具备以下特点:
- 仅复制必要的
pyproject.toml和uv.lock文件。 - 使用
--package和--no‑install‑local安装 核心(全工作区)依赖。 - 根据工作区图安装 本地 依赖。
- 复制最终的服务源代码。
# -------------------------------------------------
# Build Stage – install dependencies with UV
# -------------------------------------------------
FROM public.ecr.aws/lambda/python:3.14-arm64 AS builder
# Install UV (binary from the official image)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ARG SERVICE_NAME
# Working directory for the build
WORKDIR /build
# -------------------------------------------------
# 1️⃣ Copy dependency definition files
# -------------------------------------------------
COPY pyproject.toml uv.lock /build/
COPY services/${SERVICE_NAME}/pyproject.toml /build/services/${SERVICE_NAME}/pyproject.toml
# -------------------------------------------------
# 2️⃣ Install **core** (shared) dependencies
# -------------------------------------------------
# --no-install-local → ignore other workspace members
# --no-dev → skip dev dependencies
# --package ${SERVICE_NAME} → install only the package we care about
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-local --no-dev --package ${SERVICE_NAME}
# -------------------------------------------------
# 3️⃣ Install **common** (local) dependencies
# -------------------------------------------------
# Export the exact requirements for the service, filter only the
# `services/common` entries, then install them with pip into a clean target.
RUN uv export --package ${SERVICE_NAME} \
--no-editable --no-dev --frozen --format requirements.txt \
| grep '^./services/common' > common.txt
RUN --mount=type=bind,source=services/common,target=/build/services/common \
uv pip install --no-deps -r common.txt --target /build/common
# -------------------------------------------------
# 4️⃣ Copy the application code (service source)
# -------------------------------------------------
COPY services/${SERVICE_NAME}/src /build/app
最终运行时镜像
运行时镜像仅包含服务所需的内容:核心依赖、公共(本地)依赖以及服务代码。没有 UV 二进制文件、没有构建时缓存,也没有重复的源码拷贝。
# -------------------------------------------------
# Runtime Stage – minimal image
# -------------------------------------------------
FROM public.ecr.aws/lambda/python:3.14-arm64 AS runtime
ARG SERVICE_NAME
# Copy the prepared environment from the builder
COPY --from=builder /build/common /var/task/
COPY --from=builder /build/app /var/task/
# (Optional) Set PYTHONPATH if you need to point to the venv location
# ENV PYTHONPATH=/var/task/.venv/lib/python3.14/site-packages
# The Lambda base image automatically looks for a handler in /var/task
# No further commands are required.
为什么采用两阶段方法?
- 干净的最终镜像 – 只保留运行时依赖;构建时的产物(UV、缓存、重复的源码树)都会被抛弃。
- 友好的缓存层 – 核心依赖很少变化,Docker 可以在构建之间复用该层。本地/通用依赖安装在单独的层,服务代码是最后一层,这样只有代码变动时即可快速重建。
- UV 负责解析,pip 负责安装 – UV 仍然负责繁重的依赖解析,而
pip install --target …会创建一个简单的、独立的 site‑packages 目录,Lambda 运行时可以直接使用。
TL;DR
- UV workspaces 为整个仓库提供单一的虚拟环境,这对 IDE 的使用体验非常友好。
- 保持对 冲突的依赖 和 IDE 误报 的警惕;如有必要使用路径依赖。
- 针对 Docker/Lambda 构建,将 core 和 local 依赖的安装分离到不同的层中,然后仅将所需文件复制到最小的运行时镜像中。
- 上述模式在保持镜像体积小、缓存高效、可直接用于生产的同时,仍然利用了 UV 强大的依赖解析能力。
# Use the Python 3.14 runtime for ARM64
FROM python:3.14-arm64 AS final
# Copy core dependencies from the virtual environment built earlier
COPY --from=builder /build/.venv/lib /var/lang/lib
# Copy shared/common files
COPY --from=builder /build/common /var/task
# Copy the application code
COPY --from=builder /build/app /var/task
# Set the command to your Lambda handler
CMD [ "ingestion_worker.main.handler" ]
这将生成一个更小、更干净的最终镜像,仅包含 Lambda 运行时所需的内容。