RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

【全2回】AWS Lambda x FastAPIによるPythonモダンAPI開発のすゝめ 1

はじめに

ラクスルグループのノバセルで新卒2年目のエンジニアをしています田村(tamtam)です。

この度、私たちのチームではAWS LambdaとFastAPIを使用したAPI開発プロジェクトを進めております。

この記事を読んで得られること

この記事では、プロジェクトの中から特に効果的だった要素を、以下の3つのカテゴリーに分けて詳しく紹介します。

  1. 開発環境の構築で使用したツール
  2. 開発で活用したPythonライブラリ
  3. アーキテキチャ及びディレクトリ構造

これらはプロジェクトの初期段階で慎重に選定され、今後の開発の進行に大きな影響を及ぼします。

全2回を通じて、同じような課題を抱える他の開発者の皆さんに役立つ情報を提供できればと思います。

第1回では、1. 開発環境の構築で使用したツール2. 開発に活用したPythonライブラリについて紹介します。

それでは詳細について見ていきましょう!

対象読者

  • Pythonを使用して新しいプロジェクトを開始しようとしている人

  • Python開発に役立つリンターやフォーマッターなどの効率的なライブラリを探している人

  • Pythonでの開発に際して参考にしたいアーキテクチャを探している人

  • AWS LambdaのPython環境をサーバーレス実行プラットフォームとして検討している人

  • FastAPIを用いた開発を検討している人

あまり説明しないこと

  • FastAPIやLambda自体の深い説明

前提とするバージョン

参考となるレポジトリ

今回の記事に際して、筆者が作成したレポジトリを共有します。

github.com

なお、ソースコードは逐次更新しているため、本書の記述内容と差異がある可能性をご理解の上読んでいただきますと幸いです。

1. 開発環境の構築で使用したツール

プロジェクト開始で行われる環境構築について、AWS Lambdaのコンテナサポートを採用しました。

そして、Poetryを利用時に開発と本番環境の適切な管理でLambdaデプロイをスムーズに行いました。

加えて、開発時にMutagen Composeを用いてDocker環境での同期を速くさせました。

AWS Lambdaのコンテナサポートを採用

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のコンテナを連携させて自動テストを行いましたが、非常にやりやすさを感じました。

Poetry利用時に開発と本番環境の適切な管理でLambdaデプロイ問題を解決

Poetry利用時に起きた問題

私たちのチームでは、開発時にDockerとPoetryを活用し、依存関係の管理を行っています。

具体的には、Docker上にPoetryの仮想環境を作成し、その中で必要なライブラリやモジュールのインストールと管理を行います。

しかし、このままの形でAWS Lambdaにデプロイすると、Poetryの仮想環境が適切に認識されず、問題が生じました。

Dockerfileを分けてデプロイできない問題を回避

そこで、我々は開発環境と本番環境で異なるDockerfileを用いる方策をとりました。

開発環境では従来通りPoetryを用いて依存関係を管理します。

一方、本番環境用のDockerfileでは、Poetryを使用して作成した仮想環境からrequirements.txtファイルを生成し、それをpipを用いてインストールする形にしました。

以下のような方で提供します。

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を行います。

このように、開発環境と本番環境でDockerfileを変えることで、開発時の便利さを維持しつつ、本番環境での問題を回避することができました。

Mutagen Composeを採用

Dockerの同期遅い問題

Dockerコンテナを使用した開発においては、ディスクIOの速度が遅いという課題をよく散見します。

具体的には、ホストマシンとコンテナ間でファイルを同期する「バインドマウント」の際、データの読み書き速度の遅延が問題となります。

Mutagen Composeを利用

この問題に対処するため、私たちはオープンソースツールであるMutagen Composeを利用しました。

mutagen.io

Mutagen Composeは、ローカル環境とリモート環境(この場合、Dockerコンテナ)の間でディレクトリを高速に同期することができます。

以下のような方で提供します。

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"

Mutagen Composeに関してはこちらの記事がよくまとまっています。

このツールの採用により、バインドマウント時のディスクIO速度の低下という課題が改善し、開発効率の向上に寄与しました。

2. 開発で活用したPythonライブラリ

AWS Lambda x FastAPIによるPythonモダンAPI開発をするに当たって、実際に利用した依存関係の管理、リンター・フォーマッター、テストなどなどのさまざまなライブラリを紹介します。


パッケージ管理

Poetry

Poetryは、Pythonのパッケージ管理を行うツールです。

依存関係管理には、pyproject.tomlとpoetry.lockの二つのファイルを用いて、開発環境の再現性と整合性を確保します。

