Cloud RunとFastAPIで、ChatBotをミニマムスタートしよう
- date:
2020-08-29
- author:
Kazuya Takei
- location:
PyCon JP 2020
- links:
イントロ
今回のお題
Google Cloud Run上に
FastAPIをベースにしたWebアプリケーションとして
Slackで簡易なをボットを提供する
という話をします。
今回のお題
Google Cloud Run上に
- FastAPIをベースにしたWebアプリケーションとして↑ここがメイン予定
Slackで簡易なをボットを提供する
という話をします。
Who am I
Kazuya Takei
NIJIBOX Co., Ltd
Server-side engineer
@attakei
Twitter
GitHub
and more
Who am I
Pythonista
Sphinx extensions
sphinx-revealjs
sphinxcontrib-gtagjs
免責
- 「ミニマムスタート」を主眼にしているため、一部の実装などを意図的に省いています。
- 所属とは無関係の発表です。個人の嗜好性を多分に含んだ内容となっています。
SlackのChatOps事情
よくある、SlackのChatOps
Slackbot/ワークフロー
サードパーティのSlackアプリ
各種カスタムIntegration
Slackbot/ワークフロー
Slackbot
特定のメッセージに反応するもの
反応内容を複数用意すると、ランダムで選ばれる
ワークフロー
各種トリガーに、フォーム表示とメッセージ送信をする
「チャンネルにメンバーが増えた時にメッセージ」ぐらいの簡単なやつ向け
サードパーティのSlackアプリ
Appディレクトリに登録されている様々なコラボレーション用アプリ
Googleカレンダー
当日の予定の案内
予定の時間帯にステータス変更
Polly
簡単なアンケート
GitHub/BitBucket/etc
Issue/PRの連携
各種カスタムIntegration
混み入ったことをしたいなら
カスタムBot
Incoming/Outgoing Webhooks
Slash Commands
各種カスタムIntegration
混み入ったことをしたいなら
カスタムBot
Incoming/Outgoing Webhooks
Slash Commands <= 今回はこれ
なぜSlash Commands?
Slackをトリガーに出来る
アプリケーションの実装が楽
Incoming Webhookを含んでる
単体でもいいし、カスタムBotにも組み込める
= ミニマムスタートに丁度いい
Slack Commands
Slash Commandsって?
/
から始まるショートカットコマンドを定義して、
特定のアクションを実行できるようにしたもの。
ビルトインのSlash Commands
普段、自分がよく使っているもの
/feed
: RSSフィードを使用する/remind
: リマインダー機能を使用する/invite
: チャンネルに招待する/archive
: チャンネルをアーカイブする
自作Slash Commandの仕組み
※基本的なもの
コマンドを実行する
指定されたURLへリクエストを行う
URLは3秒以内にレスポンスを返す
レスポンスを元に、メッセージとして表示する
自作Slash Commandの仕組み
レスポンスボディが...
- 空(0byte)=> 何もしない
- テキスト=> テキストのまま表示
- json=> Webhookと同じ
自作Slash Commandの仕組み
リクエストの中身(一部)
名前 |
中身 |
例 |
---|---|---|
team_domain |
チーム名 |
pyconjp-fellow |
user_name |
ユーザー |
attakei |
text |
コマンドの引数 |
-- |
response_url |
Webhook用URL |
-- |
-> Next ->
ローカル開発する
基本構成
Python 3.8系
Poetryでパッケージ管理
ngrokを使用
ngrok
NATやファイヤーウォールなどを越えて、 ローカルホストを外部公開できるサービス。
HTTPSのURLも利用可能
※今回の例では、最初に ngrok
プロセスを用意して、
ローカルサーバは常時固定ポートを使用します。
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
ngrok
補足として...
無料プランの場合、
コマンドのたびに公開URLが変わる
同時に用意できるプロセスは1個まで
不便な場合は有料プランをどうぞ
FastAPIでSlashCommandを作る
FastAPIとは
Python製Webアプリケーションフレームワーク
名前の通り「高速なAPIサーバー」向け
Starlette
というASGIフレームワークを土台にしている特に何も考えずに、
async/await
構文が使える
Flask
やBottle
っぽい
最小の構成
ミニマムな main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello_world():
return {"msg": "Hello, World!"}
最小の構成
サーバーを起動する
$ pip install fastapi uvicorn
$ uvicorn main:app
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [2719205] using statreload
INFO: Started server process [2719212]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:46564 - "GET / HTTP/1.1" 200 OK
動作確認
$ http localhost:8000
HTTP/1.1 200 OK
content-length: 23
content-type: application/json
{
"msg": "Hello, World!"
}
最小の構成との比較
Flask
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return json.dumps({
"msg":"Hello, World!"
})
Fast API
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello_world():
return {"msg": "Hello, World!"}
※見た目の関係で、意図的にコードを省いています
機能サンプル
ページネーション用のクエリパラメータ
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"}
機能サンプル
$ 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 integer",
"type": "type_error.integer"
}
]
}
その他機能
OpenAPIの提供
Pydanticによる恩恵各種
ミドルウェア
各種セキュリティ要求機能(Basic認証,OAuth2)
Dependency Injection
Background Task機能
Starletteが出来ること
FastAPIでChatBotローカル開発(1)
フォルダ構成
+ chatbot.py+ poetry.lock+ pyproject.toml
pyproject.toml
はウィザードに従い作成poetry.lock
はそこから自動生成
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"
chatbot.py
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}`"
}
ルーティング関数のデフォルト引数が Form
の場合、
POSTリクエストから取る
動きを確認
$ 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`"
}
実際に動かす
試したいワークスペースにSlashCommandを作成して
コマンドとURLの組み合わせを登録すれば
🎉 準備完了です 🎉
→→ デモ?
FastAPIでChatBotローカル開発(2)
よりChatOpsっぽいコマンド
/dig
を作ります
※ dig
= DNSの問い合わせをするツール
$ dig example.com
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 48418 IN A 93.184.216.34
Slack上のコマンドイメージ
/dig example.com
(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}```"
}
試してみる
$ 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\nflags QR RD RA\n;QUESTION\nexample.com. IN A\n;ANSWER\nexample.com. 20986 IN A 93.184.216.34\n;AUTHORITY\n;ADDITIONAL```"
}
試してみる
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```
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}```"
}
3秒しか猶予がない
対策をしないといけない
Background Task機能
FastAPIに標準提供されている機能。
レスポンスとは非同期に処理させたいことを、 バックグラウドのイベントループに引き渡すことが出来る。
これにより、 レスポンスを早期に返してメイン処理を別途実行することが可能になる
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()
Background Task機能
図解
asyncioのイベントループ上で処理しているだけなので、依存関係が増えない。 (ライブラリ的にも連携サービス的にも)
デモ?(擬似的なもの)
…これで、真っ当にミニマムなチャットボットが出来ました。
-> Next ->
Cloud Runで運用する
Cloud Runとは
コンテナ化されたアプリケーションをすばやく安全にデプロイ、スケーリングできる、フルマネージド型のコンピューティング プラットフォーム
(Cloud Runのトップページより)
Cloud Runとは
割と気軽に
コンテナWebアプリの実行を
ドメイン付きで
実現するサービス
3行で説明するCloud Runの使い方
GCPの契約をして、プロジェクトで必要なサービスを有効化する
ローカルでDockerアプリを開発してから、イメージをpush
サービスを起動する
図解
全てGCPでやる感じだと、こう
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"]
Cloud Runのメリット/デメリット
メリット:
マシン管理が不要になる
従量課金
- HTTPS+FQDNが自動で付与される(
{指定名+ランダムな文字列}.a.run.app
)
Cloud Runのメリット/デメリット
デメリット:
サーバー初動が遅い(微妙な頻度でタイムアウトする
運用にDockerの知識が少々必要
DDoS怖い
Cloud Runの未知の部分
請求対象期間は、
「リクエストを受け付けてインスタンスが起動した時刻」
から
「インスタンスが最後にリクエストの処理をした時刻」
非同期処理している間は?
インスタンスはいつ停止になる?
まとめ
発表振り返り
FastAPIの標準機能は強力で、ChatBotの実装を機能面・制約面に対する対応が比較的容易でした
Cloud Runベースの運用も比較的容易。ただし、この種のサービスは初動がネック
※やりきってみたい
ブラウザやCLIで確認してたネット系コマンドをSlashCommandsで提供するSlack App
dig
whois
openssl
(HTTPS validation)
各種リンク集 - Slack
Slack API
Slash Commands
各種リンク集 - FastAPI
Fast API
Starlette
各種リンク集 - Cloud Run
Cloud Run
各種リンク集 - その他
Sphinx
sphinx-revealjs
各種リンク集 - その他
NIJIBOX Co., Ltd