SNS + Lambda + Slack でアラート通知を受け取る


[SNS + Lambda + Twilio で音声電話をかける v2](http://qiita.com/u-minor/items/d72e77e79203aab6ca70) を社内で披露していたら、「Slack にも通知が欲しいよね」とステキな提案をしてくれたメンバーがいたので、ついでに実装しました。

表示される内容は以下の様な感じになります。

7/1 追記:
CloudWatch から上がる Alarm については、State の状態で色を変えつつ JSON メッセージを解析してわかり易く表示、それ以外の通知に関してはそのまま表示しています。

![Slack.png](https://qiita-image-store.s3.amazonaws.com/0/15611/ee5157db-513c-d1d9-95a5-ece29ff7dcdf.png "Slack.png")

## コード

### package.json

Slack には [API](https://api.slack.com/) が用意されているため、自前で call してもいいのですが、npm パッケージでさくっと解決することに。
始めは Slack 純正の [node-slack-client](https://github.com/slackhq/node-slack-client) の利用を考えたのですが、一部 native build が必要なパッケージを利用していて Lambda での利用がちょっと面倒なため、Webhook と API の両方に対応している [slack-node](https://github.com/clonn/slack-node-sdk) を選択。

```json:package.json
{
  "name": "lambda-slack-alert",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment": "^2.10.3",
    "slack-node": "^0.1.3"
  },
  "devDependencies": {
    "coffee-script": "^1.9.3",
    "del": "^1.2.0",
    "gulp": "^3.9.0",
    "gulp-coffee": "^2.3.1",
    "gulp-zip": "^3.0.2",
    "run-sequence": "^1.1.1"
  }
}
```

### gulpfile

gulpfile は Twilio 版ほぼそのまま。
パッケージ化対象の node_modules の部分だけ修正。

```coffeescript:gulpfile.coffee
gulp = require 'gulp'
coffee = require 'gulp-coffee'
zip = require 'gulp-zip'
del = require 'del'
runSequence = require 'run-sequence'

gulp.task 'clean', (cb) ->
  del 'build', cb

gulp.task 'coffee', ->
  gulp.src 'src/*.coffee'
  .pipe coffee bare: true
  .pipe gulp.dest 'build'

gulp.task 'copy', ->
  gulp.src 'node_modules/@(moment|slack-node)/**'
  .pipe gulp.dest 'build/node_modules'

gulp.task 'zip', ['coffee', 'copy'], ->
  gulp.src 'build/**'
  .pipe zip 'lambda.zip'
  .pipe gulp.dest 'dist'

gulp.task 'build', ->
  runSequence 'clean', 'zip'
```

### lambda 用コード本体

`apiToken` と `channel` には、Slack 側の情報を入れます。

> 自分の場合は、Slack を無料利用していて Integration の数を増やしたくなかったので、Hubot 用の API Token を使い回しています。

メッセージの見栄えを良くするため、[Attachments](https://api.slack.com/docs/attachments) を使っています。好みに応じていろいろカスタマイズしてみてもいいでしょう。

```coffeescript:src/index.coffee
apiToken = 'YOUR_SLACK_API_TOKEN'
channel = 'YOUR_SLACK_CHANNEL'

moment = require 'moment'
Slack = require 'slack-node'
slack = new Slack apiToken

exports.handler = (event, context) ->
  try
    color = 'danger'
    title = 'アラートが発生しました。速やかにシステムの状態を確認し、対応して下さい。'

    if event.Records[0].Sns.Subject.match /^OK: /
      color = 'good'
      title = 'システムの状態が正常になりました。'

    try
      # try to detect CloudWatch alarm
      data = JSON.parse event.Records[0].Sns.Message
      payload =
        as_user: true
        channel: channel
        attachments: JSON.stringify [
          fallback: title
          text: "*#{event.Records[0].Sns.Subject}*"
          pretext: title
          color: color
          mrkdwn_in: ['pretext', 'text', 'fields']
          fields: [
              title: 'Description'
              value: "#{data.AlarmDescription}"
              short: false
            ,
              title: 'Reason'
              value: data.NewStateReason
            ,
              short: false
              title: 'Datetime'
              value: moment(data.StateChangeTime).utcOffset(9).format('YYYY-MM-DD HH:mm:ss')
              short: false
          ]
        ]
    catch e
      # other messages
      payload =
        as_user: true
        channel: channel
        attachments: JSON.stringify [
          fallback: title
          text: "*#{event.Records[0].Sns.Subject}*"
          pretext: title
          color: color
          mrkdwn_in: ['pretext', 'text', 'fields']
          fields: [
            value: event.Records[0].Sns.Message
            short: false
          ]
        ]
  catch e
    context.done null, 'Invalid event data.'
    return

  console.log 'Start message post'

  slack.api 'chat.postMessage', payload, (err, response) ->
    if err
      console.error err
      context.done null, 'Post failed.'
      return

    console.log response
    context.done null, 'Post succeeded.'

  return
```

## ビルド、function 更新

ビルドおよび Lambda の更新は、[SNS + Lambda + Twilio で音声電話をかける v2](http://qiita.com/u-minor/items/d72e77e79203aab6ca70) と同じため、省略。

## テスト実行

event データを付けずに単に `invoke` しても何も起こりません。
console で event データを用意してもいいのですが面倒なので、AWS Management Console から Lambda の Edit/Test に行き、Sample event で SNS を選択し、適当にメッセージをカスタマイズしてテストしておきます。

![AWS_Lambda.png](https://qiita-image-store.s3.amazonaws.com/0/15611/a8946015-c40b-12a7-a749-039c369def5b.png "AWS_Lambda.png")

## Amazon SNS との連携

作成された function の ARN をコピーし、SNS の「Create Subscription」で AWS Lambda を選択、コピーした ARN を貼り付けて登録します。

詳細な手順は、[ここ](http://qiita.com/u-minor/items/3eb01783b818273080c2#amazon-sns-%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA) を参照してください。

## 終わりに

Amazon SNS に通知系を集約すれば、AWS Lambda を用いて様々な Output が可能になるので、非常に便利です。
メール通知だけでなく、Twilio での電話通知や Slack への Post 等、少人数で効率的に回す仕掛けが安価且つ簡単に作れるのは非常にありがたいですね。

AWS の利便性にどっぷり浸かってしまい、そう簡単には抜けられそうにありません。。

--------
- 7/1
  - CloudWatch の通知の場合にメッセージ内容と色を変更するよう修正しました