LambdaのエラーをSlackBotで受け取る

チャットボット Advent Calendar 2016 の 17日目です。
遅くなってすみません！！

#昨日はBotkitの基礎+応用の手引！
http://qiita.com/okajax/items/071d75aa6d2927fd7a8e
１６日目はokajaxさんによる、Botkitの使い方指南でしたね。
ボタン一つでherokuのサンプルを動かせるのは、凄いですね！

Botkitはプログラムベースでゴリゴリと対話を書いていけるのがいいですね。
うちの社内のBotアーキテクチャは別データベースのシナリオスクリプトを読み込んで動かす遷移型なので、
現在の会話にあった回答しか受け入れられないのが悩みどころで、
Botkitの設計は参考になりますね！

#挨拶・自己紹介
アレク八幡と申します。
スマホアプリの開発がメインのフリーランスみたいな１人社長エンジニアです。
最近ハマっているのはAWSのLambdaを中心としたサーバーレスアーキテクチャを使ったLINEボット開発です。
非エンジニアでもBotを使えるようになるサービスを提携先といくつか作ってリリースしています。
http://www.injus.co.jp/bot/

自分で言うのも何ですが、大学中退の落ちこぼれ君で、
プログラムも動けばいいや主義がかなり強くて、バグ生産性が高いので、
こうやって時々記事を書くことで勉強したり反省したりしています。

#Amazon Lexについて書くはずが
2016年11月28日から12月2日の5日間にラスベガスで開催された「AWS re:Invent 2016」で、
Amazon Lexが発表されましたね。
https://aws.amazon.com/jp/lex/

うちのBotに組み込んだら、Botちゃんが物凄く賢くなったり、今後開発が楽になるかも・・・・とワクワクし、
さっそくPreviewに申込んだのですが、残念ながら今日まで音沙汰なく・・・・。
AmazonからPreview待機リストに追加したよと連絡はあったので、
会社規模などの理由から後回しにされているだけだろうと思います。
（Amazon Lexに関する記事もまだあまり見つからないし、Preview自体まだ動いてない？？）

#本題：LambdaのエラーをSlackBotで受け取る

ということで、代わりにBotっぽいことということで、AWS LambdaやAWSでエラーなどが発生した時に、
Slackのチャンネルにメッセージを表示するBotについて書こうと思います。

#利用シーン
弊社のようにサーバーレスアーキテクチャを採用して、AWSに依存しまくっていると、
大量のLambda関数を作成して運用していくことになると思います。
エラーハンドリングの方法は各社様々だと思いますが、AWSはログがCloud Watch Logsに残るのでついついサボりがちですね。
ただ、放って置いたらいつの間にか色んなエラーがプロダクト環境で起きていた、それに数日気付いていなかった！ということが起きる危険性はあります。
そんな不安を紛らわせるために、エラーっぽいログがLambdaからCloudWatchLogsに流れたら、
即座にSLACKのチャンネルにエラーの内容と詳細ログへのリンクが投稿されるようにしましょう！

