MOB-LOG

モブおじの記録 (Programming, 統計・機械学習)

(運用準備・環境構築編) Optunaを用いたコーヒーのハンドドリップ最適化のSlackBot (Human in the Coffee Loop)

はじめに

Optunaを用いたコーヒーのハンドドリップ最適化をしようとしていて(下記参照)、 moblog.hatenablog.jp いまSlackBotに落とし込んだところ (下記参照)。 moblog.hatenablog.jp

その環境構築とかをまとめる。

システムの概要

システムの概要は過去記事と、以下画像を参照のこと。

Botを動かすとしてSlackのApplication Clientから仲介されたリクエストを受け付けるサーバが必要となるが、

のどちらかが想定できる。今回は自身の適当なサーバをNgrokで安全に公開した。

環境構築

以下のパッケージやソフトウェアのインストール、認証情報の配置は、SlackBotを動作させるサーバーで行う(起動し続けるならラップトップでもいい)。

SlackBotの準備 (Workspaceでの設定)

SlackAppでのbotの作成が必要で、https://api.slack.com/apps から適当に作っておいてWorkspaceに追加しておこう(Webで探せばやり方は見つかる)。

スコープの設定

Botが何をどこまでできるかの範囲(スコープ)を決める。今回は

の2つのみで十分 (コマンドとか設定していない)。

WorkspaceのBotの管理画面で、スコープが変更できる。スコープを追加後はReinstall すること。

SlackBotの設定テスト

以下のリクエストでメッセージを送れるかどうかのテストができる (SlackBotとしてSlackのサーバにPOSTを送ってWorkspaceへのポストの権限があるかどうかを確認)。

$ curl -X POST -F channel=[channel ID] -F text="Slack投稿テスト" https://slack.com/api/ch
at.postMessage -H "Authorization: Bearer xoxb-[hogehoge]"

xoxb-[hogehoge]の部分は、https://api.slack.com/apps/ のOAuthのタブから OAuth Token がわかる。

OAuth and Permissions

[Channel ID]Bot自身のものか、どこかのChannelにInviteしてそこのChannel IDを使用。

アプリの詳細からBotのDMのチャンネルIDを確認

成功したらJson"ok":trueが返って来て、次のような投稿が飛んでくる。

SlackAppへのPOSTのテスト(投稿成功)。楽しくて2回POSTした。

$ curl -X POST -F channel=[channel ID] -F text="Slack投稿テスト" https://slack.com/api/chat.postMessage -H "Authorization: Bearer xoxb-***hogehoge***"
{"ok":true,"channel":"[channel ID]","ts":"1687491751.172649","message":{"bot_id":"B05DYD5V3EG","type":"message","text":"Slack\u6295\u7a3f\u30c6\u30b9\u30c8","user":"U05DT2A1WAH","ts":"1687491751.172649","app_id":"A01TM7Q7YP8","blocks":[{"type":"rich_text","block_id":"Mfi3k","elements":[{"type":"rich_text_section","elements":[{"type":"text","text":"Slack\u6295\u7a3f\u30c6\u30b9\u30c8"}]}]}],"team":"TBHPDDE7N","bot_profile":{"id":"B05DYD5V3EG","app_id":"A01TM7Q7YP8","name":"Human in the Coffee Loop","icons":{"image_36":"https:\/\/a.slack-edge.com\/80588\/img\/plugins\/app\/bot_36.png","image_48":"https:\/\/a.slack-edge.com\/80588\/img\/plugins\/app\/bot_48.png","image_72":"https:\/\/a.slack-edge.com\/80588\/img\/plugins\/app\/service_72.png"},"deleted":false,"updated":1687487127,"team_id":"TBHPDDE7N"}}}

以下のようにScopeが足りないぞと言われたらスコープを追加しよう *1

$ curl -X POST -F channel=[channel ID] -F text="Slack投稿テスト" https://slack.com/api/ch
at.postMessage -H "Authorization: Bearer xoxb-[hogehoge]"
{"ok":false,"error":"missing_scope","needed":"chat:write:bot","provided":"app_mentions:read,bookmarks:read,commands"}

