Python
AWS
lambda
slackbot
serverless

AWS初心者でもわかる! ブラウザ上で完結! AWS+Slack Event APIを使ったSlackボット超入門


はじめに

Slackはアメリカで開発されたチャットツールで、特にスタートアップやIT業界で利用されるようになってきたコミュニケーションツールです。弊社でも、業務内外問わずSlackの利用シーンが増えてきています。Slackでは利用者の用途に応じたボットを作成することができ、定期的なタスクの実行や、利用者の発言に応じてアクションを起こすなど、自由なカスタマイズができます。

今回は「Slackでおもしろいボットを作った」「Slackボットで業務を効率化した」といった記事をいくつか読んで興味を持ったので、Slackボットの作り方を詳しく調べ実装の流れを記事としてまとめることにしました。

この記事では、前半でSlackボットの作り方を簡単にまとめ、後半で実際に簡単なボットを作成します。AWS初心者でも記事に沿って作っていけばボットが作れる、そんな記事を目指しました。


この記事を読むことで...


  • 2通りあるインタラクティブなSlackボットの作り方とその違いがどちらもわかるようになる

  • 話しかけると挨拶をするSlackボットのコードが手元にできる(それをカスタマイズしていけば便利なボットが簡単に作れる)

  • いくつかのAWSサービスに触れて基本的な使い方がわかるようになる


前提知識


  • Pythonの基本文法を理解している

  • 本記事で利用するPythonの標準的なライブラリ(os/json/logging/random/urllib)の使い方を知ってること

  • (Optional)本記事で扱うAWSサービス(IAM・API Gateway・Lambda・CloudWatch)がそれぞれどのようなサービスか知っていること(知らなくても作れますが知っているとより理解が深まると思います)


Slackボット作成に利用できるAPI

ユーザーと相互にやり取りするインタラクティブなSlackボットを作る場合、2通りの方法があります。


  • Web APIとEvent APIを併用する方法

  • Realtime Messaging APIを利用する方法

記事の前半では、


  • Web API

  • Event API

  • Realtime Messaging API

がそれぞれどんなものなのかを簡単に説明します。


Web API

Slackが用意しているWeb APIを利用して、外部からSlackにアクションを起こすことができます。メッセージの投稿はもちろんのこと、チャンネルの管理やプライベートな会話の管理までSlackでできることは一通りこのAPIを使えば実現できます。利用できるAPIの一覧は「Methods」にまとめられているので利用する際は参考にしてください。

Web API単体では、Slackに対して一方的にアクションを起こすことしかできないため、チャンネルに投稿があったら○○するボットのようにSlack上で発生したイベントに反応するボットを作るためには、後述のEvent APIとともに利用する必要があります。


Event API

Event APIはSlack上でイベントが発生した際、指定したURLにそのイベントの内容をHTTPリクエストしてもらえる仕組みであり、このような仕組みはWebhookと呼ばれます。例えば、Slackにメッセージが投稿されると、指定したURLにメッセージの内容や投稿者の情報を含んだリクエストが飛びます。この仕組みを利用するには、SlackからのHTTPリクエストを処理するためのエンドポイントを作成しておく必要があります。

以下はWeb APIとEvent APIを併用してSlackボットを作成した場合のイメージ図です。Slack上で起きたイベントを、あらかじめ用意しておいたボット用サーバのエンドポイントにリクエストしてもらい、ボット用サーバ側で前述のWeb APIを利用してSlackにアクションを起こすことで、Slackと相互にやり取りするインタラクティブなボットを作ることができます。

event_api.png


Realtime Messaging API

一方、Realtime Messaging APIは双方向のデータ通信を可能とするプロトコルであるWebSocketを利用しています。そのため、HTTPではEvent APIとWeb APIの2つの仕組みを使わなければならなかった相互の通信を、これ単体で実現できます。以下の図はRealtime Messaging APIを利用してSlackボットを作成した場合のイメージ図です。

websocket.png

