Slack の Chatbot を作ってみたいなぁ・・・と、なんとなく思いながらも、でも、会話ロボットがほしいわけでもないので、手を出してなかったのですが、 AWS Chatbot で ChatOps の一端に触れる機会があり、少し興味が出てきたので、どんなものなのかを調べてみることにしました。
うまくいけば、社内専用の Chatbot を育てながら、普段の仕事の中で ChatOps が実践できるとよいなと思ってます。
この記事では、サンプルを動かしたところまでを順を追ってご紹介します。
Chatbot のフレームワーク
普段一番使っているのが Slack なので、Slack の Bot を作ります。
できるだけ仕事で使える ChatOps にしたいので、最初から Chatbot のフレームワークを使って、先々の高機能化に耐えられるものに目指します。
そんなわけで、npm trends で比較検索してみました。
Hubot というのを見聞きしたことがありましたが Bot Builder の Download 数が一番多いようです。
Bot Builder は Microsoft の OSS で、開発が止まってしまう心配もないので、これを使うことにします。
Bot Builder は、対応している Chat が 2020年11月時点で、21 種類もあって、SDKが4つの言語用(C# / JavaScript / Python / Java)に用意されています。
そして、サンプル実装が、大量にあります。
今回は、この中から Python SDK を使って Slack のサンプルを動かすことにします。
- Bot Framework SDK:
https://github.com/microsoft/botframework-sdk - Bot Framework SDK for Python:
https://github.com/microsoft/BotBuilder-python - サンプル:
https://github.com/microsoft/BotBuilder-Samples/ - Slack Adapterのサンプル
https://github.com/microsoft/BotBuilder-Samples/tree/main/samples/python/60.slack-adapter
他にも、 Slack Bot の仕組みも、読んでおいた方がよいでしょう。
こちらの記事が、判りやすかったように思います。
- Slack Botの種類と大まかな作り方
https://qiita.com/namutaka/items/233a83100c94af033575
Slack の API は、 Slack Adapter が吸収してくれるので、当面は、Slack API の仕様は不要かと思いますが、こちらで参照することができます。
- Slack API
https://api.slack.com/apis
上記の Slack Adapterのサンプル は Azure Bot Service を使って Slack に接続してメッセージに応答するシンプルなエコーボットです。
でも仕事では、AWS を使うことが多いので、 Lambda にデプロイして Slack Bot をサーバーレスで運用 できるようにします。
ローカル環境を作成する
開発は、Dockerコンテナで行いますが、環境を作る前に、メッセージが、どんな感じで流れるのかを説明します。
サンプルの Bot は、Slack の Event API を使っています。
前提
- コンテナ内の Bot は、Webサーバー機能が動いていて Slack の Event を受信する API が実装されてある
- ngrok でパブリックな URL を取得して ローカルPC で動く Bot の Event 受信API にフォワードされる
- Slack アプリの Event Subscriptions の Request URL に ngrok の URL を設定する
- Slack アプリをチャンネルに追加する
メッセージの流れ
- チャンネルでメッセージを投稿する
- Slack が ngrok の URL にメッセージを POST する
- ngrok がコンテナ内の Bot にメッセージをフォワードする
- コンテナ内の Bot は POST されたメッセージを読んで、Slack に同じ文言でメッセージ送信する
- チャンネルに投稿したものと同じメッセージがエコーされる
サンプルをコピーして作業開始
それでは、ローカルに環境を作成していきます。
# サンプルを clone
git clone https://github.com/microsoft/BotBuilder-Samples.git
# botディレクトリを作成して、Slack Adapterのサンプルだけコピーする
mkdir bot
cd bot
cp -r ../BotBuilder-Samples/samples/python/60.slack-adapter/* .
開発用の requirements.txt
開発時にのみ使うモジュールを requirements-dev.txt
に追加します。
# ランタイムに必要なパッケージ
-r requirements.txt
# 開発環境にのみ必要なパッケージ
awscli
ptvsd
pylint
Dockerイメージ
Lambdaで実行するので、 Lambda の docker イメージを使います。
BotBuilder-python が Python 3.8 前提なので、Dockerイメージも Python3.8 にします。
https://github.com/microsoft/BotBuilder-python/#prerequisites
ngrok と、あとで使用する Severless Framework もインストールしておきます。
docker/Dockerfile
FROM lambci/lambda:build-python3.8
# python を lambda にデプロイするときには、 requirements.txt から依存モジュールのインストールをするが、
# slsでそれをするのは、plugin が必要で、その plugin が Docker を必要とする
COPY --from=docker:19.03 /usr/local/bin/docker /usr/local/bin/
# serverless framework
## serverless-python-requirements は最新の 5.1.0 では、
## deploy時にエラーが出るので、4.3.0 を使う
RUN curl -sL https://rpm.nodesource.com/setup_10.x | bash \
&& yum install -y nodejs \
&& npm install -g serverless \
&& npm install -g serverless-python-requirements@4.3.0
# ngrok
RUN curl -sSL -o ngrok.zip https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip \
&& unzip ngrok.zip \
&& mv ngrok /usr/local/bin
# このイメージは、ビルドの段階で、requirements-dev.txt を pip install します。
# `requirements*.txt ` が変更されたときには、ビルドし直してください。
COPY ./requirements*.txt ./
RUN pip install -r requirements-dev.txt
docker の起動オプションがいろいろあるので docker-compose にしておきます。
docker-compose.yml
version: '3'
services:
bot:
build:
context: .
dockerfile: docker/Dockerfile
volumes:
- '.:/opt/bot'
# aws cofigure のディレクトリをマウント
- '.aws:/opt/.aws'
# serverless-python-requirements が Docker を使うので、
# dood (Docker outside of Docker) でコンテナ内からホストのDockerを使う
- /var/run/docker.sock:/var/run/docker.sock
working_dir: '/opt/bot'
environment:
# aws cofigure の配置場所を設定
AWS_CONFIG_FILE: /opt/.aws/config
AWS_SHARED_CREDENTIALS_FILE: /opt/.aws/credentials
ports:
# ngrok 用
- 3978:3978
# ptvsd 用
- 5678:5678
command: 'sleep infinity'
ビルドとコンテナの起動
docker-compose build
# コンテナを起動
docker-compose up -d
Bot 用の Slack Application を作成する
Slackにログインして、アプリ一覧 ページを開きます。
'Create new app' ボタンをクリック
"App Name" を入力し "Development Slack Workspace" を選択
"Create App" ボタンをクリック
サンプルを動かすために必要な設定は、 Basic Information と OAuth & Permissions にあります。
Basic Information は、すでに設定されていますが、 "Display Information" のところを、任意に変更してください。
OAuth & Permissions は、"Bot Token Scopes" で "Add an OAuth Scope" ボタンをクリックして
chat:write
,im:history
,im:read
を追加してください。上の方にスクロールすると、"Install App to Workspace" がクリックできるようになっているので、クリックします。
OAuth の権限の確認画面が表示されるので、"許可"をクリックします。
Slack Application の設定は、まだやることがあるのですが、いったん、次に進みます。
環境変数を設定する
環境変数に、Slack のアプリ情報を設定します。
# Basic Information の Verification Token
export SlackVerificationToken="xxxxxxxxxxx"
# Basic Information の Signing Secret
export SlackClientSigningSecret="xxxxxxxxxxx"
# OAuth & Permissions の Bot User OAuth Access Token
export SlackBotToken="xxxxxxxxxxx"
Ngrok でテスト用のURLを準備する
Ngrok を使ったことがない場合には、下記の記事を読んでおいてください。
Dockerイメージには、ngrok のコマンドはインストール済です。 特に、Sign in しなくても、使えますが、長時間のテストをする場合には、Sign In するとよいと思います。どっちでも、無料プランでテストできます。
botコンテナに入ります。docker-compose exec bot bash
コンテナ内で ngrok を起動します。ngrok http 3978 -host-header="127.0.0.1:3978"
テスト中は、起動したままにします。
Bot を起動
上記とは異なるターミナルから、コンテナ内に入ります。docker-compose exec bot bash
botを起動します。python app.py
この時点では、WEBサーバーが起動しただけなので、まだ何も動きはないです。
Slack Application の Event Subscriptions を有効にする
サンプルの Bot は、Slack の Event API を使っているので、そのための設定を行います。
Slack api dashboard から作成した Slack Application を選択します。
Event Subscriptions を選択
Enable Events を On にする
'Request URL' に ngrok で Forwarding される https で始まる URL を設定する
ただし、ngrok の URL は、ホスト名だけなので、サンプルがルーティング実装している/api/messages
をパスに加えます。
(例)https://xxxxxxxxxx.ngrok.io/api/messages'Subscribe to bot events' を展開して、'Add Bot User Event' ボタンをクリックして、
message.im
を追加する
動かしてみる(1)
Slack のチャンネルに作成したSlackアプリを招待します。
そして、以下を試してみてください。
チャンネルにメッセージを書き込む
同じメッセージがエコーされる
添付ファイルを付けてメッセージを書き込む
メッセージのエコーと、添付ファイルを受信したことがエコーされる
メッセージにURLを含めて送信する
メッセージのエコーと、URLを受信したことがエコーされる
動かしてみる(2)
/test
コマンドもサンプルに実装されています。
Slack Application の設定にコマンドを追加します。
Slack Application にコマンドを追加設定
アプリ一覧 からアプリを選択
"Slash Commands" を選択
"Create New Command" ボタンをクリック
以下の内容を入力
- Command:
/test
- Request URL:
https://xxxxxxxxxx.ngrok.io/api/slack
(xxxxxxxxxx 部分は、ngrok の Forwarding のURLを参照) - Short Description:
テスト
- Command:
”Save” ボタンをクリック
コマンド実行
コマンドは、Slack の Workspace 全体で使われます。
- 任意のチャンネルで
/test
を送信する処理されて、ディナーのレストラン選択のメッセージが返信されます。
Lambda 用の変更
さて、Ngrok で動くことを確認したら、次には、Lambda にデプロイして、サーバーレスで動かしてみます。
LambdaのeventオブジェクトからRequestオブジェクトへの変換
Bot Builder は、WEB APIで動くことが前提になっているようで、Slack Adapter とのインターフェースで aiohttp.web_request.Request
が使われています。
Lambda の handler の引数の event オブジェクトとは、違うものなので、変換してから渡すことにします。
app.py
を修正します。
import sys
import traceback
import logging
import json
import base64
import asyncio
from datetime import datetime
from urllib.parse import parse_qs
from aiohttp.web_request import Request
from aiohttp.web_response import Response
from aiohttp import test_utils
from aiohttp.streams import StreamReader
from aiohttp.test_utils import make_mocked_request
from unittest import mock
from botbuilder.adapters.slack import SlackAdapterOptions
from botbuilder.adapters.slack import SlackAdapter
from botbuilder.adapters.slack import SlackClient
from botbuilder.core import TurnContext
from botbuilder.core.integration import aiohttp_error_middleware
from botbuilder.schema import Activity, ActivityTypes
from bots import EchoBot
from config import DefaultConfig
CONFIG = DefaultConfig()
# Create adapter.
SLACK_OPTIONS = SlackAdapterOptions(
CONFIG.SLACK_VERIFICATION_TOKEN,
CONFIG.SLACK_BOT_TOKEN,
CONFIG.SLACK_CLIENT_SIGNING_SECRET,
)
SLACK_CLIENT = SlackClient(SLACK_OPTIONS)
ADAPTER = SlackAdapter(SLACK_CLIENT)
_logger = logging.getLogger(__name__)
# Catch-all for errors.
async def on_error(context: TurnContext, error: Exception):
_logger.error(f"\n [on_turn_error] unhandled error: {error}", exc_info=True)
ADAPTER.on_turn_error = on_error
# Create the Bot
echo_bot = EchoBot()
# Listen for incoming requests on /api/messages
async def messages(req: Request) -> Response:
return await ADAPTER.process(req, echo_bot.on_turn)
# Listen for incoming slack events on /api/slack
async def slack(req: Request) -> Response:
return await ADAPTER.process(req, echo_bot.on_turn)
def handler(event, context):
if _logger.level <= logging.DEBUG:
_logger.debug(f'event={event}')
try:
result = asyncio.get_event_loop().run_until_complete(__async_handler(event, context))
return __respond(None, result)
except Exception as e:
_logger.error('エラー発生', exc_info=True)
return __respond(e)
async def __async_handler(event, context):
request = await __conv_lambda_event_to_request(event, context)
response = await messages(request)
return response
def __respond(err, res=None):
if err:
lamda_res = {
'statusCode': '400',
'body': type(err),
'headers': {
'Content-Type': 'text/plain'
},
}
else:
lamda_res = {
'statusCode': res.status,
'body': res.text,
'headers': {},
}
for k, v in res.headers.items():
lamda_res['headers'][k] = v
if _logger.level <= logging.DEBUG:
_logger.debug(f'__respond={lamda_res}')
return lamda_res
async def __conv_lambda_event_to_request(event, context):
''' lambda の event から aiohttp の Request に変換する
この変換処理は、 aiohttp のユニットテスト用のユーティリティ
test_utils を使っています。
'''
body = event.get('body', '')
if not event.get('isBase64Encoded', False):
data = body.encode()
else:
data = base64.b64decode(body)
protocol = mock.Mock(_reading_paused=False)
payload = StreamReader(protocol=protocol, limit=2 ** 16, loop=asyncio.get_event_loop())
payload.feed_data(data)
payload.feed_eof()
req = make_mocked_request(
method=event['httpMethod'],
path=event['path'],
headers=event['headers'],
payload=payload)
return req
Serverless Framework
デプロイは、Serverless Framework で行います。
serverless.yml
service: bot
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
stage: ${opt:stage, self:custom.defaultStage}
region: ${opt:stage, self:custom.defaultRegion}
# ログの保存期間
logRetentionInDays: 5
iamRoleStatements:
- Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- "arn:aws:logs:::"
environment: ${self:custom.environment.${self:provider.stage}}
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
layer: true
defaultStage: dev
defaultProfile: default
defaultRegion: ap-northeast-1
environment:
dev: ${file(./conf/dev.yml)}
prod: ${file(./conf/prod.yml)}
package:
exclude:
- .aws/**
functions:
handler:
handler: app.handler
events:
- http:
path: api/messages
method: post
cors: true
- http:
path: api/slack
method: post
cors: true
layers:
- {Ref: PythonRequirementsLambdaLayer}
環境変数は yml にします。
conf/dev.yml
# Basic Information の Verification Token
SlackVerificationToken: "xxxxxxxxxxx"
# Basic Information の Signing Secret
SlackClientSigningSecret: "xxxxxxxxxxx"
# OAuth & Permissions の Bot User OAuth Access Token
SlackBotToken: "xxxxxxxxxxx"
デプロイ
AWSのアカウントは .aws/
に作成しておいてください。
sls deploy
Slack Application の URL を API Gateway のものに変える
deploy コマンドが、作成された API Gateway の URL を出力するので、それをコピーして、 Slack Application の設定画面で、URLを置き換えます。
Slack api dashboard から作成した Slack Application を選択します。
Event Subscriptions を選択
'Request URL' に API Gateway の URL を設定する
動かしてみる
- チャンネルにメッセージを書き込む
同じメッセージがエコーされる
まとめ
Slack 起点で、いろんなことができそうな気がしてきました。
例えば
- ちょっとしたデプロイのコマンド
- メンテナンス画面への切替コマンド
- 常時起動してない ec2 の起動
- 社内 wiki の横断検索
- GitHub への Issue 投稿コマンド
- リアクションを集計して感謝ネットワークとして可視化
- 社内図書(自炊PDF)の検索
- チーム内の作業時間の回収と集計
・・・。
いろいろ試してみようと思います。
みなさんも、社内にお助け Chatbot を作ってみてはいかがでしょうか?
最後に、実際に chatbot を作ってみようと思ったら、こちらを読んでみることをオススメします。
- Microsoft Bot Framework v4 完全制覇 : 目次:
https://qiita.com/kenakamu/items/6dc043cfc1f199032883 - Bot Framework SDK のドキュメント : 公式ドキュメント:
https://docs.microsoft.com/ja-jp/azure/bot-service/index-bf-sdk?view=azure-bot-service-4.0