ChatOpsのススメ
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
pylintDockerイメージ
Lambdaで実行するので、 Lambda の docker イメージを使います。
BotBuilder-python が Python 3.8 前提なので、Dockerイメージも Python3.8 にします。
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.txtdocker の起動オプションがいろいろあるので 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 -dBot 用の 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を受信したことがエコーされる メッセージのエコーと、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 reqServerless 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 deploySlack 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
