Skip to content

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 파일은 다음과 같다:

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 파일은 다음과 같다:

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

그런 다음 웹 브라우저에서 http://127.0.0.1:8000/을 열면 "Hello, world!"가 표시된다.

도커 이미지 배포하기

AWS Lambda에 배포하려면 애플리케이션 코드와 의존성을 단일 출력 디렉터리에 포함한 컨테이너 이미지를 빌드해야 한다.

Docker 가이드에 설명된 원칙(특히 멀티스테이지 빌드)을 따라 최종 이미지가 가능한 한 작고 캐시 친화적으로 만들어야 한다.

첫 번째 단계에서는 모든 애플리케이션 코드와 의존성을 단일 디렉터리에 모은다. 두 번째 단계에서는 이 디렉터리를 최종 이미지로 복사하면서 빌드 도구와 불필요한 파일은 제외한다.

Dockerfile
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.13public.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 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 함수에 전달할 이벤트 페이로드를 포함한다:

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는 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 --libhello 함수를 내보내는 패키지를 생성한다. 애플리케이션 소스 코드를 수정해 이 함수를 호출하도록 한다:

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

웹 브라우저에서 http://127.0.0.1:8000/을 열면 "Hello, World!" 대신 "Hello from library!"가 표시되는지 확인한다.

마지막으로 Dockerfile을 업데이트해 배포 패키지에 로컬 라이브러리를 포함시킨다:

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.13public.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-manylinux2014aarch64-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 함수에 전달할 이벤트 페이로드를 포함한다.

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 레이어는 약간 다른 구조를 따르므로 --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"