"ok":false,"error":"missing_scope","needed":"chat:write:bot"

Scopeを変更したらReinstallが必要。← スコープを変更すると以下のように reinstall your appしろと言われる。

「⚠️You’ve changed the permission scopes your app uses. Please reinstall your app for these changes to take effect (and if your app is listed in the Slack App Directory, you’ll need to resubmit it as well

ngrokでローカルホストとngrokのグローバルIPマッピング

ngrokを起動して得られるngrokのIPからlocalhost:5000へとポートフォワーディングするのは以下のコマンドをたたくだけ。

ngrok http 5000

出力された Forwarding にGlobal IPが記されている。

ngrok
(Ctrl+C to quit)
Send your ngrok traffic logs to Datadog: https://ngrok.com/blog-post/datadog-logs
Session Status                online
Session Expires               1 hour, 56 minutes
Terms of Service              https://ngrok.com/tos
Version                       3.3.1
Region                        Europe (eu)
Latency                       37ms
Web Interface                 http://127.0.0.1:4040
**Forwarding                    https://6828-193-167-228-172.eu.ngrok.io -> http://localhost:5000**
Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

アカウントを登録していないとSessionは2時間で切れてしまうようだが、Freeアカウントを登録してからセッションを開始すると(ngrok実行環境のconfigファイルをいじる必要がある←アカウントページのコマンドを打つだけでいい)、Session Expiresが消えて時間制限がなくなる(?)。(ほんとに?すごい)

ngrokのauthtoken

ngrok
(Ctrl+C to quit)
Send your ngrok traffic logs to Datadog: https://ngrok.com/blog-post/datadog-logs

Session Status                online
Account                       hogehoge@gmail.com (Plan: Free)
Version                       3.3.1
Region                        Europe (eu)
Latency                       37ms
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://34ee-193-167-228-172.ngrok-free.app -> http://localhost:5000
Connections                   ttl     opn     rt1     rt5     p50     p90
                                0       0       0.00    0.00    0.00    0.00

↑ アカウント登録後に要求したセッション。Session Expiresがなくなっている(無限なのか?すごすぎ)。URLは発行ごとに変わってしまうので、サーバ落とした後とかには再発行してSlackAppのRequest URLに変更したURLを再登録する必要がある。Upgradeすると固定ドメインが作れる/設定できるらしい(1月10USD程度)。

以下のように--domainオプションで指定できる様子。[hogehoge].ngrok-free.appngrokで予約されたサブドメイン

ngrok http --domain=[hogehoge].ngrok-free.app 5000

SlackAppのEventへ登録

Request URLにアプリのURL (ngrokで発行されたものか、レンタルサーバの公開URL) を登録する ([url]/slack/events)。

Subscribe to bot eventsで、受け付けるイベントの種類を登録しておく *2

SlackAppの「Event Subscriptions」で、要求に応答するendpointと(Request URL)、イベントを登録する(Subscribe to bot events)。

app_mentionイベントはDMのメッセージで発生しないらしい。massage.imイベントがDM専用のイベントっぽい。

Messages sent to your app in direct message conversations are not dispatched via app_mention, including messages sent from other apps, regardless of whether your app is explicitly mentioned or otherwise. Subscribe to [message.im](https://api.slack.com/events/message.im) events to receive messages directed to your bot user in direct message conversations.

Google Spreadsheet の用意

Google Sheet をデータベースとして使用しているので、APIを取得するのと権限の設定をしなければいけない。これが一番面倒くさい。

簡単に書き出すと

  1. Google Cloud Platform (GCP) 上でプロジェクトの作成
  2. Google Spreadsheets APIGoogle Drive APIの有効化
  3. IAMからサービスアカウントの作成
  4. サービスアカウントに対する秘密キーの作成
  5. Sheetを作成して、サービスアカウントに編集権限を追加する
  6. 秘密キーとシートのIDをサーバに配置する

が必要となる。

1—5は「PythonGoogle Sheetを編集 GCP API」とか検索するといくらでも出てくるので各自やってもらいたい(ここが面倒すぎて他人におすすめできていない)。

以下のようにワークブックへ接続しているので、実行ディレクトリの

  • ./data/sheet/human-in-the-coffee-loop-d4b97ca67511.jsonにサービスアカウントの秘密キー
  • ./data/sheet/sheet_id.datにspreadsheetのsheet id

を保存する。

# サービスアカウントの認証
dir_base = './data/sheet/'
gc = gspread.service_account(os.path.join(dir_base, 'human-in-the-coffee-loop-d4b97ca67511.json'))

# ワークブックへの接続
with open(os.path.join(dir_base, 'sheet_id.dat')) as f:
sheet_id = f.readlines()

workbook = gc.open_by_key(sheet_id[0])

Botの動作環境

インストール

以下が必要パッケージ (Dockerやらvenvで環境は分けよう)。

pip install flask slack slackclient slack_sdk slackeventsapi python-dotenv
pip install gspread optuna # for other than slackbot
  • slack_sdkslackがどっちなのかはしらん (どっちかが統合された?)。
  • python-dotenv はやっぱり OAuth token 等を扱うので必須。

以下から Human in the Coffee Loop のプロジェクトをCloneする。

git clone [git@github.com](mailto:git@github.com):indiaki1o/Human-in-the-Coffee-Loop.git

SlackBotの認証情報の設置

dotenvでBotの OAuth token と Signing secret を管理しているので、プロジェクト直下に.env ファイルを作成し、以下のように保存しておく。

SLACK_BOT_OAUTH_TOKEN=xoxb-[OAuth token]
SLACK_SIGNING_SECRET=[Signing secret]

Slack App マネージメント画面の Settings > Basic informationから Signing secret を確認できる。

実行・運用

SlackBotを動作させるコンピュータ上で Human_in_the_Coffee_Loop_SlackApp.py を実行すると、 * Running on http://127.0.0.1:5000 と表示されるようにFlaskサーバがLocalhostで動作するので、そのポート番号をngrokにフォワードしてもらう。

$ python ./Human_in_the_Coffee_Loop_SlackApp.py
DEBUG:slack_sdk.web.legacy_base_client:Sending a request - url: https://www.slack.com/api/auth.test, query_params: {}, body_params: {}, files: {}, json_body: None, headers: {'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': '(redacted)', 'User-Agent': 'Python/3.10.11 slackclient/3.21.3 Windows/10'}
 * Serving Flask app 'Human_in_the_Coffee_Loop_SlackApp'
 * Debug mode: on
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
INFO:werkzeug:Press CTRL+C to quit
INFO:werkzeug: * Restarting with stat
INFO:werkzeug: * Debugger PIN: XXX-XXX-XXX
ngrok http 5000 &

実際はSSHセッションが切れるとFlaskサーバもngrokもKillされてしまうので、nohup を用い、殺されないように実行しよう (ngrokがすでにフォワードしているなら再度起動する必要はない)。

nohup python ./Human_in_the_Coffee_Loop_SlackApp.py &
nohup ngrok http 5000 &

ログはloggingパッケージを使用しておりstderrに出力されるため(デフォルトの設定から変えていない)、ログを取りたければリダイレクトをすること (以下のコマンドだと主に stderr.log へ出力されるはず)。

$ nohup python ./Human_in_the_Coffee_Loop_SlackApp.py 1>> stdout.log 2>> stderr.log &

ここまで来たら、BotにDMを送ればレシピを送ってくれるはず。

SlackBotのレシピ応答

おわりに

環境構築長すぎ面倒すぎる。

*1:chat:write:bot と書いているが、やろうとしていることがbotのDMでのchat.postMessage なので、chat:writeと、もしかしたらchat:write.customize の追加が必要?

*2:イベントの種類はこちらを参考に:https://api.slack.com/events/app_mention