AWS LambdaでサーバレスなSlackボットをつくったよー(Scheduled Event対応・npmモジュールあり)

## TL;DR;

AWS LambdaとScheduled Eventを利用した、サーバレスのSlackボットをつくることができるnpmモジュールと、
それを使ったボットの作り方を紹介します。
hubotの代替を意図しています。できるだけサーバ保守などの手間をかけず、安価にSlackボットを運用したい方におすすめです。
本質はLambdaにデプロイされる単一のNode.jsアプリなので、多くの人がhubotでやってることはだいたいできると思います。

GitHub
https://github.com/mumoshu/lambda_bot

npm
https://www.npmjs.com/package/lambda_bot

## 背景

社内向けのSlackボットをいくつか運用しています。
現状のボットはhubotベースで、hubotの実行環境の整備や保守はEC2インスタンス上の温かみのある手作業で行っています。結果的に、ボットの開発環境をつくるのが面倒（他のエンジニアが開発に参加しづらい）、早朝・深夜はほとんど使われない割に、止めて良いわけでもないので必要以上にEC2インスタンスの利用料金がかかっている、などの問題が発生しています。選択肢はいくつかあると思いますが、例えばAWS Lambdaベースで作り直すことができればこれらの問題が解決しそうです。

## ボットの主な振る舞いと実装

弊社のSlackボットの主な振る舞いは、

* 誰かの発言に応答する
    * hubot、Slack Incoming WebHooks APIを利用するWebアプリなどで実装
* ある日時に特定の発言をする
    * hubot+hubot-cron、cron+bash+Slack APIなどで実装

