diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md new file mode 100644 index 000000000..115d94bd5 --- /dev/null +++ b/docs/guides/integration/aws-lambda.md @@ -0,0 +1,416 @@ +# Using uv with AWS Lambda + +[AWS Lambda](https://aws.amazon.com/lambda/) is a serverless computing service that lets you run +code without provisioning or managing servers. + +You can use uv with AWS Lambda to manage your Python dependencies, build your deployment package, +and deploy your Lambda functions. + +!!! tip + + Check out the [`uv-aws-lambda-example`](https://github.com/astral-sh/uv-aws-lambda-example) project for + an example of best practices when using uv to deploy an application to AWS Lambda. + +## Getting started + +To start, assume we have a minimal FastAPI application with the following structure: + +```plaintext +project +├── pyproject.toml +└── app + ├── __init__.py + └── main.py +``` + +Where the `pyproject.toml` contains: + +```toml title="pyproject.toml" +[project] +name = "uv-aws-lambda-example" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + # FastAPI is a modern web framework for building APIs with Python. + "fastapi", + # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway. + "mangum", +] + +[dependency-groups] +dev = [ + # In development mode, include the FastAPI development server. + "fastapi[standard]>=0.115", +] +``` + +And the `main.py` file contains: + +```python title="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!" +``` + +We can run this application locally with: + +```console +$ uv run fastapi dev +``` + +From there, opening http://127.0.0.1:8000/ in a web browser will display "Hello, world!" + +## Deploying a Docker image + +To deploy to AWS Lambda, we need to build a container image that includes the application code and +dependencies in a single output directory. + +We'll follow the principles outlined in the [Docker guide](./docker.md) (in particular, a +multi-stage build) to ensure that the final image is as small and cache-friendly as possible. + +In the first stage, we'll populate a single directory with all application code and dependencies. In +the second stage, we'll copy this directory over to the final image, omitting the build tools and +other unnecessary files. + +```dockerfile title="Dockerfile" +FROM ghcr.io/astral-sh/uv:0.5.15 AS uv + +# First, bundle the dependencies into the task root. +FROM public.ecr.aws/lambda/python:3.13 AS builder + +# Enable bytecode compilation. +ENV UV_COMPILE_BYTECODE=1 + +# Enable copy mode to support bind mount caching. +ENV UV_LINK_MODE=copy + +# Bundle the dependencies into the Lambda task root via `uv pip install --target`. +# +# Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`). +# This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock` +# files change, but remains robust to changes in the application code. +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 the runtime dependencies from the builder stage. +COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} + +# Copy the application code. +COPY ./app ${LAMBDA_TASK_ROOT}/app + +# Set the AWS Lambda handler. +CMD ["app.main.handler"] +``` + +!!! tip + + To deploy to ARM-based AWS Lambda runtimes, replace `public.ecr.aws/lambda/python:3.13` with `public.ecr.aws/lambda/python:3.13-arm64`. + +We can build the image with, e.g.: + +```console +$ uv lock +$ docker build -t fastapi-app . +``` + +The core benefits of this Dockerfile structure are as follows: + +1. **Minimal image size.** By using a multi-stage build, we can ensure that the final image only + includes the application code and dependencies. For example, the uv binary itself is not included + in the final image. +2. **Maximal cache reuse.** By installing application dependencies separately from the application + code, we can ensure that the Docker layer cache is only invalidated when the dependencies change. + +Concretely, rebuilding the image after modifying the application source code can reuse the cached +layers, resulting in millisecond builds: + +```console + => [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 +``` + +After building, we can push the image to +[Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) with, e.g.: + +```console +$ 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 +``` + +Finally, we can deploy the image to AWS Lambda using the AWS Management Console or the AWS CLI, +e.g.: + +```console +$ 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 +``` + +Where the +[execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-api) +is created via: + +```console +$ 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"}]}' +``` + +Or, update an existing function with: + +```console +$ aws lambda update-function-code \ + --function-name myFunction \ + --image-uri aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest \ + --publish +``` + +For details, see the +[AWS Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/python-image.html). + +### Workspace support + +If a project includes local dependencies (e.g., via +[Workspaces](../../concepts/projects/workspaces.md), those too must be included in the deployment +package. + +We'll start by extending the above example to include a dependency on a locally-developed library +named `library`. + +First, we'll create the library itself: + +```console +$ uv init --lib library +$ uv add ./library +``` + +Running `uv init` within the `project` directory will automatically convert `project` to a workspace +and add `library` as a workspace member: + +```toml title="pyproject.toml" +[project] +name = "uv-aws-lambda-example" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + # FastAPI is a modern web framework for building APIs with Python. + "fastapi", + # A local library. + "library", + # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway. + "mangum", +] + +[dependency-groups] +dev = [ + # In development mode, include the FastAPI development server. + "fastapi[standard]", +] + +[tool.uv.workspace] +members = ["library"] + +[tool.uv.sources] +lib = { workspace = true } +``` + +By default, `uv init --lib` will create a package that exports a `hello` function. We'll modify the +application source code to call that function: + +```python title="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() +``` + +We can run the modified application locally with: + +```console +$ uv run fastapi dev +``` + +And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hello from library!" +(instead of "Hello, World!") + +Finally, we'll update the Dockerfile to include the local library in the deployment package: + +```dockerfile title="Dockerfile" +FROM ghcr.io/astral-sh/uv:0.5.15 AS uv + +# First, bundle the dependencies into the task root. +FROM public.ecr.aws/lambda/python:3.13 AS builder + +# Enable bytecode compilation. +ENV UV_COMPILE_BYTECODE=1 + +# Enable copy mode to support bind mount caching. +ENV UV_LINK_MODE=copy + +# Bundle the dependencies into the Lambda task root via `uv pip install --target`. +# +# Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`). +# This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock` +# files change, but remains robust to changes in the application code. +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}" + +# If you have a workspace, copy it over and install it too. +# +# By omitting `--no-emit-workspace`, `library` will be copied into the task root. Using a separate +# `RUN` command ensures that all third-party dependencies are cached separately and remain +# robust to changes in the workspace. +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 the runtime dependencies from the builder stage. +COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} + +# Copy the application code. +COPY ./app ${LAMBDA_TASK_ROOT}/app + +# Set the AWS Lambda handler. +CMD ["app.main.handler"] +``` + +!!! tip + + To deploy to ARM-based AWS Lambda runtimes, replace `public.ecr.aws/lambda/python:3.13` with `public.ecr.aws/lambda/python:3.13-arm64`. + +From there, we can build and deploy the updated image as before. + +## Deploying a zip archive + +AWS Lambda also supports deployment via zip archives. For simple applications, zip archives can be a +more straightforward and efficient deployment method than Docker images; however, zip archives are +limited to +[250 MB](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html#python-package-create-update) +in size. + +Returning to the FastAPI example, we can bundle the application dependencies into a local directory +for AWS Lambda via: + +```console +$ uv export --frozen --no-dev --no-editable -o requirements.txt +$ uv pip install \ + --no-compile-bytecode \ + --python-platform x86_64-manylinux2014 \ + --python-version 3.13 \ + --target packages \ + -r requirements.txt +``` + +!!! tip + + To deploy to ARM-based AWS Lambda runtimes, replace `x86_64-manylinux2014` with `aarch64-manylinux2014`. + +Following the +[AWS Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html), we can +then bundle these dependencies into a zip as follows: + +```console +$ cd packages +$ zip -r ../package.zip . +``` + +Finally, we can add the application code to the zip archive: + +```console +$ cd .. +$ zip -r package.zip app +``` + +We can then deploy the zip archive to AWS Lambda via the AWS Management Console or the AWS CLI, +e.g.: + +```console +$ 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 +``` + +Where the +[execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-api) +is created via: + +```console +$ 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"}]}' +``` + +Or, update an existing function with: + +```console +$ aws lambda update-function-code \ + --function-name myFunction \ + --zip-file fileb://package.zip +``` + +!!! note + + By default, the AWS Management Console assumes a Lambda entrypoint of `lambda_function.lambda_handler`. + If your application uses a different entrypoint, you'll need to modify it in the AWS Management Console. + For example, the above FastAPI application uses `app.main.handler`. diff --git a/docs/guides/integration/index.md b/docs/guides/integration/index.md index 3e00d7bf2..209ec358a 100644 --- a/docs/guides/integration/index.md +++ b/docs/guides/integration/index.md @@ -10,6 +10,7 @@ Learn how to integrate uv with other software: - [Using with alternative package indexes](./alternative-indexes.md) - [Installing PyTorch](./pytorch.md) - [Building a FastAPI application](./fastapi.md) +- [Using with AWS Lambda](./aws-lambda.md) Or, explore the [concept documentation](../../concepts/index.md) for comprehensive breakdown of each feature. diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 0753abbdb..ef1ca20d3 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -112,6 +112,7 @@ nav: - FastAPI: guides/integration/fastapi.md - Alternative indexes: guides/integration/alternative-indexes.md - Dependency bots: guides/integration/dependency-bots.md + - AWS Lambda: guides/integration/aws-lambda.md - Concepts: - concepts/index.md - Projects: diff --git a/pyproject.toml b/pyproject.toml index cc8cc9715..0ef523cf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ version_files = [ "docs/guides/integration/docker.md", "docs/guides/integration/pre-commit.md", "docs/guides/integration/github.md", + "docs/guides/integration/aws-lambda.md", ] [tool.mypy]