SNS + Lambda + Twilio で音声電話をかける v2


[アラート発生時に AWS Lambda を使って音声電話をかける](http://qiita.com/u-minor/items/3eb01783b818273080c2) では単に電話をかけるだけでしたが、

- 複数人に同時に電話をかけたい
  - 1人だと、その人が電話に出なかったら終了なので
- 夜から朝方までの時間帯で電話を鳴らしたい
  - 日中はアラートメールで十分

という変更をしたくなってきたので、いろいろ改変。

npm パッケージを使うことになったので、JSベタ貼りではなくきちんとzipでアップロードすることになります。
となると、いろいろ面倒になってくるので、gulp 対応、CoffeeScript 化もついでに行います。

## コード

### package.json

使う npm パッケージは以下の通り。

- [moment-timezone](http://momentjs.com/timezone/): 有名な日時ライブラリの timezone 対応版。UTC-JST 変換、時刻判定に。
- [q](http://documentup.com/kriskowal/q/): 有名な Deferred-Promise ライブラリ。
- [twilio](http://twilio.github.io/twilio-node/): せっかく npm 使うので、直接 REST API を叩くのではなく、サクッとライブラリで。

以下、dev 用。

- coffee-script: gulpfile を coffee で書くために。
- del: clean task でのファイル削除用。
- gulp: gulp 本体。
- gulp-coffee: coffee compile 用。
- gulp-zip: zip 圧縮用。
- run-sequence: gulp task の順序制御用。

```json:package.json
{
  "name": "lambda-twilio-alert",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment-timezone": "^0.4.0",
    "q": "^1.4.1",
    "twilio": "^2.2.1"
  },
  "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 も適当にサクッと作成。

```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-timezone|q|twilio)/**'
  .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 用コード本体

本体はこんな感じで。
`toNumbers` には、配列で電話番号を列挙します。

TwiML については S3 Bucket 等の HTTP アクセス可能な場所に設置してもいいのですが、Twilio Labs に [Twimlets](https://www.twilio.com/labs/twimlets) なるものがあり、これの [Echo](https://www.twilio.com/labs/twimlets/echo) を使うと QueryString で渡したデータをそのまま XML としてエコーバックしてくれるので、こちらを使っています。
Twimlets を使うことで、任意のメッセージを動的に喋らせることも簡単になりますね。

また、moment-timezone で Asia/Tokyo に変換、10時-19時の間はスキップするようにしています。

[twilio-node](http://twilio.github.io/twilio-node/) は Q の Promise を返す仕組みを持っていますので、複数の makeCall の結果を Promise オブジェクトの配列で持ち、Q.allSettled で処理しています。

```coffeescript:src/index.coffee
accountSid = 'YOUR_TWILIO_ACCOUNT_SID'
authToken = 'YOUR_TWILIO_AUTH_TOKEN'
fromNumber = '+81XXXXXXXXXX'
toNumbers = [
  '+81XXXXXXXXXX'
  '+81XXXXXXXXXX'
  '+81XXXXXXXXXX'
  '+81XXXXXXXXXX'
  '+81XXXXXXXXXX'
]
twiML = '''
<Response>
<Say language="ja-JP" voice="alice">サービス名 アラート通知です。</Say>
<Pause length="1" />
<Say language="ja-JP" voice="alice">障害が発生した可能性がありますので、アラートメール及びシステムの状態を確認して下さい。</Say>
</Response>
'''

https = require 'https'
queryString = require 'querystring'
moment = require 'moment-timezone'
Q = require 'q'
twilio = require 'twilio'
client = new twilio.RestClient accountSid, authToken

exports.handler = (event, context) ->
  try
    # Skip CloudWatch recovery notification
    if event.Records[0].Sns.Subject.match /^OK: /
      context.done null, 'Skip notification.'
      return
  catch e
    context.done null, 'Invalid event data.'
    return

  d = moment().tz('Asia/Tokyo')
  day = d.day()
  hour = d.hour()
  if day >= 1 and day <= 5 and hour >= 10 and hour <= 18
    # 平日（月〜金）の10時〜19時は通知を抑制する
    context.done null, 'Skip notification.'
    return

  console.log 'Start twilio call'
  promises = []
  for to in toNumbers
    promises.push client.makeCall
      from: fromNumber
      to: to
      url: "http://twimlets.com/echo?Twiml=#{queryString.escape twiML}"

  Q.allSettled(promises).then ->
    args = [].slice.apply arguments
    for arg in args
      if arg.state is 'rejected'
        context.done null, "Call failed! Reason: #{arg.reason}"
        return
    context.done null, 'Call succeeded.'
  return
```

## ビルド、更新

`gulp build` すると、dist 以下に lambda.zip が生成されます。

あとは、AWS CLI を使って update を行います。

```console
aws --region ap-northeast-1 lambda update-function-code --function-name YOUR_FUNCTION_NAME --zip-file fileb://dist/lambda.zip
```

## テスト実行

AWS CLI の invoke でテストができます。Management Console から実行しても OK です。

```console
aws --region ap-northeast-1 lambda invoke --function-name YOUR_FUNCTION_NAME lambda.log
```

## 追加予定のアイデア

- SNS のメッセージ内容（重要度）によって電話のかけ先を変える
- TwiML のメッセージ内容を具体的な障害内容にする
- 休日カレンダーを考慮した業務時間の判定
  - このためだけに Google API 対応するのはちょっと面倒。。

--------
- 6/26 修正
  - 土日はアラートメールを見過ごす可能性が高いので、時間に関係なく電話をかけるように修正

- 7/1 修正
  - AWS Lambda が東京リージョンで利用可能になったため、コマンドの region を修正
  - CloudWatch の recovery 通知は無視するよう修正