uv Workspaces와 AWS Lambda를 활용한 Python Monorepo 관리
I’m happy to translate the article for you, but I need the text you’d like translated. Could you please paste the content of the article (or the specific sections you want translated) here? Once I have the text, I’ll provide a Korean translation while preserving the original formatting, markdown, and code blocks.
Source: …
UV Workspaces – A Quick Overview
UV workspaces는 특히 모노레포 환경에서 상호 연결된 Python 패키지를 개발할 때 슈퍼‑툴입니다.
레포 루트에 pyproject.toml이 있고 서브 폴더 안에서 uv init을 실행하면 UV는 다음을 수행합니다:
-
모든 프로젝트의 종속성을 포함하는 단일 가상 환경을 생성합니다.
IDE에 친화적 – 가상 환경을 계속 전환할 필요가 없습니다.
⚠️ 중요한 주의사항: 아래를 참고하세요. -
[tool.uv.sources]에 로컬 프로젝트를 추가하여 편집 가능한 패키지로 설치합니다. 따라서 변경 후마다 재빌드/재설치할 필요가 없습니다.
[tool.uv.sources]
common_logging = { workspace = true }
Caveats
| Caveat | Description |
|---|---|
| Conflicting dependencies | 워크스페이스 구성원 중 하나라도 호환되지 않는 버전을 요구하면 uv sync가 실패합니다. 마이크로서비스 환경에서는 일반적으로 호환 가능한 버전으로 종속성을 맞추는 것이 좋습니다. |
| IDE false‑positives | 가상 환경이 하나뿐이기 때문에 IDE가 실제로 해당 서비스의 종속성에 선언되지 않은 import를 제안할 수 있습니다. 이는 로컬 개발 중에 눈에 띄지 않을 수 있습니다. 이런 경우 워크스페이스 항목 대신 경로 종속성을 사용하는 것이 좋습니다: |
[tool.uv.sources]
common_logging = { path = "../common/logging", editable = true }
CI / 프로덕션에서 UV 워크스페이스 사용
저는 계속 UV 워크스페이스를 사용하고 있지만, 코드가 프로덕션에 도달하기 전에 “conflict” 문제를 잡아내는 CI 검사가 있습니다.
아래 예시는 AWS Lambda 컨테이너 이미지를 전제로 하며, /var/task 및 /var/lang/lib와 같은 경로를 설명합니다. 동일한 접근 방식은 Lambda가 아닌 컨테이너에서도 작동합니다; 단지 베이스 이미지와 경로만 조정하면 됩니다.
두 가지 핵심 요구사항
- 선택적 설치 – 모든 워크스페이스 의존성을 모든 마이크로서비스에 설치할 필요는 없습니다.
- 레이어 캐싱 – 핵심(공유) 의존성은 서비스‑특정(로컬) 의존성과 별도의 Docker 레이어에 존재해야 합니다. 이는 이미지 크기와 레이어 캐싱이 콜드‑스타트 성능에 직접 영향을 주기 때문에 Lambda 이미지에서 중요합니다.
Docker Build – 단계별
아래는 정리된 재현 가능한 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.
왜 이 2단계 접근법인가?
- 깨끗한 최종 이미지 – 런타임 의존성만 남고, 빌드 시 생성된 아티팩트(UV, 캐시, 중복 소스 트리)는 남지 않습니다.
- 캐시 친화적인 레이어 – 핵심 의존성은 거의 변경되지 않으므로 Docker가 해당 레이어를 재사용할 수 있습니다. 로컬/공통 의존성은 별도 레이어에 설치하고, 서비스 코드는 마지막 레이어에 두어 코드만 변경될 때 빠르게 재빌드됩니다.
- 해결은 UV, 설치는 pip – UV가 의존성 해결의 무거운 작업을 수행하고,
pip install --target …가 Lambda 런타임이 직접 사용할 수 있는 단순하고 독립적인 site‑packages 디렉터리를 생성합니다.
TL;DR
- UV workspaces는 전체 레포지토리에서 단일 venv를 제공하므로 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가 런타임에 실제로 필요로 하는 것만 포함된 더 작고 깔끔한 최종 이미지가 생성됩니다.