注意する点としては、Event APIとRealtime Messaging APIではSlackから受け取ることのできるイベントが異なります。こちらにそれぞれができること、できないことがまとめてあるので作成するときに自分のやりたいことはどちらで実現できるのかを確認しておきましょう。

記事の後半で実際にSlackボットを作成しますが、この記事ではWeb APIとEvent APIを使った例のみ説明します。Realtime Messaging APIを使った方法でボットを作りたい、という方のためにRealtime Messaging APIを使ったSlackボット用ライブラリをいくつか紹介しておきます。(全部Python用ですが...)


Slackボットを作ってみよう

ここからは前半で紹介した3つの仕組みのうちWeb API・Event APIを使って、自分が参加しているチャンネルにメッセージが投稿されたら、「Hello, Slack Bot!」と固定文言を返答するボットを作成します。イベントに反応して何かする、という流れさえ作ることができればあとはいくらでもカスタマイズできるようになるので、あくまでシンプルなボットを作ります。

ちなみに、Web API・Event APIを使った方法を選んだのは、AWS Lambdaを使ったサーバレスな構成で、しかもWebブラウザ上で全ての開発が完結できて簡単だったからです。Realtime Messaging APIを使った方法だとWebSocketのコネクションを管理しなければならず状態を持つことになるので、サーバレスな構成にはできません。


開発環境

今回はAWSのAPI Gateway+LambdaでSlackからのイベントを受け取るエンドポイントを作成し、エンドポイントの処理はPython 3.6を使って実装します。今回紹介するボットの作成はコーディングを含めてすべてブラウザ上で完結しますので、AWSのアカウントさえあれば前もって環境を用意する必要はありません。アカウントを持っていない方はこちらから取得しておいてください。


アーキテクチャ全体像

以下のような構成でボットを作成します。

スクリーンショット 2017-12-21 8.35.14.png


SlackからのイベントをLambdaで受け取るまで

何はともあれ、SlackからEvent API経由でイベント情報を受け取ることから全てが始まります。まずはそこまでを目指します。


Slack APIのサイト上でボット作成用のアプリを作成する

Web APIやEvent APIを利用するには、Slack APIのサイト上でアプリを作成する必要があります。

初めて作成する方はSlack APIにアクセスし、画面中央の「Start Building」から自分のアプリを作成します。既にSlack APIのサイトを利用したことがある方は右上の「Your Apps」から自分の作成したアプリの一覧を開き、「Create New App」でアプリを作成できます。

今回アプリ名は「bot-sample」としました。「Development Slack Workspace」にはボットを参加させたいワークスペースを指定します。ワークスペースにはあらかじめログインしておく必要があります。

スクリーンショット 2017-12-21 5.37.13.png

アプリ作成完了後、以下のようなアプリの基本情報ページに遷移します。

スクリーンショット 2017-12-20 6.57.03.png


Slack APIのアプリ上にボットユーザを作成する

次に、基本情報ページから主役となるボットユーザを作成しましょう。

Slack APIのサイト上でサイドメニューの「ボットユーザー」をクリックしボットユーザページに遷移した後、「ボットユーザーを追加する」ボタンを押してアプリにボットユーザーを追加します。

追加したボットユーザーには以下の画面のように名前をつけることができます。画像では「botsample」と愛情の感じられない名前になっていますが、皆さんは愛嬌のある名前をつけてあげてください。

スクリーンショット 2017-12-20 7.00.09.png


Slackイベントを受け取るエンドポイントを作成する

アプリが作成できたら、イベントを受け取る先を用意します。AWS上にSlackからのイベントを受け取るLambda関数を作成し、API Gateway経由でアクセスできるようにします。


IAMのロールを作成

まず、Lambda関数に付与するロールを作成します。後のステップでAWSコンソール上でコーディングをするのですが、その時にログを出力できるとデバッグしやすいです。なので今回はLambda関数からCloudWatchにデバッグ用のログを出力できるようにロールを設定します。

