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"