これにより、依存関係がプロジェクトの成長に応じて適切に管理されることが可能となります。

さらに、Poetryはバージョン管理機能も提供しており、プロジェクト全体のバージョン管理を助けます。

github.com

Ryeも検討したものの採用せず

RyeはPythonのインタプリタ環境とパッケージ管理を行います。

「Rye」はRust製であり、高速な実行速度とPythonへの依存関係がないことを特長としています。

開発時こちらに乗り換えることも少し検討しましたが、Ryeのメンテナンスの意向が不明瞭だったのと、コンテナベースでのRyeを用いた開発の知見がなかったため採用しませんでした。

とはいえ、Ryeの作者でDockerでの利用方法をまとめようとする動きがあり、今後の開発で移行する可能性も出てきそうです。

github.com


ベースのライブラリ

FastAPI

FastAPIは型アノテーションを使用した設定管理やデータバリデーションをするPydanticと軽量のASGIフレームワークであるStarletteをベースに開発されている高速なフレームワークです。

バリデーションやシリアライズが容易に行えるだけでなく、APIのDocs生成も自動でおこなってくれます。

またStarletteをベースにしているため、BackgroundTaskといった軽量な非同期タスクなども実装しやすいです。

github.com

Mangum

Mangumは、Function URL、API Gateway、ALB、およびLambda@Edgeイベントを処理しAWS LambdaでASGIアプリケーションを実行するためのアダプターです。

Magnumを利用する際は、uvicornといったASGIのweb serverを起動するわけではなく、 FastAPIとLambda Proxy Integrationとの間に立ちイベントを変換します。

そのためオーバヘッドもほとんどゼロであり、Lambdaが適切にスケーリングすればパフォーマンスの心配はなく積極的に使っていけそうです。

github.com

Powertools for AWS Lambda

Lambdaでの実装をサポートしてくれるライブラリです。

特にPythonの標準ライブラリのloggingのLoggerをラップしたLoggerは便利です。

  • Lambda コンテキスト、コールドスタートの情報をキャプチャし、JSON で構造化されたログを出力

  • 呼び出された時に Lambda のイベントをロギング (デフォルトでは無効)

  • リクエストのパーセンテージでログのサンプリングを DEBUG ログレベルで有効化可能 (デフォルトは無効)

  • どの時点でも構造化されたログにキーを追加可能

引用: AWS Lambda Powertools Python入門 第 3 回 ~Logger Utility - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

わかりやすい利用例としては、handler対してeventが入力された際にログ出力するといった形です。

def handler(event: Any, context: Any) -> Any:
    # Loggerをラップしたモノ
    ApiLogger.info(event)

    asgi_handler = Mangum(app, lifespan="off")
    return asgi_handler(event, context)

github.com

docs.powertools.aws.dev


リンター・フォーマッター

Ruff

Rust製で書かれたリンター及びフォーマッターです。

Flake8やPylintをはじめとする他のリンターと比べ、10〜100倍の速度を誇ります。

その上、Flake8, isort, pydocstyle, autoflake等のリンターを代替する互換性を持ちます。

そのため、Flake8等の既存のリンターをRuffで置き換えることをおすすめします。

github.com

設定項目の詳細は公式を参照すると良いです。 beta.ruff.rs

注意点として一部のルールが未対応であるため、Blackといったパッケージの併用をすることが良さそうです。

他方、設定はpyproject.tomlに記述することが可能です。

参考リポジトリにて利用しているpyproject.tomlの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

静的解析による型チェッカーです。型アノテーションが正しく適用されているかを確認します。

Pythonは基本的には動的型付けの言語ですが、型アノテーションを用いることで、コードの可読性を高め、エラーを早期に発見することが可能になります。

また、型アノテーションを用いると、エディタが型情報を理解し、コードのリファクタリングを助けてくれます。

大規模なプロジェクトでは、このような型アノテーションの活用が推奨されています。

Mypyはこの型アノテーションを効果的に活用し、コードの品質を向上させるための有力なツールとなります。

github.com

mypyではmypy.iniという設定ファイルにルールを記載します。以下参考です。