AWSコンソールにてIAMの画面を開き、サイドバーからロールのページを開きます。「ロールの作成」ボタンを押してロール作成のページに遷移します。

2017-12-20_12h13_52.png

このロールはLambda関数に付与するので、信頼関係の画面で「AWSサービス>Lambda」を選択して次のステップへ。

2017-12-20_12h13_06.png

アクセス権限の画面ではCloudWatchへのログの書き込み権限を持つ「AWSLambdaBasicExecutionRole」ポリシーを選択して次へ。(作成したいボットで他のAWS上のサービスを利用したい場合はここで権限を追加しちゃいましょう。)

2017-12-20_12h18_27.png

確認画面にてロール名をつけ、設定内容を確認してロールを作成します。今回はロール名を「botSampleRole」としました。


Lambda関数の作成

ロールを作成したら、次にAWSのコンソールからLambda関数を作成しましょう。AWSコンソールにてLambdaの画面を開き「関数の作成」ボタンから関数の作成画面へ遷移します。

2017-12-20_15h09_19.png

関数の作成画面では以下のように「一から作成」を選択して各項目を埋めます。今回はランタイムにPython 3.6を、ロールには先ほど作成したロールを指定します。

2017-12-20_12h25_00.png

関数の作成が完了すると以下のような画面に遷移します。何やらエディタのようなものが見えるかと思いますが、この画面でLambda関数のの処理内容をコーディングできます。

2017-12-20_15h31_59.png


Event APIの認証に対応できるようにコーディング

Event APIにイベント情報の通知先エンドポイントを登録する際、エンドポイントに以下の形式で認証のためのリクエストが飛びます。(値は仮です。)