#前準備1 Slackに「Incoming WebHooks」を追加
[Slack API (Incoming Webhooks) が簡単すぎた](http://docs.hatenablog.jp/entry/slack-Incoming-webhooks)
ここを参考に、自分のチームにIncoming WebHooksのBotを追加しましょう。
残念ながらBot的なことはすべてこのBotがやってくれるので、この記事で紹介するのはその先だけです。

#前準備2 運用しているLambdaの関数を用意
Lambdaじゃなくてもいいですが、要はCloudWatchLogsにエラーが来たらBotで通知するってことなので、
CloudWatchLogsにログが流れなければ全く意味がないですね。
Lambdaを使ったことすら無い人は、この先はLambdaを使い始めてから来て下さい。

#リポジトリ
ソースコードはこちら
https://bitbucket.org/srare/lambda_error_log_to_slack

#使い方
Linux, nodeJS, npm, git, pythond2.6, pip, aws-cli辺りが必要です。
この辺りはAWSを使用するためにインストールされているという前提で行きます。

必要な場合は以下URLを参考に
[AWS CLI のインストールと設定](http://docs.aws.amazon.com/ja_jp/streams/latest/dev/kinesis-tutorial-cli-installation.html)

## リポジトリの取得と初期設定
リポジトリにおいてあるシェルスクリプトは引数なしでも適当な名前で動くようになっています。
試しに使うなら引数無しで、実運用するなら名前を指定して設定して下さい。

```zsh:とりあえず使ってみたい人向け。コピペで動くはず。slack-webhook-urlだけ、IncomingWehHooksのURLを設定して下さい
git clone git@bitbucket.org:srare/lambda_error_log_to_slack.git
cd lambda_error_log_to_slack
npm install
./create_role.sh
./create_lambda_func.sh --slack-webhook-url=https://hooks.slack.com/services/xxxxx
```

### Lambda実行用ロールの作成（すでにある場合は必要ない）

```zsh:ロールの作成
#引数無しで「lambda_error_log_to_slack_exec_role」というロールが作成される。
#もしロール名を指定したい場合は「--role-name=ロール名」を引数に追加する
./create_role.sh --role-name=test_role
```

### Lambda関数の作成とソースのアップロード

```zsh:関数作成、アップロード
# slack-webhook-urlだけは必須。ないとSLACKに通知が来ないよ
./create_lambda_func.sh --function-name=lambda_error_log --region=ap-northeast-1 --timezone=Asia/Tokyo --role-name=test_role --slack-webhook-url=
```
## トリガーを設定

エラーログを監視したい対象のLambda関数のTriggerを設定します。
1. [AWS CloudWatch Logs](https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logs:)のページを開きます
2. 監視したいログにチェックボックスを付けて「アクション」ボタンから「Lambdaサービスへのストリーミング開始」を選びます。
3. 今回作成したLambda関数を選択して、画面右下の「次へ」を押します
4. ログの形式は（正直良く分かってない。誰かフォローして欲しい）、とりあえず「その他」にしておきます。（注：但し、ここでLambdaを呼ぶログをフィルタリングしたほうが、Lambdaの呼ばれる数が減るので、無駄な料金がかからなくなります）
5. 「ストリーミングの開始」を押して、完了

## ソースコード

```js:lambda_error_log_to_slack/index.js
'use strict'; 

const Promise = require('bluebird');
const request = require('request-promise');
const zlib = Promise.promisifyAll(require('zlib'));
const slack_url = process.env.SLACK_WEBHOOK_URL;
const region = process.env.REGION || process.env.AWS_REGION; // AWS_REGIONは予約済み環境変数

exports.handler = function(event, context, callback) {
  // Promiseで非同期処理をしますよ
  Promise.coroutine(function*(){
    // awslogs.dataのデータは圧縮して送信されるので解凍する必要がある
    var payload = new Buffer(event.awslogs.data, 'base64');
    let resultData = yield zlib.gunzipAsync(payload);
    let result = resultData.toString('utf8');
    
    var skip = true;
    if (result.toLowerCase().indexOf("error") != -1) {
      skip = false;
    }
    if (result.indexOf("Task timed out") != -1) {
      skip = false;
    }
    if (skip) {
      callback(null, "skip");
      return;
    }
    let resultJson = JSON.parse(result);
    
    let date = new Date(resultJson.logEvents[0].timestamp);
    let datetime = `${date.getFullYear()}/${date.getMonth()+1}/${date.getDate()} ${date.toTimeString()}`;
    let function_name = resultJson.logGroup.split("/").pop();
    let group = resultJson.logGroup;
    let stream = resultJson.logStream;

    // CloudWatchLogsでログのページを開く。細かくログの行を指定することはできないのでページを開いたあとは目grepしてください
    let log_url = `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logEventViewer:group=${group};stream=${stream}`;
    var log_messages = [];
    for (var i=0; i<resultJson.logEvents.length; i++) {
      if (resultJson.logEvents[i].message.indexOf("RequestId: ") != -1) {
        continue;
      }
      log_messages.push(resultJson.logEvents[i].message);
    }
    // メッセージの内容を変えたい場合はこの行を変更して下さい
    let message = `:warning::warning::warning:Warning!!:warning::warning::warning:\n:clock2:${datetime}\n:computer:${function_name}\n:page_facing_up:${log_url}\n\n>>>${log_messages.join("\n")}`;
    console.log("message : " + message);
    yield request({
      method: 'POST',
      uri: slack_url,
      headers: {
          "Content-Type" : 'application/json',
      },
      body: JSON.stringify({
          text : message,
      }),
    });
    callback(null, "success");
  })().catch((e) => {
    if (e) {
      callback(e);
    }
  });
};
```

#感想
いつも、とりあえず動けばいいやで作っているものを、
共有するために書き直したり、調べなおしたり、
手順をシェルスクリプトにする作業は骨が折れますね。

最終的に、ロールの作成やLambdaの作成を引数付きのシェルスクリプトで行えるようになったことが、
今回の記事を書いた中で一番の成果だった気がします。
リポジトリに入ってるので、もし良かったら似た用途で使ってみて下さい。

疲れた・・・・。仕事しなきゃ・・・・。

#明日は出勤Bot！
チャットボット Advent Calendar、明日は「出勤したらLINE/Slackに通知してくれるBot」です！
既に日にちは過ぎていますが、ぼくも数日遅れての記事公開でしたが、なんとか頑張って書いたので、
shoyaさん、がんばってください！応援しています！