[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
型アノテーション自動生成ツールの活用

型チェックの対となる型の自動生成について、どのようなツールがあるのか調査してみました。

その結果、関数の実行結果を元に型を自動生成するツール、MonkeyTypeを見つけました。

このツールは型アノテーションの手間を省くために便利そうです。

github.com

しかし、現在ではGithub Copilotというツールもあり、これも型の自動生成(コードの生成)をしてくれます。

ノバセルではGithub Copilotの社内導入をおこなっているため、私たちのチームではGithub Copilotを利用しています。

Github Copilotを利用する場合では、コンテキストディレクトリーを活用し自動生成を促すといったことがよさそうです。

github.com

Black

BlackはPythonの自動コードフォーマッターで、コードをPEP 8と互換性のある形式に自動的に整形します。これにより、コードのスタイルが一貫性を持ち、読みやすさが向上します。

Ruffは一部の機能が未実装されているため、その補完のためにblackを利用します。

github.com


テスト

Pytest

Pytestとはテスティングフレームワークのー種です。

github.com

Pytest時にモジュールのインポートエラーに悩まれる機会があると思いますが、その際にPytestのGood Integration Practiceを見ることをおすすめします。

こちらでは、ディレクトリ構造やpytestコマンドの呼び出し方について言及されています。

docs.pytest.org

pytest-cov

pytest実行時のテストカバレッジを出力するPytestのプラグインです。

github.com

業務ではmake testを経由して以下のような形で呼び出しています。

test:
  docker exec container_name python3 -m poetry run pytest tests --cov=/var/task --cov-report term-missing

pytest-env

pytest実行時のみに使われる環境変数を設定するために使われるPytestのプラグインです。

主な用途は、テストやローカル環境など、実行時に読み込むべきファイルを環境に応じて変更するためのものです。

これはENVの値を設定することで、どの環境を使用するべきかを決定します。

pytest.iniに以下のように設定できます。

[pytest]
env =
    ENV=test

github.com

その他

Pydantic

型アノテーションを利用した型のバリデーションと設定管理を行なってくれるライブラリです。

github.com

Pydanticとよく比較されるDataclassesの違いはこちらの記事がまとまっていますが、大きな違いとしてはPydanticはDataclassにはない型バリデーションを持っており堅牢な印象です。

Pydanticでは、BaseSettingsクラスを継承し利用することで、環境変数を読み込ませ設定管理を実現させています。

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

その他嬉しいポイントとしては、Pydanticで定義したmodelをdictであったりjsonであったりとさまざまな形へ自由度高く変換することができます。

なおDataclassesではjsonに変換させるものは標準搭載されておらず、自前実装をするかpyserdeなどと併用する必要があります。

https://docs.pydantic.dev/latest/usage/exporting_models/docs.pydantic.dev

Pycharmをお使いの方はPydanticのプラグインがあるのでぜひ利用を検討してみてください。

github.com

Pandera

PanderaはDataframeの型バリデーションを行います。

Dataframe内のデータが指定されたスキーマに従っていることを確認するための柔軟なAPIを提供します。

github.com

ノバセルの開発ではデータサイエンティストが作成した学習/推論ロジック等をシステム化することが時折あります。 その際にDataframeを扱うことが多いのですが、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ファイルを処理するライブラリです。

github.com

リポジトリで管理可能な環境変数(機密情報ではないもの)については、それらをyamlファイルに記述し、その内容を読み込むようにしています。

# 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

DIコンテナを提供するライブラリです。

後述しますが、今回の開発ではオニオンアーキテクチャをベースにしているため、ドメイン層にリポジトリのインタフェースを定義し、インフラストラクチャ層のリポジトリが実装させるといったことがあります。

その際のインターフェースと実装のバインディングも容易に行うことができます。

github.com

injector.readthedocs.io

検討したが使用しなかったモノ

pre-commit

pre-commit hookを行うPython製のライブラリです。

こちらのライブラリでは、pre-commitの設定ファイル、pre-commit.ymlはgitのルートディレクトリに配置する必要があります。

今回のチーム開発でのプロジェクト構造では、これが適用できないため、このツールの導入を見送ることになりました。

そこで我々は、代替策としてMakefileを利用する形を選びました。

Makefile内に、リンターやフォーマッターを用いたスクリプトを設定し、開発者がコードの品質を保つ支援を行っています。

この方法では、pre-commitのような自動的な検証は行えませんが、開発者が任意のタイミングで品質チェックを実行できるようしています。

pre-commit.com

まとめ

第1回では、AWS Lambda x FastAPIによるPythonモダンAPI開発を実現する上で役立つ、1. 開発環境の構築で使用したツール2. 開発に活用したPythonライブラリについて紹介していきました。

この記事を通じて、同じような課題を抱える他の開発者の皆さんに役立つ情報を提供できればと幸いです。

第2回では3. アーキテキチャ及びディレクトリ構造について紹介していきます!💪