Slide 1

Slide 1 text

Cloud RunとFastAPIで、ChatBotをミニマ ムスタートしよう date: 2020-08-29 author: Kazuya Takei location: PyCon JP 2020 links: #pyconjp_4

Slide 2

Slide 2 text

イントロ

Slide 3

Slide 3 text

今回のお題 Google Cloud Run上に FastAPIをベースにしたWebアプリケーションとして Slackで簡易なをボットを提供する という話をします。

Slide 4

Slide 4 text

今回のお題 Google Cloud Run上に FastAPIをベースにしたWebアプリケーションとして ↑ここがメイン予定 Slackで簡易なをボットを提供する という話をします。

Slide 5

Slide 5 text

Who am I Kazuya Takei NIJIBOX Co., Ltd Server-side engineer @attakei Twitter GitHub and more

Slide 6

Slide 6 text

Who am I Pythonista Sphinx extensions sphinx-revealjs sphinxcontrib-gtagjs

Slide 7

Slide 7 text

免責 「ミニマムスタート」を主眼にしているため、 一部の実装などを意図的に省いています。 所属とは無関係の発表です。 個人の嗜好性を多分に含んだ内容となっています。

Slide 8

Slide 8 text

SlackのChatOps事情

Slide 9

Slide 9 text

よくある、SlackのChatOps Slackbot/ワークフロー サードパーティのSlackアプリ 各種カスタムIntegration

Slide 10

Slide 10 text

Slackbot/ワークフロー Slackbot 特定のメッセージに反応するもの 反応内容を複数用意すると、ランダムで選ばれる ワークフロー 各種トリガーに、フォーム表示とメッセージ送信をす る 「チャンネルにメンバーが増えた時にメッセージ」ぐ らいの簡単なやつ向け

Slide 11

Slide 11 text

サードパーティのSlackアプリ Appディレクトリに登録されている様々なコラボレーショ ン用アプリ

Slide 12

Slide 12 text

各種カスタムIntegration 混み入ったことをしたいなら カスタムBot Incoming/Outgoing Webhooks Slash Commands

Slide 13

Slide 13 text

各種カスタムIntegration 混み入ったことをしたいなら カスタムBot Incoming/Outgoing Webhooks Slash Commands <= 今回はこれ

Slide 14

Slide 14 text

なぜSlash Commands? Slackをトリガーに出来る アプリケーションの実装が楽 Incoming Webhookを含んでる 単体でもいいし、カスタムBotにも組み込める = ミニマムスタートに丁度いい

Slide 15

Slide 15 text

Slack Commands

Slide 16

Slide 16 text

Slash Commandsって? / から始まるショートカットコマンドを定義して、 特定の アクションを実行できるようにしたもの。

Slide 17

Slide 17 text

ビルトインのSlash Commands 普段、自分がよく使っているもの /feed : RSSフィードを使用する /remind : リマインダー機能を使用する /invite : チャンネルに招待する /archive : チャンネルをアーカイブする

Slide 18

Slide 18 text

自作Slash Commandの仕組み ※基本的なもの コマンドを実行する 指定されたURLへリクエストを行う URLは3秒以内にレスポンスを返す レスポンスを元に、メッセージとして表示する

Slide 19

Slide 19 text

自作Slash Commandの仕組み レスポンスボディが… 空(0byte) => 何もしない テキスト => テキストのまま表示 json => Webhookと同じ

Slide 20

Slide 20 text

自作Slash Commandの仕組み リクエストの中身(一部) 名前 中身 例 team_domain チーム名 pyconjp-fellow user_name ユーザー attakei text コマンドの引数 – response_url Webhook用URL –

Slide 21

Slide 21 text

-> Next ->

Slide 22

Slide 22 text

ローカル開発する

Slide 23

Slide 23 text

基本構成 Python 3.8系 Poetryでパッケージ管理 ngrokを使用

Slide 24

Slide 24 text

