AWS Lambda에서 uv 사용하기¶
AWS Lambda는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있는 서버리스 컴퓨팅 서비스다.
uv를 AWS Lambda와 함께 사용하면 Python 의존성을 관리하고, 배포 패키지를 빌드하며, Lambda 함수를 배포할 수 있다.
Tip
uv를 사용해 애플리케이션을 AWS Lambda에 배포하는 최선의 사례를 확인하려면 uv-aws-lambda-example 프로젝트를 참고한다.
시작하기¶
시작하기 전에, 다음과 같은 구조의 간단한 FastAPI 애플리케이션이 있다고 가정한다:
project
├── pyproject.toml
└── app
    ├── __init__.py
    └── main.py
pyproject.toml 파일은 다음과 같다:
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI는 Python으로 API를 구축하기 위한 모던 웹 프레임워크다.
    "fastapi",
    # Mangum은 ASGI 애플리케이션을 AWS Lambda와 API Gateway에 적응시키는 라이브러리다.
    "mangum",
]
[dependency-groups]
dev = [
    # 개발 모드에서는 FastAPI 개발 서버를 포함한다.
    "fastapi[standard]>=0.115",
]
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
그런 다음 웹 브라우저에서 http://127.0.0.1:8000/을 열면 "Hello, world!"가 표시된다.
도커 이미지 배포하기¶
AWS Lambda에 배포하려면 애플리케이션 코드와 의존성을 단일 출력 디렉터리에 포함한 컨테이너 이미지를 빌드해야 한다.
Docker 가이드에 설명된 원칙(특히 멀티스테이지 빌드)을 따라 최종 이미지가 가능한 한 작고 캐시 친화적으로 만들어야 한다.
첫 번째 단계에서는 모든 애플리케이션 코드와 의존성을 단일 디렉터리에 모은다. 두 번째 단계에서는 이 디렉터리를 최종 이미지로 복사하면서 빌드 도구와 불필요한 파일은 제외한다.
FROM ghcr.io/astral-sh/uv:0.6.2 AS uv
# 먼저, 의존성을 task 루트에 번들링한다.
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 task 루트에 번들링한다.
#
# 로컬 패키지(`--no-emit-workspace`)와 개발 의존성(`--no-dev`)은 제외한다.
# 이를 통해 Docker 레이어 캐시가 `pyproject.toml` 또는 `uv.lock` 파일이 변경될 때만 무효화되고,
# 애플리케이션 코드 변경에는 영향을 받지 않도록 한다.
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"]
Tip
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 구조의 주요 장점은 다음과 같다:
- 최소한의 이미지 크기. 멀티스테이지 빌드를 사용해 최종 이미지에 애플리케이션 코드와 의존성만 포함되도록 한다. 예를 들어, uv 바이너리 자체는 최종 이미지에 포함되지 않는다.
 - 최대 캐시 재사용. 애플리케이션 의존성을 애플리케이션 코드와 별도로 설치해 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 Management Console 또는 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 Management Console 또는 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 함수에 전달할 이벤트 페이로드를 포함한다:
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}
그리고 response.json은 Lambda 함수의 응답을 포함한다:
{
  "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를 워크스페이스 멤버로 추가한다:
[project]
name = "uv-aws-lambda-example"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    # FastAPI는 Python으로 API를 구축하기 위한 모던 웹 프레임워크다.
    "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 함수를 내보내는 패키지를 생성한다. 애플리케이션 소스 코드를 수정해 이 함수를 호출하도록 한다:
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
웹 브라우저에서 http://127.0.0.1:8000/을 열면 "Hello, World!" 대신 "Hello from library!"가 표시되는지 확인한다.
마지막으로 Dockerfile을 업데이트해 배포 패키지에 로컬 라이브러리를 포함시킨다:
FROM ghcr.io/astral-sh/uv:0.6.2 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`)을 생략한다.
# 이렇게 하면 Docker 레이어 캐시가 `pyproject.toml` 또는 `uv.lock` 파일이 변경될 때만 무효화되고,
# 애플리케이션 코드 변경에는 영향을 받지 않는다.
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"]
Tip
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
Tip
ARM 기반 AWS Lambda 런타임에 배포하려면 x86_64-manylinux2014를 aarch64-manylinux2014로 대체한다.
AWS Lambda 문서를 참고하여 의존성을 zip 파일로 묶는다.
$ cd packages
$ zip -r ../package.zip .
$ cd ..
마지막으로 애플리케이션 코드를 zip 아카이브에 추가한다.
$ zip -r package.zip app
이제 zip 아카이브를 AWS Management Console이나 AWS CLI를 통해 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
Note
기본적으로 AWS Management Console은 Lambda 엔트리포인트를 lambda_function.lambda_handler로 간주한다. 애플리케이션이 다른 엔트리포인트를 사용한다면 AWS Management Console에서 수정해야 한다. 예를 들어 위의 FastAPI 애플리케이션은 app.main.handler를 사용한다.
Lambda를 테스트하려면 AWS Management Console이나 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 함수에 전달할 이벤트 페이로드를 포함한다.
{
  "httpMethod": "GET",
  "path": "/",
  "requestContext": {},
  "version": "1.0"
}
그리고 response.json은 Lambda 함수의 응답을 포함한다.
{
  "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 레이어는 약간 다른 구조를 따르므로 --target 대신 --prefix를 사용한다:
$ 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
Tip
결정론적 zip 아카이브를 생성하려면 zip에 -X 플래그를 전달해 확장 속성과 파일 시스템 메타데이터를 제외할 수 있다.
그리고 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 -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 함수에 연결한다:
$ 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 함수 설정을 업데이트해 독립적으로 업데이트할 수 있다:
$ # 레이어의 의존성을 업데이트한다.
$ 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"