FastAPI, Azure WebAppsでLINEBot実装の事始め

5 min read読了の目安(約5000字

はじめに

LINEBotのよくある開発手法を調べると、全体の規模的にFlaskなどの薄いフレームワークで実装することをよく目にする気がしています(実際に自分もFlaskでよく作っていた)。
最近個人的によく目にするFastAPI で実装すると色々嬉しいことがありそうと思い、既存のFlaskで作っていたLINEBotを書き換えようかなと思ったついでにZennデビューをかまそうと思い、雑に記事を書くことにします。

FastAPIについて

FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。
(https://fastapi.tiangolo.com/ja/ より)

主な特徴としては

  • GoやNode.js並みの高いパフォーマンス
  • シンプルな記述でAPI実装できる。(Flaskと同じぐらいのコード量で実装可能)
  • OpenAPI規格に基づくドキュメントの自動生成
  • リクエスト, レスポンスの型定義
    • 型自動補完
    • リクエストのvalidation
    • 自動生成されるドキュメントに反映
  • 非同期処理対応

高いパフォーマンスを出せるというところでは、内部でStarlette, Pydanticを利用しているからと公式に記載されています。
FastAPIがASGI(Asynchronous Server Gateway Interface)で動くようになっており、ASGIのframework/toolkitであるStarletteを用いることで、受け取ったHTTP Requestをasync定義した関数に渡して非同期処理を走らせながらレスポンスを返す、というように効率的な処理を行うことができます。
データの扱いの部分では、Pythonの型ヒントに基づくvalidation/serializationを行うPydanticを使って、APIを実装する上での無効な型のデータが渡されるケースを考慮する部分のコード量の削減などができます。

LINEBot開発をFastAPIで実装するモチベーション

外部APIや他データストアと連携をすることで、ユーザーが欲しいデータをユーザー自身が簡単にアクセスするためのより良いインターフェースを構築するという面で、LINEBotは非常に有効だと個人的に思います。

その際に外部APIへのリクエスト部分を非同期で行いつつ、LINEサーバーへレスポンスをすぐに返すといった実装がしやすいのではないかと思い、FastAPIを使って実装しようと思いました。

まずは事始めとしてオウム返し

とはいえまずはオウム返しからやってみようと思い、実際にオウム返しをするLINEBotをFastAPIで構築し、それをAzure WebAppsというPaaSで実際に稼働させてみようと思います。

ここでは最初のLINE側のセットアップを省略したいと思います。
そちらを確認したいという方は以下の記事を参考にされるといいと思います。

https://qiita.com/nkjm/items/38808bbc97d6927837cd

必要なライブラリ

今回はFastAPI本体と、ASGIのライブラリであるuvicorn, 非同期対応のLINE SDKであるaiolinebotをインストールします

$ pip install fastapi uvicorn aiolinebot

オウム返しを行うLINEBotのコード

実際にオウム返しを行うLINEBotのコードは以下になります。

app.py
from fastapi import FastAPI, Request, BackgroundTasks 
from linebot import WebhookParser
from linebot.models import TextMessage
from aiolinebot import AioLineBotApi

import os
from os.path import join, dirname
from dotenv import load_dotenv

load_dotenv(verbose=True)

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

line_api = AioLineBotApi(channel_access_token=os.environ.get("LINEBOT_CHANNEL_ACCESS_TOKEN"))
parser = WebhookParser(channel_secret=os.environ.get("LINEBOT_CHANNEL_SECRET"))

app = FastAPI()

@app.get("/health")
def health_check():
    return {'status': 'OK'}

@app.post("/callback")
async def callback(request: Request, background_tasks: BackgroundTasks):
    events = parser.parse(
        (await request.body()).decode("utf-8"),
        request.headers.get("X-Line-Signature", "")
    )

    background_tasks.add_task(handle_events, events=events)

    return "ok"

async def handle_events(events):
    for event in events:
        try:
            await line_api.reply_message_async(
                event.reply_token,
                TextMessage(text=event.message.text))
        except Exception:
            pass

Flaskを使って構築した方は、ほとんど同じコード量であることがわかるかと思います。
今回はまず個人で試したのと、純粋なオウム返しなのでPydanticによるrequest, responseのvalidation/serializationの実装をしてません(すみませんm(_ _)m)

コードについてですが、まずLINE経由のユーザーのRequestを受ける部分としてcallbackという関数があります。(LINEBotのWebhookを設定するエンドポイントにhttps://my-domain.com/callback を設定している想定です。)
callback内で受け取ったrequestをeventsとしてparseして、そのeventsをbackgroundで行うhandle_eventsに渡しています。これをすることでユーザーへLINEで返信する処理を非同期で行いつつ、先にLINEサーバー側へokのレスポンスを返しています。

あとはこれを動かすために以下のコマンドを実行します。

$ uvicorn app:app --debug

ローカルで検証する際はngrokを使って以下のコマンドを実行し、生成されたhttpsのエンドポイントをLINE側で設定すると確認することができます。

$ ngrok http 8000

Azure WebAppsにデプロイする

次にこれをAzure WebAppsでデプロイして本番稼働させたいと思います。
まずはAzureのApp Serviceから作成するWebAppsの設定を行います。

ここは通常のPythonのほかフレームワークなどを使ったWebアプリの設定と変わらないと思います。

次に作成したApp Serviceに実装したFastAPIのコードをデプロイします。
ここではAzure App Serviceが持っているデプロイセンターという機能を使ってデプロイします。これを使うとGitHubやBitBucketのリポジトリとそのブランチを設定すると、そのブランチにpushされた際にデプロイのフローが走ります。

https://docs.microsoft.com/ja-jp/azure/app-service/deploy-continuous-deployment

これも通常のデプロイと変わらないと思います。
次にLINEBOTのアクセストークンなどの環境変数を、構成というところから設定できるのでこれを設定します。

ここまでをやるとFlaskの場合であれば動くのですが、FastAPIの場合、ASGIのライブラリのuvicornで動くので、このままだとServer Errorになってしまいます。

そこでAzure WebAppsの構成からスタートアップコマンドというところを設定できるので、以下の内容を含んだテキストファイルを新たに追加して再デプロイすることで動くようになります。

startup.txt
python -m uvicorn app:app --host 0.0.0.0

これでFastAPI, Azure WebAppsでLINE Botを稼働させるための設定は以上です。
今回はシンプルなオウム返しを行うだけのものだったんですが、外部APIなどへのリクエストを含んだBotの実装を行う際のrequest, responseのvalidation/serializationの実装や同じ動作を行うFlaskとのパフォーマンス面の比較などを今後していきたいと思いました。
他にもgrapheneを使うことでGraphQLの実装も対応できるみたいなので、これも試してみたいですね。

https://github.com/graphql-python/graphene