ngrok NATやファイヤーウォールなどを越えて、 ローカルホスト を外部公開できるサービス。 HTTPSのURLも利用可能 ※今回の例では、最初に ngrok プロセスを用意して、 ロー カルサーバは常時固定ポートを使用します。

Slide 25

Slide 25 text

ngrok こんな感じに動く $ ngrok http 8000 Session Status online Account Kazuya Takei (Plan: Free) Update update available (version 2.3.35, Ctrl-U to update) Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://8e1d754c956e.ngrok.io -> http://localhost:8000 Forwarding https://8e1d754c956e.ngrok.io -> http://localhost:8000

Slide 26

Slide 26 text

ngrok 補足として… 無料プランの場合、 コマンドのたびに公開URLが変わる 同時に用意できるプロセスは1個まで 不便な場合は有料プランをどうぞ

Slide 27

Slide 27 text

FastAPIでSlashCommand を作る

Slide 28

Slide 28 text

FastAPIとは Python製Webアプリケーションフレー ムワーク 名前の通り「高速なAPIサーバー」 向け Starlette というASGIフレームワ ークを土台にしている 特に何も考えずに、 async/await 構文が使える Flask や Bottle っぽい

Slide 29

Slide 29 text

最小の構成 ミニマムな main.py from fastapi import FastAPI app = FastAPI() @app.get("/") async def hello_world(): return {"msg": "Hello, World!"}

Slide 30

Slide 30 text

最小の構成 サーバーを起動する 動作確認 $ pip install fastapi uvicorn $ uvicorn main:app INFO: Uvicorn running on http://127.0.0 INFO: Started reloader process [2719205 INFO: Started server process [2719212] INFO: Waiting for application startup. INFO: Application startup complete. INFO: 127.0.0.1:46564 - "GET / HTTP/1.1 $ http localhost:8000 HTTP/1.1 200 OK content-length: 23 content-type: application/json { "msg": "Hello, World!" }

Slide 31

Slide 31 text

最小の構成との比較 Flask Fast API ※見た目の関係で、意図的にコードを省いています from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return json.dumps({ "msg":"Hello, World!" }) from fastapi import FastAPI app = FastAPI() @app.get("/") async def hello_world(): return {"msg": "Hello, World!"}

Slide 32

Slide 32 text

機能サンプル ページネーション用のクエリパラメータ from fastapi import FastAPI app = FastAPI() @app.get("/items/") async def get_items(p: int = 1): """ ``/items/`` => ``p`` = 1 ``/items/?p=3`` => ``p`` = 3 ``/items/?p=three`` => バリデーションエラー """ return {"msg": "Hello World"}

Slide 33

Slide 33 text

