🌟

Blueprint機能を使ってChaliceのapp.pyを分割する

2023/03/22に公開

はじめに

AWSでサーバレスのシステムを開発する場合、コンソールを使って一から手でポチポチするのもしんどいので専用のフレームワークを使うことが多いと思います。代表的なところでいくとSAM、Amplify、Serverless Framework、CDKなどなど。
この中でも圧倒的な生産性の高さから、私は特にChaliceを好んで使っています。Pythonを使ってサーバレスアプリを組むならChalice一択とさえ思っているのですが、今回はそのChaliceを使ったときに発生しがちな悩みどころの解消方法を書いておきたいと思います。
そもそもChaliceってなんやねん、という方は以下の記事を見てみてください。
https://aws.amazon.com/jp/builders-flash/202003/chalice-api/?awsf.filter-name=

3行でまとめると

  • Chaliceを使うとapp.pyが肥大化しがち
  • それを解決するために、ChaliceにはBlueprintsという機能がある
  • Blueprintsを使えばapp.pyを任意の単位で分割できるのでステキ

Chaliceを使った開発における悩みどころ

Chaliceで新規プロジェクトを作成すると、デフォルトでapp.pyというファイルができます。こんな感じです。

app.py
from chalice import Chalice
app = Chalice(app_name='testApiProject')

@app.route('/')
def index():
    return {'hello': 'world'}

で、簡単なREST APIを組む場合はこのapp.pyにエンドポイントをどんどん追加していきがちかと思います。こんな感じ。

app.py
from chalice import Chalice
app = Chalice(app_name='testApiProject')

@app.route('/')
def index():
    return {'hello': 'world'}

# 追加エンドポイント1(商品管理)
@app.route('/items/{itemId}')
def getItem(itemId):
    # ~
    # 何がしかの処理
    # ~~~
    return {'item': itemId}

# 追加エンドポイント2(顧客管理)
@app.route('/users/{userId}')
def getUser(userId):
    # ~~~
    # 何がしかの処理
    # ~~~
    return {'user': userId}

こういった書き方でもまあ動くは動くのですが、開発するプログラムの規模が大きくなってくるとapp.pyが肥大化してしまい、生産性も保守性も悪くなってしまいます。何とかしたいところです。

今回記事での解決方法

解決方法は色々あると思いますが、今回はChaliceのBlueprints機能に注目してみたいと思います。Blueprintsを使えばapp.pyを任意の単位で分割できるため、肥大化したapp.pyを割と簡単にスッキリ整理することが可能です。

Blueprintの使い方

先程の肥大化したapp.pyを複数のモジュールに分割してみたいと思います。まず、現在のChaliceプロジェクトのディレクトリ構成はこんな感じです。

testApiProject/
├── app.py
└── requirements.txt

先程のapp.pyでは「①商品管理エンドポイント(items)」と「②顧客管理エンドポイント(users)」の2つの機能があるので、モジュールを2分割するためにディレクトリを以下のように切ってみます。

testApiProject/
├── app.py
├── chalicelib               ※追加
│   ├── items                ※追加(商品管理エンドポイント用)
│   └── users                ※追加(顧客管理エンドポイント用)
└── requirements.txt

ちなみにChaliceでは自作のモジュールを全てchalicelibに纏めるのがお作法のようです。あと、見づらくなるので __init__.py はあえて除外しています。ご了承ください。
で、各items、usersディレクトリ内にBlueprints用のファイルを作成します。こんな感じです。

testApiProject/
├── app.py
├── chalicelib
│   ├── items
│   │   └── blueprints.py    ※追加
│   └── users
│       └── blueprints.py    ※追加
└── requirements.txt

肥大化した app.py の中身を、今回作成した各 blueprints.py に引っ越します。こんな感じで書いていきます。

items/blueprints.py
from chalice import Blueprint
extra_routes = Blueprint(__name__)

# 追加エンドポイント1(商品管理)
@extra_routes.route('/items/{itemId}')
def getItem(itemId):
    # ~
    # 何がしかの処理
    # ~~~
    return {'item': itemId}

users/blueprints.py
from chalice import Blueprint
extra_routes = Blueprint(__name__)

# 追加エンドポイント2(顧客管理)
@extra_routes.route('/users/{userId}')
def getUser():
    # ~~~
    # 何がしかの処理
    # ~~~
    return {'user': userId}

最後にapp.pyを編集して、上記で作成したblueprints.pyを読み込ませるようにします。こんな感じです。なお、デフォルトのエンドポイント(/)は不要なのでここではコメントアウトしました。

app.py
from chalice import Chalice
from chalicelib.items.blueprints import extra_routes as items    # 追加
from chalicelib.users.blueprints import extra_routes as users    # 追加

app = Chalice(app_name='testApiProject')
app.register_blueprint(items)                                    # 追加
app.register_blueprint(users)                                    # 追加

#@app.route('/')                                                 # デフォルトのエンドポイントはいらないのでコメントアウト
#def index():
#    return {'hello': 'world'}

最後に動かしてみましょう。まずはChaliceをローカル起動。

$ chalice local
Serving on http://127.0.0.1:8000

Curlでアクセスしてみます。

curl http://127.0.0.1:8000/items/aaa
{"item":"aaa"}
curl http://127.0.0.1:8000/users/bbb
{"user":"bbb"}

想定通りのレスポンスが返ってきましたね。
今回は単純な例でしたが、実際に肥大化して膨れ上がったファイルも同じように分割することが可能です。

Blueprintsの活用例

例えばECサイト向けのREST APIを作ることを考えた場合、実装する機能としては ①注文管理機能、②商品管理機能、③顧客管理機能、、、のように複数のかたまり(グループ)が出てくると思います。今回の上記サンプルもそんな感じでした。
そしてそれぞれの機能グループの中にさらに複数のAPIエンドポイントを切っていったりします。こういったケースでは1つのapp.pyだと肥大化しすぎて取り回しがつらくなるので、Blueprintsを使って機能グループの単位で分割するのがよいのではと考えています。こうしておけばどのモジュールでどんな機能が実装されているか一目で分かりますし、複数メンバで開発する時もソースコードが重複しないので作業の割り振りが楽になるというメリットもあるかと思います。

さいごに

今回はChaliceのBlueprints機能をご紹介しました。
Blueprintsも便利で優れた機能だと思うのですが、これだけをもって保守性の高いコードにできるとは言えず、やはり設計上の工夫は他にも色々必要と思っています。
今回の記事が誰かのお役に立てると幸いです。

Discussion