ラクスルグループのノバセルで新卒2年目のエンジニアをしています田村(tamtam)です。 この度、私たちのチームではAWS LambdaとFastAPIを使用したAPI開発プロジェクトを進めております。 この記事では、プロジェクトの中から特に効果的だった要素を、以下の3つのカテゴリーに分けて詳しく紹介します。 これらはプロジェクトの初期段階で慎重に選定され、今後の開発の進行に大きな影響を及ぼします。 全2回を通じて、同じような課題を抱える他の開発者の皆さんに役立つ情報を提供できればと思います。 第1回では、1. 開発環境の構築で使用したツール、2. 開発に活用したPythonライブラリについて紹介します。 それでは詳細について見ていきましょう! Pythonを使用して新しいプロジェクトを開始しようとしている人 Python開発に役立つリンターやフォーマッターなどの効率的なライブラリを探している人 Pythonでの開発に際して参考にしたいアーキテクチャを探している人 AWS LambdaのPython環境をサーバーレス実行プラットフォームとして検討している人 FastAPIを用いた開発を検討している人 今回の記事に際して、筆者が作成したレポジトリを共有します。 なお、ソースコードは逐次更新しているため、本書の記述内容と差異がある可能性をご理解の上読んでいただきますと幸いです。 プロジェクト開始で行われる環境構築について、AWS Lambdaのコンテナサポートを採用しました。 そして、Poetryを利用時に開発と本番環境の適切な管理でLambdaデプロイをスムーズに行いました。 加えて、開発時にMutagen Composeを用いてDocker環境での同期を速くさせました。 AWS Lambdaでは2020年12月からコンテナイメージのサポートを開始しています。 (2023年6月15日時点ではPython3.10のAWS Lambda用のベースイメージが公開されています。) 私たちのチームでは普段から主にDockerを用いたコンテナベースでの開発をしているため利用しました。 AWS CDKでもDockerImageAssetがあり、Lambdaをデプロイする際のインフラ構築もやりやすい印象です。 Python開発においては、バージョン管理や依存関係の管理がしばしば問題となりますが、コンテナ技術を使用することである程度緩和されると考えます。 他方、開発プロセスにおいては、AWSLambdaを使用する関係上、他のAWSリソースとの連携も発生することが多く、そのためのテストも必要になってきます。 その際に、ローカルで模倣するためのツールであるLocalStackがよく利用されますが、LocalStack自体もコンテナイメージとして提供されています。 実際にAWS LambdaのPythonコンテナとLocalStackのコンテナを連携させて自動テストを行いましたが、非常にやりやすさを感じました。 私たちのチームでは、開発時にDockerとPoetryを活用し、依存関係の管理を行っています。 具体的には、Docker上にPoetryの仮想環境を作成し、その中で必要なライブラリやモジュールのインストールと管理を行います。 しかし、このままの形でAWS Lambdaにデプロイすると、Poetryの仮想環境が適切に認識されず、問題が生じました。 そこで、我々は開発環境と本番環境で異なるDockerfileを用いる方策をとりました。 開発環境では従来通りPoetryを用いて依存関係を管理します。 一方、本番環境用のDockerfileでは、Poetryを使用して作成した仮想環境からrequirements.txtファイルを生成し、それをpipを用いてインストールする形にしました。 以下のような方で提供します。 開発環境では通常通り、 このように、開発環境と本番環境でDockerfileを変えることで、開発時の便利さを維持しつつ、本番環境での問題を回避することができました。 Dockerコンテナを使用した開発においては、ディスクIOの速度が遅いという課題をよく散見します。 具体的には、ホストマシンとコンテナ間でファイルを同期する「バインドマウント」の際、データの読み書き速度の遅延が問題となります。 この問題に対処するため、私たちはオープンソースツールであるMutagen Composeを利用しました。 Mutagen Composeは、ローカル環境とリモート環境(この場合、Dockerコンテナ)の間でディレクトリを高速に同期することができます。 以下のような方で提供します。 Mutagen Composeに関してはこちらの記事がよくまとまっています。 このツールの採用により、バインドマウント時のディスクIO速度の低下という課題が改善し、開発効率の向上に寄与しました。 AWS Lambda x FastAPIによるPythonモダンAPI開発をするに当たって、実際に利用した依存関係の管理、リンター・フォーマッター、テストなどなどのさまざまなライブラリを紹介します。 Poetryは、Pythonのパッケージ管理を行うツールです。 依存関係管理には、pyproject.tomlとpoetry.lockの二つのファイルを用いて、開発環境の再現性と整合性を確保します。 これにより、依存関係がプロジェクトの成長に応じて適切に管理されることが可能となります。 さらに、Poetryはバージョン管理機能も提供しており、プロジェクト全体のバージョン管理を助けます。 RyeはPythonのインタプリタ環境とパッケージ管理を行います。 「Rye」はRust製であり、高速な実行速度とPythonへの依存関係がないことを特長としています。 開発時こちらに乗り換えることも少し検討しましたが、Ryeのメンテナンスの意向が不明瞭だったのと、コンテナベースでのRyeを用いた開発の知見がなかったため採用しませんでした。 とはいえ、Ryeの作者でDockerでの利用方法をまとめようとする動きがあり、今後の開発で移行する可能性も出てきそうです。 FastAPIは型アノテーションを使用した設定管理やデータバリデーションをするPydanticと軽量のASGIフレームワークであるStarletteをベースに開発されている高速なフレームワークです。 バリデーションやシリアライズが容易に行えるだけでなく、APIのDocs生成も自動でおこなってくれます。 またStarletteをベースにしているため、BackgroundTaskといった軽量な非同期タスクなども実装しやすいです。 Mangumは、Function URL、API Gateway、ALB、およびLambda@Edgeイベントを処理しAWS LambdaでASGIアプリケーションを実行するためのアダプターです。 Magnumを利用する際は、uvicornといったASGIのweb serverを起動するわけではなく、
FastAPIとLambda Proxy Integrationとの間に立ちイベントを変換します。 そのためオーバヘッドもほとんどゼロであり、Lambdaが適切にスケーリングすればパフォーマンスの心配はなく積極的に使っていけそうです。 Lambdaでの実装をサポートしてくれるライブラリです。 特にPythonの標準ライブラリのloggingのLoggerをラップしたLoggerは便利です。 Lambda コンテキスト、コールドスタートの情報をキャプチャし、JSON で構造化されたログを出力 呼び出された時に Lambda のイベントをロギング (デフォルトでは無効) リクエストのパーセンテージでログのサンプリングを DEBUG ログレベルで有効化可能 (デフォルトは無効) どの時点でも構造化されたログにキーを追加可能 引用: AWS Lambda Powertools Python入門 第 3 回 ~Logger Utility - 変化を求めるデベロッパーを応援するウェブマガジン | AWS わかりやすい利用例としては、handler対してeventが入力された際にログ出力するといった形です。 Rust製で書かれたリンター及びフォーマッターです。 Flake8やPylintをはじめとする他のリンターと比べ、10〜100倍の速度を誇ります。 その上、Flake8, isort, pydocstyle, autoflake等のリンターを代替する互換性を持ちます。 そのため、Flake8等の既存のリンターをRuffで置き換えることをおすすめします。 設定項目の詳細は公式を参照すると良いです。
beta.ruff.rs 注意点として一部のルールが未対応であるため、Blackといったパッケージの併用をすることが良さそうです。 他方、設定はpyproject.tomlに記述することが可能です。 参考リポジトリにて利用しているpyproject.tomlのruffの箇所になります。 静的解析による型チェッカーです。型アノテーションが正しく適用されているかを確認します。 Pythonは基本的には動的型付けの言語ですが、型アノテーションを用いることで、コードの可読性を高め、エラーを早期に発見することが可能になります。 また、型アノテーションを用いると、エディタが型情報を理解し、コードのリファクタリングを助けてくれます。 大規模なプロジェクトでは、このような型アノテーションの活用が推奨されています。 Mypyはこの型アノテーションを効果的に活用し、コードの品質を向上させるための有力なツールとなります。 mypyではmypy.iniという設定ファイルにルールを記載します。以下参考です。 型チェックの対となる型の自動生成について、どのようなツールがあるのか調査してみました。 その結果、関数の実行結果を元に型を自動生成するツール、MonkeyTypeを見つけました。 このツールは型アノテーションの手間を省くために便利そうです。 しかし、現在ではGithub Copilotというツールもあり、これも型の自動生成(コードの生成)をしてくれます。 ノバセルではGithub Copilotの社内導入をおこなっているため、私たちのチームではGithub Copilotを利用しています。 Github Copilotを利用する場合では、コンテキストディレクトリーを活用し自動生成を促すといったことがよさそうです。 BlackはPythonの自動コードフォーマッターで、コードをPEP 8と互換性のある形式に自動的に整形します。これにより、コードのスタイルが一貫性を持ち、読みやすさが向上します。 Ruffは一部の機能が未実装されているため、その補完のためにblackを利用します。 Pytestとはテスティングフレームワークのー種です。 Pytest時にモジュールのインポートエラーに悩まれる機会があると思いますが、その際にPytestのGood Integration Practiceを見ることをおすすめします。 こちらでは、ディレクトリ構造やpytestコマンドの呼び出し方について言及されています。 pytest実行時のテストカバレッジを出力するPytestのプラグインです。 業務ではmake testを経由して以下のような形で呼び出しています。 pytest実行時のみに使われる環境変数を設定するために使われるPytestのプラグインです。 主な用途は、テストやローカル環境など、実行時に読み込むべきファイルを環境に応じて変更するためのものです。 これはENVの値を設定することで、どの環境を使用するべきかを決定します。 pytest.iniに以下のように設定できます。 型アノテーションを利用した型のバリデーションと設定管理を行なってくれるライブラリです。 Pydanticとよく比較されるDataclassesの違いはこちらの記事がまとまっていますが、大きな違いとしてはPydanticはDataclassにはない型バリデーションを持っており堅牢な印象です。 Pydanticでは、BaseSettingsクラスを継承し利用することで、環境変数を読み込ませ設定管理を実現させています。 その他嬉しいポイントとしては、Pydanticで定義したmodelをdictであったりjsonであったりとさまざまな形へ自由度高く変換することができます。 なおDataclassesではjsonに変換させるものは標準搭載されておらず、自前実装をするかpyserdeなどと併用する必要があります。 https://docs.pydantic.dev/latest/usage/exporting_models/docs.pydantic.dev Pycharmをお使いの方はPydanticのプラグインがあるのでぜひ利用を検討してみてください。 PanderaはDataframeの型バリデーションを行います。 Dataframe内のデータが指定されたスキーマに従っていることを確認するための柔軟なAPIを提供します。 ノバセルの開発ではデータサイエンティストが作成した学習/推論ロジック等をシステム化することが時折あります。
その際にDataframeを扱うことが多いのですが、Panderaはデータの堅牢性を向上させる上で役立ちます。 yamlファイルを処理するライブラリです。 リポジトリで管理可能な環境変数(機密情報ではないもの)については、それらをyamlファイルに記述し、その内容を読み込むようにしています。 DIコンテナを提供するライブラリです。 後述しますが、今回の開発ではオニオンアーキテクチャをベースにしているため、ドメイン層にリポジトリのインタフェースを定義し、インフラストラクチャ層のリポジトリが実装させるといったことがあります。 その際のインターフェースと実装のバインディングも容易に行うことができます。 pre-commit hookを行うPython製のライブラリです。 こちらのライブラリでは、pre-commitの設定ファイル、pre-commit.ymlはgitのルートディレクトリに配置する必要があります。 今回のチーム開発でのプロジェクト構造では、これが適用できないため、このツールの導入を見送ることになりました。 そこで我々は、代替策としてMakefileを利用する形を選びました。 Makefile内に、リンターやフォーマッターを用いたスクリプトを設定し、開発者がコードの品質を保つ支援を行っています。 この方法では、pre-commitのような自動的な検証は行えませんが、開発者が任意のタイミングで品質チェックを実行できるようしています。 第1回では、AWS Lambda x FastAPIによるPythonモダンAPI開発を実現する上で役立つ、1. 開発環境の構築で使用したツール、2. 開発に活用したPythonライブラリについて紹介していきました。 この記事を通じて、同じような課題を抱える他の開発者の皆さんに役立つ情報を提供できればと幸いです。 第2回では3. アーキテキチャ及びディレクトリ構造について紹介していきます!💪はじめに
この記事を読んで得られること
対象読者
あまり説明しないこと
前提とするバージョン
参考となるレポジトリ
1. 開発環境の構築で使用したツール
AWS Lambdaのコンテナサポートを採用
Poetry利用時に開発と本番環境の適切な管理でLambdaデプロイ問題を解決
Poetry利用時に起きた問題
Dockerfileを分けてデプロイできない問題を回避
FROM public.ecr.aws/lambda/python:3.10
RUN pip install poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export --without-hashes --output requirements.txt
RUN pip install -r requirements.txt
COPY . ./
CMD [ "main.handler" ]
poetry install
を行います。Mutagen Composeを採用
Dockerの同期遅い問題
Mutagen Composeを利用
x-mutagen:
sync:
mutagen:
mode: "two-way-resolved" # 同期モード指定(ホスト ↔ コンテナ 双方を同期)
alpha: "." # プロジェクトのパス
beta: "volume://mutagen-app-volume" # volumeの指定
volumes:
mutagen-app-volume:
services:
app:
container_name: app
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- mutagen-app-volume:/var/task:delegated
working_dir: '/var/task'
environment:
- PYTHONPATH=/var/task
tty: true
stdin_open: true
entrypoint: ''
command: "poetry run uvicorn main:app --reload --host 0.0.0.0"
ports:
- "8000:8000"
2. 開発で活用したPythonライブラリ
パッケージ管理
Poetry
Ryeも検討したものの採用せず
ベースのライブラリ
FastAPI
Mangum
Powertools for AWS Lambda
def handler(event: Any, context: Any) -> Any:
# Loggerをラップしたモノ
ApiLogger.info(event)
asgi_handler = Mangum(app, lifespan="off")
return asgi_handler(event, context)
リンター・フォーマッター
Ruff
[tool.ruff]
select = ["ALL"]
# ref: https://github.com/takashi-yoneya/python-template/blob/main/pyproject.toml
ignore = [
"G004", # `logging-f-string`
"PLC1901", # compare-to-empty-string
"PLR2004", # magic-value-comparison
"ANN101", # missing-type-self
"ANN102", # missing-type-cls
"ANN002", # missing-type-args
"ANN003", # missing-type-kwargs
"ANN401", # any-type
"ERA", # commented-out-code
"ARG002", # unused-method-argument
"INP001", # implicit-namespace-package
"PGH004", # blanket-noqa
"B008", # Dependsで使用するため
"A002", # builtin-argument-shadowing
"A003", # builtin-attribute-shadowing
"PLR0913", # too-many-arguments
"RSE", # flake8-raise
"D", # pydocstyle
"C90", # mccabe
"T20", # flake8-print
"SLF", # flake8-self
"BLE", # flake8-blind-except
"FBT", # flake8-boolean-trap
"TRY", # tryceratops
"COM", # flake8-commas
"S", # flake8-bandit
"EM",#flake8-errmsg
"EXE", # flake8-executable
"ICN", # flake8-import-conventions
"RET",#flake8-return
"SIM",#flake8-simplify
"TCH", # flake8-type-checking
"PTH", #pathlibを使わないコードが多いので、除外する
"ISC", #flake8-implicit-str-concat
"N", # pep8-naming
"PT", # flake8-pytest-style
"PGH003"
]
line-length = 115
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10.
target-version = "py310"
Mypy
[mypy]
# ref: https://github.com/takashi-yoneya/fastapi-mybest-template/blob/master/mypy.ini
python_version = 3.10
platform = linux
plugins = pydantic.mypy
# Import discovery
namespace_packages=True
ignore_missing_imports = True
# follow_imports=error
# follow_imports_for_stubs=True
no_site_packages = True
no_silence_site_packages=True
# Disallow dynamic typing
# disallow_any_unimported=True
# disallow_any_expr=True
# disallow_any_decorated=True
# disallow_any_explicit=True
disallow_any_generics=True
# disallow_subclassing_any=True
# Untyped definitions and calls
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
# disallow_untyped_decorators = True
# None and Optional handling
no_implicit_optional = True
# Configuring warnings
warn_redundant_casts = True
warn_unused_ignores = True
# warn_return_any = True
warn_unreachable = True
# Miscellaneous strictness flags
strict_equality = True
# Configuring error messages
show_error_context = True
show_column_numbers = True
show_error_codes = True
# pretty = True
# Miscellaneous
warn_unused_configs = True
[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True
warn_untyped_fields = True
型アノテーション自動生成ツールの活用
Black
テスト
Pytest
pytest-cov
test:
docker exec container_name python3 -m poetry run pytest tests --cov=/var/task --cov-report term-missing
pytest-env
[pytest]
env =
ENV=test
その他
Pydantic
from pydantic import BaseSettings, HttpUrl
class Settings(BaseSettings):
OPEN_AI_API_KEY: str
SENTRY_DSN: HttpUrl | None
ENV: str | None = None
class Config:
case_sensitive = True
Pandera
import pandas as pd
from pandera import Column, DataFrameSchema, Int, Check
simple_schema = DataFrameSchema({
"column1": Column(
Int, Check(lambda x: 0 <= x <= 10, element_wise=True,
error="range checker [0, 10]"))
})
# validation rule violated
fail_check_df = pd.DataFrame({
"column1": [-20, 5, 10, 30],
})
simple_schema(fail_check_df)
PyYAML
# YAMLをロードする
#ref https://gist.github.com/ericvenarusso/dcaefd5495230a33ef2eb2bdca262011
def read_yaml(file_path: str) -> Settings:
with open(file_path) as stream:
config = yaml.safe_load(stream)
return Settings(**config)
env = os.environ["ENV"]
settings = read_yaml(f"{os.getcwd()}/core/yaml_configs/{env}.yaml")
Injector
検討したが使用しなかったモノ
pre-commit
まとめ