機能サンプル $ http localhost:8000/items/ p==3 HTTP/1.1 200 OK { "msg": "Hello World" } $ http localhost:8000/items/ p==three HTTP/1.1 422 Unprocessable Entity { "detail": [ { "loc": [ "query", "p" ], "msg": "value is not a valid int "type": "type_error.integer" } ] }

Slide 34

Slide 34 text

その他機能 OpenAPIの提供 Pydanticによる恩恵各種 ミドルウェア 各種セキュリティ要求機能(Basic認証,OAuth2) Dependency Injection Background Task機能 Starletteが出来ること

Slide 35

Slide 35 text

FastAPIでChatBotローカ ル開発(1)

Slide 36

Slide 36 text

フォルダ構成 pyproject.toml はウィザードに従い作成 poetry.lock はそこから自動生成 + chatbot.py + poetry.lock + pyproject.toml

Slide 37

Slide 37 text

pyproject.toml 重要なところだけ抜粋 [tool.poetry] name = "chatbot" [tool.poetry.dependencies] python = "^3.8" # 本体 fastapi = "^0.60.1" # POSTリクエスト時にformdataを受け付けるのに必要 python-multipart = "^0.0.5" # ASGIサーバー uvicorn = "^0.11.7"

Slide 38

Slide 38 text

chatbot.py ルーティング関数のデフォルト引数が Form の場合、 POST リクエストから取る from fastapi import FastAPI, Form app = FastAPI() @app.post("/slash-commands/hello") async def hello(text: str = Form(...), user_name: str = Form(...)): return { "text": f"Hello {user_name}, I recieved `{text}`" }

Slide 39

Slide 39 text

動きを確認 $ http --form localhost:8000/slash-commands/hello text=Example user_name=attakei HTTP/1.1 200 OK content-length: 46 content-type: application/json date: Fri, 28 Aug 2020 18:14:23 GMT server: uvicorn { "text": "Hello attakei, I recieved `Example`" }

Slide 40

Slide 40 text

実際に動かす 試したいワークスペースにSlashCommandを作成して コマンドとURLの組み合わせを登録すれば 準備完了です →→ デモ?

Slide 41

Slide 41 text

FastAPIでChatBotローカ ル開発(2)

Slide 42

Slide 42 text

よりChatOpsっぽいコマンド /dig を作ります ※ dig = DNSの問い合わせをするツール Slack上のコマンドイメージ $ dig example.com ;; QUESTION SECTION: ;example.com. IN A ;; ANSWER SECTION: example.com. 48418 IN A 93.184.216.34 /dig example.com

Slide 43

Slide 43 text

(1)を拡張して作る、 /dig # import を省略 # dns-pythonを使います app = FastAPI() @app.post("/slash-commands/dig") async def dig(text: str = Form("")): # DNSPythonを使ってDNS情報を取得する name = dns.name.from_text(text) query = dns.message.make_query(name, dns.rdatatype.A) msg = dns.query.udp(query, "8.8.8.8") return { "text": f"Answer for `A` of `{name}`\n```{msg}```" }

Slide 44

Slide 44 text

試してみる $ http --form localhost:8000/slash-commands/dig text=example.com HTTP/1.1 200 OK content-length: 207 content-type: application/json date: Fri, 28 Aug 2020 18:18:19 GMT server: uvicorn { "text": "Answer for `A` of `example.com.`\n```id 31266\nopcode QUERY\nrcode NOERROR }

Slide 45

Slide 45 text

試してみる Answer for `A` of `example.com.` ```id 31266 opcode QUERY rcode NOERROR flags QR RD RA ;QUESTION example.com. IN A ;ANSWER example.com. 20986 IN A 93.184.216.34 ;AUTHORITY ;ADDITIONAL```

Slide 46

Slide 46 text

3秒しか猶予がない # import を省略 app = FastAPI() @app.post("/slash-commands/dig") async def dig(text: str = Form("")): name = dns.name.from_text(text) query = dns.message.make_query(name, dns.rdatatype.A) # もし、ここで想定外の時間がかかったら? msg = dns.query.udp(query, "8.8.8.8") return { "text": f"Answer for `A` of `{name}`\n```{msg}```" }

Slide 47

Slide 47 text

3秒しか猶予がない 対策をしないといけない

Slide 48

Slide 48 text

Background Task機能 FastAPIに標準提供されている機能。 レスポンスとは非同期に処理させたいことを、 バックグラ ウドのイベントループに引き渡すことが出来る。 これにより、 レスポンスを早期に返してメイン処理を別途 実行することが可能になる

Slide 49

Slide 49 text

Background Task機能 # import を省略 app = FastAPI() async def query_dns(name: str, url: str): name = dns.name.from_text(name) query = dns.message.make_query(name, dns.rdatatype.A) await asyncio.sleep(5) # 時間のかかる仮定 msg = dns.query.udp(query, "8.8.8.8") slack = slackweb.Slack(url) slack.notify(text=f"Answer for `A` of `{name}`\n```{msg}```") @app.post("/slash-commands/dig") async def dig( bg: BackgroundTasks, text: str = Form(""), response_url: str = Form("") ): # 直接処理せずに、移譲する bg.add_task(query_dns, text, response_url) # 空のレスポンスを返せば、まずは何もしない return Response()

Slide 50

Slide 50 text

Background Task機能 図解 asyncioのイベントループ上で処理しているだけなので、依 存関係が増えない。 (ライブラリ的にも連携サービス的に も)

Slide 51

Slide 51 text

デモ?(擬似的なもの) …これで、真っ当にミニマムなチャットボットが出来まし た。

Slide 52

Slide 52 text

-> Next ->

Slide 53

Slide 53 text

Cloud Runで運用する

Slide 54

Slide 54 text

Cloud Runとは (Cloud Runのトッ プページより) コンテナ化されたアプリケ ーションをすばやく安全に デプロイ、スケーリングで きる、フルマネージド型の コンピューティング プラッ トフォーム

Slide 55

Slide 55 text

Cloud Runとは 割と気軽に コンテナWebアプリの実行を ドメイン付きで 実現するサービス

Slide 56

Slide 56 text

3行で説明するCloud Runの使い方 GCPの契約をして、プロジェクトで必要なサービスを 有効化する ローカルでDockerアプリを開発してから、イメージを push サービスを起動する

Slide 57

Slide 57 text

図解 全てGCPでやる感じだと、こう

Slide 58

Slide 58 text

Cloud Run向けDockerfile(例) # ------------------ # Build stage # ------------------ FROM python:3.8-slim as buildenv RUN mkdir -p /build/chatbot COPY chatbot/* /build/chatbot/ COPY poetry.lock pyproject.toml /build/ WORKDIR /build RUN poetry build # ------------------ # Running stage # ------------------ FROM python:3.8-slim COPY --from=buildenv /build/dist/chatbot-*-py3-none-any.whl / RUN pip install /chatbot-*-py3-none-any.whl CMD ["uvicorn", "chatbot:app", "--port", "8080"]

Slide 59

Slide 59 text

Cloud Runのメリット/デメリット メリット: マシン管理が不要になる 従量課金 HTTPS+FQDNが自動で付与される ( {指定名+ランダムな文字列}.a.run.app )

Slide 60

Slide 60 text

Cloud Runのメリット/デメリット デメリット: サーバー初動が遅い(微妙な頻度でタイムアウトする 運用にDockerの知識が少々必要 DDoS怖い

Slide 61

Slide 61 text

Cloud Runの未知の部分 請求対象期間は、 「リクエストを受け付けてインスタンスが起動した時刻」 から 「インスタンスが最後にリクエストの処理をした時刻」 非同期処理している間は? インスタンスはいつ停止になる?

Slide 62

Slide 62 text

まとめ

Slide 63

Slide 63 text

発表振り返り FastAPIの標準機能は強力で、ChatBotの実装を機能 面・制約面に対する対応が比較的容易でした Cloud Runベースの運用も比較的容易。ただし、この 種のサービスは初動がネック

Slide 64

Slide 64 text

※やりきってみたい ブラウザやCLIで確認してたネット系コマンドを SlashCommandsで提供するSlack App dig whois openssl (HTTPS validation)

Slide 65

Slide 65 text

各種リンク集 - Slack Slack API Slash Commands https://api.slack.com/ https://api.slack.com/interactivity/slash- commands#responding_to_commands

Slide 66

Slide 66 text

各種リンク集 - FastAPI Fast API Starlette https://fastapi.tiangolo.com/ https://www.starlette.io

Slide 67

Slide 67 text

各種リンク集 - Cloud Run Cloud Run https://cloud.google.com/run?hl=ja

Slide 68

Slide 68 text

各種リンク集 - その他 Sphinx sphinx-revealjs https://sphinx-doc.org/ja/master/ https://sphinx-revealjs.readthedocs.io/

Slide 69

Slide 69 text

各種リンク集 - その他 NIJIBOX Co., Ltd https://nijibox.jp