AWS Lambda を使って Slack ボット (命名: Lambot [ランボー]) を低予算で作ろうじゃないか

## はじめに

Slack用ボットの定番は Heroku+Hubot だと思いますが、

 - もっと簡単、シンプルに
 - よりElastic (負荷の増減に柔軟)に
 - かつ、低予算 (サーバーレス) で

運用したいので、AWS Lambda 上に、ライブラリ(Hubot)を使わないで構築します。


システム構成は以下のようになります。

![スクリーンショット 2015-11-01 12.44.16.png](https://qiita-image-store.s3.amazonaws.com/0/69410/a1568c53-ec8c-3401-b742-865c3e560c4c.png)


Slack→Lambda連携では、Content-Type について

 - Slack「Outgoing WebHooks」出力は、 `application/x-www-form-urlencoded`
 - AWS「Lambda」入力は、`application/json`

なので、API Gateway での Content-Typeの変換処理がポイントになります。 


以下、順番に作成していきます。



## AWS側の設定

最初はLambda関数から。

### Step 1: Select blueprint
 - 「microservice-http-endpoint」を選択。(これはらくちんだ!)

![スクリーンショット_2015-11-01_13_17_18.png](https://qiita-image-store.s3.amazonaws.com/0/69410/6e959d59-1bfe-9754-f3c3-997e41bb2543.png)

### Step 2: Configure function
 - Name ですが「Lambot(ランボー)」と名づけました。(Lambda+Bot)

![スクリーンショット_2015-11-01_13_24_49.png](https://qiita-image-store.s3.amazonaws.com/0/69410/38ee609b-4a24-4f07-ae9f-31afcedab3cb.png)

 - Lambda関数は、「Edit code inline」で以下を記述。(→参考 1)

```js
exports.handler = function(event, context) {

  var text = [
    event.token,
    event.team_id,
    event.team_domain,
    event.channel_id,
    event.channel_name,
    event.timestamp,
    event.user_id,
    event.user_name,
    event.trigger_word
  ].join("\n");

  text = ["```" + text + "```", event.text].join("\n");

  context.done(null, {text: text});

};
```
 - Role は「Basic execution role」で。
 - Memory は最小(128MB) でよい。(→参考 2)

![スクリーンショット 2015-11-01 13.24.02.png](https://qiita-image-store.s3.amazonaws.com/0/69410/2afd300f-cc56-2409-0ee2-4560ded9f6ab.png)


### Step 3: Configure endpoints
 - API Name は「Lambot」
 - Method は「POST」
 - Security は「Open」

![スクリーンショット 2015-11-01 14.21.57.png](https://qiita-image-store.s3.amazonaws.com/0/69410/62c5ca4f-4bdf-9c23-902b-9cdb41d0a25f.png)

Lamba 関数が作成できたら、以下の画面になります。
「API endpoint URL」をメモしておいて下さい。(後でSlack側に設定します。)
「POST」をクリックして、API Gateway に移動します。
![スクリーンショット_2015-11-01_15_12_19.png](https://qiita-image-store.s3.amazonaws.com/0/69410/dd7a1605-dd9c-61aa-a8c0-e471cf242c21.png)


以下の API Gateway に移動したら、「Integration Request」をクリック。

![スクリーンショット_2015-11-01_1_38_36.png](https://qiita-image-store.s3.amazonaws.com/0/69410/af95509a-f658-e10f-35fd-9e79f8f2898c.png)

さらに、「Mapping Template」をクリック。

![スクリーンショット_2015-11-01_14_33_31.png](https://qiita-image-store.s3.amazonaws.com/0/69410/ea2a9fee-bb1e-c308-0b93-f8e32ef227ab.png)

ここが一番分かりづらいですが、要は「application/x-www-form-urlencoded」の時は json 型に変換するマッピングを定義します。
1.「Add mapping template」をクリック
2.「application/x-www-form-urlencoded」を入力して、すぐ右にあるチェックをクリック
3. 保存されたら、「application/x-www-form-urlencoded」をクリック
4.「Input passthrough」の右の鉛筆をクリック。
5.「Input passthrough」を「Mapping template」に変更。
6. 「Template」に以下を記載。
7. 保存されていることを確認。

![スクリーンショット_2015-11-01_14_37_01.png](https://qiita-image-store.s3.amazonaws.com/0/69410/fb565244-be8d-5202-5572-5376bb77b829.png)

Templateコード (→参考 3)

```
## convert HTML POST data or HTTP GET query string to JSON
 
## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path('$'))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end
 
## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())
 
## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end
 
## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))
 
## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])
 
## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end
 
## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}
```
最後に、「Deploy API」をクリックして、ステージ「prod」をデプロイしてください。
![スクリーンショット_2015-11-01_15_06_11.png](https://qiita-image-store.s3.amazonaws.com/0/69410/c514904f-3b35-c288-376b-4225ca5b3ae0.png)
![スクリーンショット_2015-11-01_15_07_41.png](https://qiita-image-store.s3.amazonaws.com/0/69410/fd3c20e6-f6ea-7d70-6cf4-1172fd39462f.png)

これで AWS 側は完成。

## Slack側の設定

### Step 1: Outgoing WebHooks

チームのIntegrations 設定で、「Outgoing WebHooks」を選択してください。

![スクリーンショット_2015-11-01_15_18_54.png](https://qiita-image-store.s3.amazonaws.com/0/69410/dc0b8f7f-6756-e7a9-0be6-e365011f5418.png)


以下は、「Outgoing WebHook」の設定になります。
 - Channelを「Any」にしたい場合は、「Trigger Word」を指定する必要があります。
 - 今回は全ての発言をトリガーしたいので、「#general」チャンネルに限定して、「Trigger Word」は「(なし)」にしています。

![スクリーンショット_2015-11-01_15_24_17.png](https://qiita-image-store.s3.amazonaws.com/0/69410/8b6631a2-767c-97e3-3ec1-c704677adc9f.png)

URLは、先ほどメモした API Gateway のリソース URL を記述してください。
![スクリーンショット_2015-11-01_15_24_55.png](https://qiita-image-store.s3.amazonaws.com/0/69410/c9fb6ae1-aa9f-5785-880c-e3e3d03897c4.png)
Name と Icon は、お好みで。
![スクリーンショット_2015-11-01_15_25_53.png](https://qiita-image-store.s3.amazonaws.com/0/69410/477151b2-ae07-024d-414b-ac51fd5388c7.png)

「Save Settings」を押下すれば、Slack側の設定は完成です。

## 確認
Slack画面で発言してみます。おぉ、Lambotから返事がきました。
![スクリーンショット_2015-11-01_15_35_40.png](https://qiita-image-store.s3.amazonaws.com/0/69410/9517f259-37d2-39b0-fde1-7aeb6ded7994.png)



## 今後の展開

 - Lambda関数の内部を充実させて、何かコマンドが実行できるようにします。
 - LambdaはSQSをトリガーにできない(2015/11/01)です。もしできるようになれば、非同期処理的なコマンドも処理できるようになるでしょう。

## 参考

### 参考 1
 - Lambda関数は、今回はインライン編集ですませましたが、npmモジュールが必要な場合は package.json と node_modules が含まれたコード一式をzipで固めてアップロードします。




### 参考 2 (AWS Lambda 費用)

リクエストのうち毎月最初の 1,000,000 件は無料

メモリ128MB(最小)の場合

|1 か月の無料利用枠の秒数|100 ミリ秒単位の価格 (USD)|
|----|-----|
|3,200,000|0.000000208|

費用について詳しくは以下を参照
https://aws.amazon.com/jp/lambda/pricing/

### 参考 3 (x-www-form-urlencoded を Lambda で処理するには)
Special Thanks!!!!
 - https://forums.aws.amazon.com/thread.jspa?messageID=673012&tstart=0#673012