{

"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}

この中のchallengeの項目の内容(トークン)をSlackに送り返してやることで認証を通過できます。要は、登録したエンドポイントは本当にEvent API用のものですか?という確認をしているんですね。ソースコードを以下に示します。いろいろインポートしているのは、この後のステップで使うものも含めているためです。

# -*- coding: utf-8 -*-

import os
import json
import logging
import urllib.request

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle_slack_event(slack_event: dict, context) -> str:

# 受け取ったイベント情報をCloud Watchログに出力
logging.info(json.dumps(slack_event))

# Event APIの認証
if "challenge" in slack_event:
return slack_event.get("challenge")

Lambdaでは2つの引数を持つ関数をプログラムのエントリポイントとして指定できます。1つ目の引数がリクエストの内容、2つ目がLambdaの設定や実行の状態を(あと何秒でタイムアウトするかなど)取得できるコンテキストになります。

コードを書き終えたら、同じ画面内のハンドラの設定をlambda_function.handle_slack_eventとします。ハンドラは{ファイル名}.{メソッド名}の形でプログラムのエントリポイントを指定する設定箇所です。最後に保存するのをお忘れなく。

スクリーンショット 2017-12-21 6.32.33.png


API Gatewayの作成

次に、作成したLambda関数を叩くためのエンドポイントを作るためにAPI Gatewayの設定をします。API Gatewayの画面を開き、新しいAPIの作成をします。適当な名前をつけてAPIを作成しましょう。今回は「botSampleAPI」としました。

スクリーンショット 2017-12-21 6.37.24.png

作成完了後の画面から、「アクション>メソッドの作成」を選択し、POSTメソッドを追加します。

スクリーンショット 2017-12-21 6.40.04.png

POSTメソッドのセットアップ画面にて、先ほど作成したLambda関数と統合するよう設定します。

スクリーンショット 2017-12-21 6.42.12.png

POSTメソッドを作成したら、「アクション>APIのデプロイ」からAPIをデプロイするとエンドポイントの完成です!デプロイボタンを押してエンドポイントが生成されると、画面上部にURLが出ているはずですので、それをコピーしておいてください。

スクリーンショット 2017-12-21 6.44.31.png


Event APIにエンドポイントを登録

長い道のりでしたが、これでやっとSlackのイベントを受け取ることができます。

Slack APIにアクセスし、最初のステップで作成したアプリを開きましょう。サイドメニューの「イベントの購読」を開いて、イベントを有効にします。

すると、URLを入力する欄が出てきますので、コピーしておいたエンドポイントのURLを貼り付けます。貼り付けた瞬間に先ほど説明したchallenge項目を含むリクエストが行われ、実装がうまくいっていれば認証をパスするはずです。

スクリーンショット 2017-12-21 6.52.24.png

同じページの下部にある「ボットのイベントに参加する」に画像のように設定します。これで、ボットユーザが参加したチャンネルにメッセージが投稿されると、Lambda関数が呼ばれるようになりました!

スクリーンショット 2017-12-21 6.58.51.png


ボットユーザをワークスペースに追加

最後にボットユーザをワークスペースに追加します。

Slack APIの自分のアプリのページのサイドメニューから「アプリをインストール」を選択。「ワークスペースへのアプリのインストール」ボタンを押下して画面の指示に従って進めていくとトークンが発行され、ワークスペースにアプリとボットユーザが追加されます。

あとはチャンネルにボットを招いて、メッセージを投稿するとLambdaにメッセージが送られます。Lambdaが呼ばれたことを確認するにはAWSコンソールからCloudWatchを開き、ログを確認しましょう。

スクリーンショット 2017-12-21 7.09.06.png

このように、pythonのコード中でlogging.infoを呼ぶことでCloudWatchにログが出力できます。もちろんwarningやerror用のメソッドもあります。


Web APIを使ってボットにメッセージを投稿させる

ここまでで必要な環境は整ったので、あとはコーディングに集中できます。次は誰かがチャンネルに投稿した時に、ボットに「Hello, Slack Bot!」と言わせてみましょう。


メッセージ投稿用の関数を作成する

投稿するメッセージと投稿先のチャンネル名またはチャンネルIDを受け取ってメッセージを投稿する関数を作成します。

def post_message_to_slack_channel(message: str, channel: str):

# Slackのchat.postMessage APIを利用して投稿する
# ヘッダーにはコンテンツタイプとボット認証トークンを付与する
url = "https://slack.com/api/chat.postMessage"
headers = {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
}
data = {
"token": os.environ["SLACK_APP_AUTH_TOKEN"],
"channel": channel,
"text": message,
"username": "Bot-Sample"
}

req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
urllib.request.urlopen(req)
return

この関数ではchat.postMessage APIを使ってSlackにメッセージを投稿しています。POSTメソッドを利用したWeb APIには、URLエンコード形式でしかリクエストボディを受け付けないものと、JSON形式のリクエストボディを利用できるものが存在します。chat.postMessage APIはJSON形式をサポートしているAPIです。JSON形式をサポートしているAPIは「Methods supporting JSON POSTs」にまとまっています。

ソースコード中ではヘッダに2つの情報を指定しています。

    headers = {

"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
}

Content-typeはURLエンコードの形式ならばapplication/x-www-form-urlencodedに、JSONでリクエストする際にはapplication/jsonにします。

また、JSON形式でのリクエストの場合のみ、ボットユーザの認証のためのトークンをBearer Tokenとしてヘッダーに追加する必要があります。認証トークンがxoxa-xxxxxxxxx-xxxxだとすれば、リクストヘッダのAuthorizationBearer xoxa-xxxxxxxxx-xxxxと指定します。(私はBearerという文字列が必要であることに気づかず少々ハマってしまいました。)上記の実装では環境変数から認証トークンを取得する実装になっていますが文字列で直接指定しても構いません。ボットユーザの認証トークンは、Slack APIのサイトのサイドバーから「OAuth & 権限」ページを開き、「ボットユーザーの OAuth アクセストークン」から取得できます。

また、リクエストボディについてボットユーザの認証トークンとは別にアプリ自体の認証トークンをtoken項目に渡す必要があります。こちらのトークンはリクエストの形式に関わらず必須です。

    data = {

"token": os.environ["SLACK_APP_AUTH_TOKEN"],
"channel": channel,
"text": message,
"username": "Bot-Sample"
}

上記の実装では環境変数から取得していますが文字列で直接指定しても構いません。アプリの認証トークンはSlack APIのサイトのサイドバーから「基本情報」ページを開き、「アプリの認証情報」の中の「認証トークン」から取得できます。


Lambda関数の環境変数にトークンをセットする

環境変数からトークンを取得する実装の場合、Lambda関数の環境変数の設定が必要になります。以下の画像の画面の環境変数欄にSLACK_APP_AUTH_TOKENSLACK_BOT_USER_ACCESS_TOKENを設定しておきます。

スクリーンショット 2017-12-21 8.10.16.png


関数を呼び出す

作成した関数を呼び出してSlackにメッセージを投稿させましょう。コードの全体は以下のようになります。

# -*- coding: utf-8 -*-

import os
import json
import logging
import urllib.request

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle_slack_event(slack_event: dict, context) -> str:

# 受け取ったイベント情報をCloud Watchログに出力
logging.info(json.dumps(slack_event))

# Event APIの認証
if "challenge" in slack_event:
return slack_event.get("challenge")

# ボットによるイベントまたはメッセージ投稿イベント以外の場合
# 反応させないためにそのままリターンする
# Slackには何かしらのレスポンスを返す必要があるのでOKと返す
# (返さない場合、失敗とみなされて同じリクエストが何度か送られてくる)
if is_bot(slack_event) or not is_message_event(slack_event):
return "OK"

# Slackにメッセージを投稿する
post_message_to_slack_channel("Hello, Slack Bot!", slack_event.get("event").get("channel"))

# メッセージの投稿とは別に、Event APIによるリクエストの結果として
# Slackに何かしらのレスポンスを返す必要があるのでOKと返す
# (返さない場合、失敗とみなされて同じリクエストが何度か送られてくる)
return "OK"

def is_bot(slack_event: dict) -> bool:
return slack_event.get("event").get("subtype") == "bot_message"

def is_message_event(slack_event: dict) -> bool:
return slack_event.get("event").get("type") == "message"

def post_message_to_slack_channel(message: str, channel: str):
# Slackのchat.postMessage APIを利用して投稿する
# ヘッダーにはコンテンツタイプとボット認証トークンを付与する
url = "https://slack.com/api/chat.postMessage"
headers = {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
}
data = {
"token": os.environ["SLACK_APP_AUTH_TOKEN"],
"channel": channel,
"text": message,
"username": "Bot-Sample"
}
req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
urllib.request.urlopen(req)
return

しれっと関数を2つ追加していますが、これらはメッセージの投稿イベントかどうかを判定する関数と、ボットユーザによるイベントかどうかを判定する関数になります。ボットユーザ自身のイベントもEvent APIによって送られてくるので、ボットユーザによるイベントの場合は反応しないようにしないと自分のイベントに自分で反応してメッセージを無限に投稿し続けてしまいます。


動作確認

こんな感じになりました!

2017-12-21 08_28_30.gif


終わりに

この記事ではSlackボットの作成に使えるAPIの解説と、Web APIとEvent APIを用いた簡単なSlackボットの作成の流れを紹介しました。今回作成したボットのように簡単なものならちょっとした空き時間で作れますし、開発環境の用意すら不要です。皆さんも隙間時間で面倒な業務を自動化するボットを作ってみたり、面白いボットを作ってチームの雰囲気を和やかにしてみてください。

最後に、今回時間がなくて記事に取り入れられませんでしたが、リクルートのProofreading APIを利用してチャンネルに投稿したメッセージの日本語チェック(誤字・脱字・不自然な表現のチェック)をするボットを作成してみました。ソースコードにはできる限り丁寧に説明コメントをつけていますのでそちらも参考にしてみてください。

誤字・脱字指摘ボット(GitHub)