後者については、[UTC(Unreliable Town Clock)](https://alestic.com/2015/05/aws-lambda-recurring-schedule/)とLambdaを組み合わせればできそうでしたが、ボットとはいえUnreliableなものに頼るのはいかがなものか…ということで、結局先送りにしていました。

## つくってみた

[先日参加したAWS re:Invent 2015](http://engineer.crowdworks.jp/2015/10/14/reinvent-2015.html)でAWS LambdaのScheduled Eventが発表されて、UTCを使わなくてもサーバレスで弊社のSlackボットの主な振る舞いを実装できそうな環境が揃ったので、早速つくってnpmモジュールとして公開しました。このnpmモジュールを利用すると、Lambdaで動くSlackボットをつくることができます。

GitHub
https://github.com/mumoshu/lambda_bot

npm
https://www.npmjs.com/package/lambda_bot

> ※
> 開催期間中にAPI GatewayがTokyoリージョンでも使えるようになったので、Oregon等のAPI GatewayとLambdaを使ったり、Oregon等のAPI GatewayとTokyoのLambdaを繋いだりしなくて良くなったのも怠惰な自分的には後押しになりましたが、それはまた別の話

## lambda_botの構成

lambda_botは以下の内容を含むzipファイルとして、AWS Lambdaにデプロイされます。

1. AWS LambdaのHandlerを定義するjsファイル
2. 上記が必要とする環境変数を定義する.envファイル
3. 1が必要とするnodeモジュール(./node_modules/以下)

1はどのように振る舞うbotかを定義するファイルそのものなので、bot毎に作る必要があります。
2はどのSlackチャンネルで動く、どんな名前のボットかを決めるものなので、やはりbot毎に作る必要があります。
3は1から利用するnpmモジュールをいつも通り `npm install --save` しておけばOKです。

## lambda_botのつくり方

大まかには以下の流れでつくることができます。

* ボット自体のnpmモジュールを作って、
* ボットをjsで記述して、
* ボットが利用する環境情報（しかもGit等で管理したくないもの）を.envファイルに記述して、
* lambda_botのCLIでLambdaへデプロイして、
* API GatewayとSlack Integrationを温かみのある手作業でセットアップ

### npmモジュールをつくる

```
$ mkdir my_lambda_bot
$ cd my_lambda_bot
$ npm init
*省略*
$ npm install --save lambda_bot node-env-file
```

### コードを書く

lambda_botのnpmモジュールにコード例があるので、それをコピーして始めます。

```
$ cp node_modules/lambda_bot/example_bot.js my_lambda_bot.js
```

lambda_botの実体はLambdaで動作するNode.jsアプリなので、適切な `exports.handler` を定義する必要があります。
lambda_botはSlackボットとして振る舞う `exports.handler` を簡単に定義するためのライブラリ、と思っていただくと良いかもしれません。
lambda_botを使うと、`exports.handler` を以下のように定義できます。

```js:my_lambda_bot.js
var LambdaBot = require('lambda_bot'),
    env = require('node-env-file');

env(__dirname + '/.env');

var bot = new LambdaBot({
    iconEmoji: process.env['SLACK_ICON_EMOJI'],
    userName: process.env['SLACK_USER_NAME'],
    channelName: process.env['SLACK_CHANNEL_NAME'],
    slackIncomingWebhookURL: process.env['SLACK_INCOMING_WEBHOOK_URL']
});

// SLACK_CHANNEL_NAME環境変数で指定したチャンネルで foo を含む発言を見かけると、bar と発言します
// hubotのhearやsendと同じ意味ですが、hubotとは違いreturnが必須なので注意。
// res.sendはメッセージを送り終わるとfulfilされるPromiseを返します。
bot.hear(/foo/, function(res) {
    return res.send('bar');
});

// 「名前 hi」のように名前をつけて「hi」と呼びかけると、呼びかけたユーザに対して「@ユーザ名 hi」のように返答します
// 名前はSLACK_USER_NAME環境変数で指定したものです。
bot.respond(/hi/, function(res) {
    return res.reply('hi');
});

// 「午前10時に「It's 10am.」と発言します。
// Lambda > Event Sources > Scheduled Event > cronで下記のようなScheduled Eventを作成しておく必要があります。
bot.on('10am', function(res) {
    return res.send("It's 10am.");
});

exports.handler = bot.createHandler();
```

![image](https://qiita-image-store.s3.amazonaws.com/0/5912/d3fffced-8cc4-1dc4-6ce0-9ae448b55985.png)

### SlackのIncoming Webhook

lambda_botからSlackへメッセージを送るために使うSlackのIncoming WebHookを新規作成します。

Webhook URLは後に作成する.envファイルに記述する必要があるので、控えておきます。

![image](https://qiita-image-store.s3.amazonaws.com/0/5912/e622ffd7-5801-9b11-316c-69238be88ca5.png)

### .envファイルを書く

Slackのチャンネル名やIncoming WebHookのURLなど、Git等にコミットしたくないものは `.env` ファイルに書きます。このファイルに書く情報を共有したいときは、S3バケット等にServer-side Encryptionを利用して置いておくとよいのではないかと思いますが、それはまた別の話。

```
$ cat > .env
SLACK_INCOMING_WEBHOOK_URL='https://hooks.slack.com/services/***/***/***'
SLACK_CHANNEL_NAME='#を含まないチャンネル名'
SLACK_USER_NAME='@を含まないユーザ名'
SLACK_ICON_EMOJI=':beer:'
```

### AWS Lambdaへlambda_botをデプロイする

```
$ SLACK_CHANNEL_NAME='デプロイ完了メッセージを送るSlackチャンネル` \
  SLACK_USER_NAME='デプロイ完了メッセージの送信要求をボットへ送るSlackユーザ` \
  LAMBDA_FUNCTION_NAME='lambda_bot等' \
  LAMBDA_HANDLER='my_lambda_bot.handler' \
  node_modules/lambda_bot/bin/lambda_bot deploy
```

`my_lambda_bot.handler` の `my_lambda_bot` 部分は、今回つくるボットのjsファイル `my_lambda_bot.js` のベースネーム部分に対応しています（対応していないと、Lambda Functionの呼び出し時にエラーになります）。

### API Gatewayの設定

API Gatewayで任意の名前のAPIを作成します。
APIからLambda Functionへのつなぎ方は自由なのですが、例えば `/` リソースの `POST` メソッドをLambda Functionにつなぎます。

Integration Requestで先ほどデプロイしたLambda Function名（今回はlambda_bot）を指定します。
「API Gatewayに以下のLambda Functionの呼び出し権限を与えますがよろしいですか？」という趣旨のダイアログが表示されるので、OKで権限を与えます。

![image](https://qiita-image-store.s3.amazonaws.com/0/5912/128ebb23-de87-ae9d-d82d-81e172690cde.png)

Integration RequestのMapping Templateには、AWSフォーラムの以下のスレッドで紹介されているものを使います。このMapping Templateが必要な理由は、2015/11/6現在、API GatewayとLambdaをそのまま繋ぐだけでは、`Content-Type`が`application/x-www-formurlencoded`なリクエストメッセージの内容をJSON形式でLambdaに渡すことができないからです。

API、リソース、メソッドの作成が終わったら、適当なステージにデプロイしておきます。今回はproductionというステージにデプロイします。

### SlackのOutgoing Webhookを作成

lambda_botがSlackメッセージを受信するために使うOutgoing WebHookを新規作成します。

「URL(s)」欄には、上記でデプロイしたAPI GatewayのステージのURLを入力しておきます。
今回はAPI Gatewayのproductionというステージにデプロイしたので、URL(s)欄に入力するURLのパス部分にもproductionという文字列があるはずです。

![image](https://qiita-image-store.s3.amazonaws.com/0/5912/86f3e412-0bb6-6087-a49e-51b36677ad45.png)

### 動作確認

指定したSlackチャンネルで以下のように振る舞うはずです。

* `foo` と発言すると `bar` と返ってくる
* `lambda_bot hi` などと発言すると `@あなた hi` と返ってくる
* 月-金の10時になると(前述のLambda Scheduled Eventの設定内容による) ボットが `It's 10am.` と発言する

### まとめ

AWS LambdaとScheduled Eventを利用したサーバレスのSlackボットをつくることができるnpmモジュールと、
それを使ったボットの作り方を紹介しました。
できるだけサーバ保守などの手間をかけず、安価にSlackボットを運用したい方は、ぜひ使ってみてください。
プルリクエストお待ちしています :smile: