🕌

AWS CDK Python × uv × Docker で実現する複数の Lambda Function の Monorepo 管理

に公開

はじめに

色々なツールを AWS Lambda で作り、各ツールのコードを管理するリポジトリの数がどんどん増えてきた時に思いました。「そうだ Monorepo しよう。」

そんな中、Python のパッケージ管理ツールで「uv がアツい」という噂を聞いた時に思いました。「そうだ uv 使おう。」

AWS CDK も Python を使えば、インフラからアプリケーションコードまで全て Python で統一できると気づいた時に思いました。「そうだ インフラからアプリコードまで全部 Monorepo にして uv で管理しよう。」

このブログでは、実際にやってみて実現できた方法をシェアします。

※ GitHub リポジトリはこちら。
https://github.com/yendoooo/lambda-monorepo

ソフトウェアバージョン

  • Python: 3.11.8
  • Docker: 27.3.1
  • uv: 0.6.14
  • AWS CLI: 2.13.32
  • AWS CDK CLI: 2.1010.0
  • Node.js: v22.13.1
  • npm: 10.9.2

プロジェクト構成

今回実現した Monorepo 構成のベースは以下の通りです。

lambda-monorepo/
├── functions/
│   ├── function_a/
│   │   ├── Dockerfile
│   │   └── lambda_function.py
│   └── function_b/
│       ├── Dockerfile
│       └── lambda_function.py
├── infra/
│   └── lambda_monorepo_stack.py
├── tests/
├── app.py
├── cdk.json
├── pyproject.toml
└── uv.lock

functions/: 各 Lambda Function のソースコード、Dockerfile を格納。
infra/: インフラを定義する CDK のコードを格納。
tests/: 各種テストコードを格納。
app.py: CDK プロジェクトのエントリーポイントとなるファイル。
cdk.json: CDK プロジェクト全体の設定ファイル。
pyproject.toml: プロジェクト全体の依存関係を管理するための設定ファイル。
uv.lock: pyproject.toml に基づいて解決された依存関係のロックファイル。

ベース構成構築手順

  1. プロジェクトディレクトリの作成
    mkdir lambda-monorepo
    
  2. CDK プロジェクトの初期化
    cd lambda-monorepo && cdk init app --language python
    
  3. uv の初期化
    uv init
    
  4. 仮想環境の起動
    source .venv/bin/activate
    
  5. 既存の依存関係の移行
    uv add --group infra -r requirements.txt && \
    uv add --dev -r requirements-dev.txt
    
  6. ディレクトリの整理
    mv lambda_monorepo infra && \
    mkdir functions && \
    rm main.py requirements.txt requirements-dev.txt
    

Lambda functions の作成

今回はサンプルとして 2 つの Lambda Function を作成します。

  1. S3 Bucket 一覧を取得する Function [依存関係: Boto3]
functions/function_a/lambda_function.py
import json
import boto3

client = boto3.client('s3')

def lambda_handler(event, context):
    '''S3 Bucket の一覧を取得する'''
    try:
        response = client.list_buckets()
        return {
            'statusCode': 200,
            'body': json.dumps(
                {
                    'message': 'Successfully retrieved buckets.',
                    'buckets': [bucket['Name'] for bucket in response['Buckets']],
                }
            )
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(
                {
                    'message': 'Unexpected error occurred',
                    'error': str(e),
                }
            )
        }
  1. hello world! を返却する Function [依存関係: FastAPI, Mangum]
functions/function_b/lambda_function.py
from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

@app.get('/')
async def root():
    '''hello world! を返却する'''
    return 'hello world!'

lambda_handler = Mangum(app)

AWS CDK Python によるインフラ定義

続いて Lambda Function のリソースを作成します。

app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from infra.lambda_monorepo_stack import LambdaMonorepoStack

app = cdk.App()

LambdaMonorepoStack(
    app,
    'LambdaMonorepo',
    env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),
)

app.synth()
infra/lambda_monorepo_stack.py
from aws_cdk import Stack
from aws_cdk import aws_iam as iam
from aws_cdk import aws_lambda as _lambda
from constructs import Construct

class LambdaMonorepoStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        function_a = _lambda.Function(
            self,
            'FunctionA',
            code=_lambda.Code.from_asset_image(
                directory='.',
                file='functions/function_a/Dockerfile',
            ),
            handler=_lambda.Handler.FROM_IMAGE,
            runtime=_lambda.Runtime.FROM_IMAGE,
        )
        function_a.add_to_role_policy(
            iam.PolicyStatement(
                actions=['s3:ListAllMyBuckets'],
                resources=['*'],
            )
        )

        _lambda.Function(
            self,
            'FunctionB',
            code=_lambda.Code.from_asset_image(
                directory='.',
                file='functions/function_b/Dockerfile',
            ),
            handler=_lambda.Handler.FROM_IMAGE,
            runtime=_lambda.Runtime.FROM_IMAGE,
        )

uv による依存関係の一元管理

各 Lambda Function で必要な依存関係は異なっており、また、それぞれのコンテナイメージに不要なパッケージは含めたくありません。(コンテナイメージ軽量化のため)
そのため、それぞれの依存関係は個別に管理できるよう、uv の group 機能を活用します。

uv add --group function-a boto3 && \
uv add --group function-b fastapi mangum

ここまでで、pyproject.toml の内容は以下のようになっているはずです。
各 Lambda Function の依存関係がグループ毎に分かれていて、一目瞭然でわかりやすいと思います。

pyproject.toml
[project]
name = "lambda-monorepo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []

[dependency-groups]
dev = [
    "pytest==6.2.5",
]
function-a = [
    "boto3>=1.37.38",
]
function-b = [
    "fastapi>=0.115.12",
    "mangum>=0.19.0",
]
infra = [
    "aws-cdk-lib==2.189.0",
    "constructs>=10.0.0,<11.0.0",
]

各 Lambda Function 用の Dockerfile を作成する際も、uv の group 機能を活用します。

functions/function_a/Dockerfile
ARG PYTHON_VERSION=3.11

FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder
WORKDIR ${LAMBDA_TASK_ROOT}

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

ENV UV_COMPILE_BYTECODE=1 \
    UV_NO_INSTALLER_METADATA=1 \
    UV_LINK_MODE=copy

RUN --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 sync --group function-a --frozen


FROM public.ecr.aws/lambda/python:${PYTHON_VERSION}

COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}
COPY ./functions/function_a/lambda_function.py ${LAMBDA_TASK_ROOT}

CMD ["lambda_function.lambda_handler"]
functions/function_b/Dockerfile
ARG PYTHON_VERSION=3.11

FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

ENV UV_COMPILE_BYTECODE=1 \
    UV_NO_INSTALLER_METADATA=1 \
    UV_LINK_MODE=copy

RUN --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 --group function-b --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:${PYTHON_VERSION}

COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}
COPY ./functions/function_b/lambda_function.py ${LAMBDA_TASK_ROOT}

CMD ["lambda_function.lambda_handler"]

デプロイ & テスト

作成した Lambda Functions をデプロイします。

cdk deploy

デプロイが成功しました。

 ✅  LambdaMonorepo

✨  Deployment time: 38.33s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/LambdaMonorepo/xxxxxxxx

✨  Total time: 62.57s

テストも成功しました。

function_a

function_b

まとめ

本記事では、AWS CDK Python, uv, Docker を組み合わせることで、複数の Lambda Function を Monorepo で効率的に管理・デプロイする方法を紹介しました。Monorepo での複数プロジェクト管理に悩んでいる方のヒントになれば嬉しいです。もっと良い方法があれば、ぜひコメント等で教えてください!

最後に

株式会社 Penetrator はシリーズ A ラウンドにおいて総額 5.5 億円の資金調達を実施し、不動産テック業界における更なる成長を目指して、採用活動を一層強化しています。エンジニア、デザイナー、カスタマーサクセス、BizDev、営業、マーケティングなど、事業拡大を支える多様なポジションで共に挑戦していただける方を待っています!

▽ 会社のカルチャーを知りたい方はこちら

https://www.wantedly.com/companies/company_9924832

▽ 募集職種を知りたい方はこちら

https://hrmos.co/pages/where/jobs

Discussion