uv, ruff, docker を活かした AWS CDK の Python Lambda プロジェクト

記事タイトルとURLをコピーする

はじめに

本記事はサーバーワークス Advent Calendar 2024の9日目の記事です。

qiita.com

uv をコンテナ開発で利用したところ、Dockerfileの記述量が少なくコンテナイメージのビルドが高速でしたので、CDKでコンテナイメージを使用した Lambda 関数を作成する小さなサンプルプロジェクトを書いてみました。

特にこの記事では、CDKの公式ドキュメントである AWS Cloud Development Kit (AWS CDK) v2 開発者ガイドcdk init sample-app... コマンドで立ち上げた状態のプロジェクトから、極力小さい変更手数で構築したサンプルプロジェクトの形式で紹介します。以下5つの要素を含むプロジェクトです。

  • AWS CDK を用いた IaC ができる
  • コンテナイメージを使用した Lambda 関数
    • Amazon ECR Public Gallery の Python Lambda イメージ を使っている
  • uv を用いた python ランタイムやパッケージの管理ができる
  • ruff を用いた linting, formatting ができる

構成図

とてもシンプルですね。 SQSをトリガーとするLambda関数1個

結論

以下がコードです。

github.com

Quick Start

$ git clone git@github.com:hiromitsu-sai/cdk-uv-python-example-forblog.git
$ cd cdk-uv-python-example-forblog
$ uv sync
$ uv run npx cdk deploy

デプロイされると Cloudformation スタックができていることが確認できます。

cdk deploy された結果としての CloudFormation スタックを確認する

SQSにメッセージを与えてみます。

デプロイされたSQSにメッセージを与えてみる

Lambdaのログを確認する

SQSメッセージをトリガーとしてLambda関数が動作したログ

以上になります。以降は結論にどのように至ったのかを補足していきます。 過程はコードを見れば分かるよーという方はスキップして頂いて構いません。

構築手順

uv, docker engine, npm をインストール済の状態とします。OS は Ubuntu 24 LTS とします。

AWS CLI をインストールしプロファイルを設定しておく

$ aws configure
AWS Access Key ID [None]: AKIAEXAMPLE
AWS Secret Access Key [None]: EXAMPLEKEY
Default region name [ap-northeast-1]: 
Default output format [None]: json

uv で python 3.11 をインストールする

$ uv python install 3.11

たたき台とするCDKサンプルプロジェクトを新規作成する

$ mkdir cdk-uv-python-example-forblog
$ cd cdk-uv-python-example-forblog
$ npx cdk init sample-app --language=python cdk_uv_python_example

venv で仮想環境を作成する

$ uv venv --python 3.11
Using Python 3.11.9
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
$ uv run python -V
Python 3.11.9
$ 

uv でプロジェクトを初期化する

$ uv init
Initialized project `cdk-uv-python-example-forblog`
$

src/, tests/ ディレクトリにコードを配置する

$ mkdir -p src
$ rm hello.py
$ curl -o src/hello.py https://raw.githubusercontent.com/hiromitsu-sai/cdk-uv-python-example-forblog/refs/heads/main/src/hello.py
$ curl -o tests/test_hello.py https://raw.githubusercontent.com/hiromitsu-sai/cdk-uv-python-example-forblog/refs/heads/main/tests/test_hello.py

Dockerfileを配置する

$ curl -o Dockerfile https://raw.githubusercontent.com/hiromitsu-sai/cdk-uv-python-example-forblog/refs/heads/main/Dockerfile
$ curl -o .dockerignore https://raw.githubusercontent.com/hiromitsu-sai/cdk-uv-python-example-forblog/refs/heads/main/.dockerignore

uv で python パッケージを管理する

## requirements.txt からパッケージを追加
$ uv add -r requirements.txt
$ uv add -r requirements.txt --dev
$ rm requirements.txt requirements-dev.txt source.bat
$ uv add ruff --dev

ユニットテストとコードスタイルチェックをかける

$ uv run ruff check
$ uv run python -m pytest tests -v

デプロイする

$ uv run npx cdk bootstrap    # 初回だけ実行すればよい
$ uv run npx cdk deploy

メリット

コンテナイメージを使用した Lambda 関数を採用するメリット

Lambda関数のデプロイパッケージの方式には、大別すると次の2通りがあります。

  1. Lambda 関数の .zip ファイルアーカイブ としてデプロイする方法
  2. コンテナイメージ としてデプロイする方法

コンテナイメージとしてデプロイする場合のメリットは、イメージデプロイが可能になった時の builders.flash 記事 が分かりやすいです。特に、たくさんデプロイできしっかりした版管理がされるECR (コンテナリポジトリ) に依存している点が良いです。

項目 Zip コンテナ
ストレージ場所 S3 ECR
ストレージサイズ上限 (リージョン単位) 75GB (上限緩和可能) ECRのクォータに準拠。10万リポジトリまで
アーティファクトサイズ上限 250 MB (展開後) 10 GB
Layer 対応 あり なし

なお、この表は2024-12-7 時点の情報です。アップデートされる可能性がありますので最新情報は 公式ドキュメント をご覧ください。

  • AWS Lambdaとしてで稼働するための構成がされており動作保証範囲が広い
  • Amazon Linux Base オペレーティングシステムで構成される
  • AWS 自体がLambdaメンテナンスポリシーをもって保守しており、定期的にセキュリティパッチや更新、その他最適化が適用される
  • Lambda コンテナイメージとしてデプロイ・実行するために必要なコンポーネントや構成 (RICなど) が適用済

gallery.ecr.aws

AWS CDK を用いた IaC をするメリット

AWS CDK を使うメリットは IaC ができることですが、特に効率よく安全に行いやすい点がメリットです。

aws.amazon.com

uv を用いた python ランタイムやパッケージの管理をするメリット

uvとは、Python ランタイムのバージョン管理と、Pythonパッケージの管理を効率的に行えるツールです。この2種類の管理ツールの世界には先行の著名ツール(pyenv, anyenv, pip, pipenv, poetry, etc...)が複数あり、長短様々で必ずしも優劣はつけがたいと思います。

今回特に uv に見出すメリットとしては、コンテナイメージとして実行されるPythonプログラムを開発する場合に、環境の構築速度が速く構築ステップの少なさがあります。

コンテナ開発における辛みは、イメージのビルド時間が長く、Python実行環境としての構築オペレーション (Dockerfile) の内容が長大になりがちな点なのですが、uvを使うとpython環境構築の部分は他のツールとは比べ物にならないほど縮小されます。

docs.astral.sh

試しにコンテナイメージをビルドして所要時間を確認してみました。 FROM ghcr.io/astral-sh/uv:latest... に着目していただくと1.9秒でとても早いです。 なお、実行環境は ap-northeast-1 リージョンに配置した t2.xlarge タイプのオンデマンドEC2インスタンスです。

$ DOCKER_BUILDKIT=1 docker build . --no-cache
[+] Building 7.7s (12/12) FINISHED                                                                                                                                                             docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                                     0.0s
 => => transferring dockerfile: 2.12kB                                                                                                                                                                   0.0s
 => [internal] load metadata for ghcr.io/astral-sh/uv:latest                                                                                                                                             1.2s
 => [internal] load metadata for public.ecr.aws/lambda/python:3.11                                                                                                                                       0.9s
 => [internal] load .dockerignore                                                                                                                                                                        0.0s
 => => transferring context: 324B                                                                                                                                                                        0.0s
 => FROM ghcr.io/astral-sh/uv:latest@sha256:23272999edd22e78195509ea3fe380e7632ab39a4c69a340bedaba7555abe20a                                                                                             1.9s
 => => resolve ghcr.io/astral-sh/uv:latest@sha256:23272999edd22e78195509ea3fe380e7632ab39a4c69a340bedaba7555abe20a                                                                                       0.0s
 => => sha256:dca2e425d1cd38e6c4f4dbe1125694d7e3f52efedc7d8fc516a823a2fde512e9 669B / 669B                                                                                                               0.0s
 => => sha256:b8ee93cf5ac57f844743147b3376659348dced3855a97492350f0d2cd16f51af 1.30kB / 1.30kB                                                                                                           0.0s
 => => sha256:334074b91934d845cad75878f0896e6e7ecfb59b10f678493db5badc64a02d23 14.66MB / 14.66MB                                                                                                         1.5s
 => => sha256:5cd11d181efec426e34eb3c524911557fc9b9d36111c1750075f36670a6aa971 94B / 94B                                                                                                                 0.4s
 => => sha256:23272999edd22e78195509ea3fe380e7632ab39a4c69a340bedaba7555abe20a 2.19kB / 2.19kB                                                                                                           0.0s
 => => extracting sha256:334074b91934d845cad75878f0896e6e7ecfb59b10f678493db5badc64a02d23                                                                                                                0.3s
 => => extracting sha256:5cd11d181efec426e34eb3c524911557fc9b9d36111c1750075f36670a6aa971                                                                                                                0.0s
 => [internal] load build context                                                                                                                                                                        0.0s
 => => transferring context: 21.59kB                                                                                                                                                                     0.0s
 => CACHED [stage-0 1/5] FROM public.ecr.aws/lambda/python:3.11@sha256:885e7899bbfb56dbc34401eb07b5bee738f0909032d5342479acf229f196a4a2                                                                  0.0s
 => [stage-0 2/5] COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv                                                                                                                                    0.1s
 => [stage-0 3/5] COPY pyproject.toml uv.lock /var/task/                                                                                                                                                 0.0s
 => [stage-0 4/5] RUN uv sync --frozen                                                                                                                                                                   2.6s
 => [stage-0 5/5] COPY src/ /var/task/                                                                                                                                                                   0.0s 
 => exporting to image                                                                                                                                                                                   1.5s 
 => => exporting layers                                                                                                                                                                                  1.5s 
 => => writing image sha256:2ee5f4b6ca4ac0bf7c9da074a925b8e5b2c626c00c49f5963d6dce7210512fdb                                                                                                             0.0s

 2 warnings found (use docker --debug to expand):
 - UndefinedVar: Usage of undefined variable '$LOGLEVEL' (line 9)
 - UndefinedVar: Usage of undefined variable '$IS_DEBUG' (line 10)
$ 

ruff を用いた linting, formatting ができるメリット

ruff は flake8 や black のような Linter, Formatter で、動作が高速です。

docs.astral.sh

さらに、uv も ruff もAstral 社によって開発されたツールで、両者には親和性があります。例えば、今回書いたサンプルプロジェクトでは ruff パッケージをわざわざインストールしていますが、パッケージとしてインストールせずにuv toolとして利用することができます。

$ uv remove ruff --dev
Resolved 21 packages in 125ms
Uninstalled 1 package in 0.72ms
 - ruff==0.8.2
$ uvx ruff check
Installed 1 package in 1ms
All checks passed!
$ 

docs.astral.sh