CloudWatchのアラームをLambdaでチャットワークに通知 w/ Serverless

AWS Lambda & Serverless Frameworkの簡単なお試し。

CloudWatchのアラームの通知先をSNSトピックに設定し、それをLambdaのトリガーにしてChatworkに通知する、というのをやってみる。

[Serverless - The Serverless Application Framework powered by AWS Lambda and API Gateway](https://serverless.com/)

## 環境
- serverless 1.3.0

## 準備
作業用ディレクトリの作成と、ライブラリのインストールなど。

```console
$ mkdir hello-serverless
$ cd hello-serverless
$ npm init
$ npm install -D serverless
```

`package.json`に、Serverlessのプロジェクト（テンプレート）作成用の`npm run`タスクを追加。

```json:package.json
{
  "scripts": {
    "create-template": "sls create --template"
  },
  ...
}
```

Lambdaで動かす言語として、今回はNode (v4.3) を選択。

```console
$ npm run create-template -- aws-nodejs

> hello-serverless@1.0.0 create-template /Users/Aono/works/hello-serverless
> sls create --template aws-nodejs

Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.3.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name
```

作業ディレクトリを確認してみると、`handler.js`, `serverless.yml`が作成されている。前者が実際にLambdaファンクションとして動くロジックを書くファイル、後者がServerless Frameworkの動作に関わる設定ファイルとなる。

↓デフォルトで作成される`handler.js`。

```js:handler.js
'use strict';

module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};
```

### Lambdaファンクションのテストデプロイ
とりあえず上記のデフォルト関数をAWS側にデプロイ(`sls deploy`)してみる。

`serverless.yml`の`service`ブロックの値を適当なものに変更。また、AWSの東京リージョンを使うので、`provider`ブロックの`region`を追加。（デフォルトだと us-east-1になってしまうらしい）

```yaml:serverless.yml
service: hogehoge-functions # NOTE: update this with your service name

provider:
  name: aws
  runtime: nodejs4.3
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
```

`handler.js`にてexportした`hello`関数をデプロイする、というのがなんとなく見て分かる。

続いて、`package.json`にデプロイ用のタスクも追加。ここから、Serverlessの操作にはAWSのAPIキーが必要となる。Administrator Access権限を与えたIAMユーザを作成し、APIキーとシークレットアクセスキーを入手。`config`キーに設定しておく。

※今回はテストなので無視したが、フルアクセスのIAMユーザを使うのはキケンなので、権限の精査が必要になりそう。

参考: [Serverless Framework利用時のアカウント毎のIAM権限について考えてみた - Qiita](http://qiita.com/quartette/items/be95c13908dabcd06644)

```json:package.json
{
  "config": {
    "aws_access_key_id": "XXXXX",
    "aws_secret_access_key": "XXXXXXXXXXXXXXX"
  },
  "scripts": {
    "deploy-lambda": "cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls deploy",
  },
  "devDependencies": {
    "cross-env": "^3.1.3",
    "serverless": "^1.3.0"
  }
}
```

タスクの中で環境変数を指定するので、[cross-env](https://www.npmjs.com/package/cross-env)を利用している。

ようやくデプロイの実行。

```console
$ npm run deploy-lambda -- -f hello 

> hello-serverless@1.0.0 deploy-lambda /Users/Aono/works/hello-serverless
> cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls deploy "-f" "dispatcher"

Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (13.07 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...

Service Information
service: hogehoge-functions
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  None
functions:
  hogehoge-functions-dev-hello: arn:aws:lambda:ap-northeast-1:xxxxxx:function:hogehoge-functions-dev-hello
```

これで完了。AWSのコンソールを見ると、"hogehoge-functions-dev-helloLambda"という名前のファンクションが登録されている。(※今回は触れないが、Serverlessでは本番環境用とテスト環境用を分けてデプロイすることができる。テスト用のdevがデフォルト)

また、ログを見ると、ServerlessがCloudFormationやS3を利用していることが分かる。こちらもAWSのコンソールから確認が可能。
(作業ディレクトリの`./serverless`以下に設定に利用したjsonができてたりもする)

### テスト実行
Serverlessを利用することで、ローカル環境からLambdaをテスト実行(`sls invoke`)することもできる。`package.json`にさらにタスクを追加。

```json:package.json
{
  "scripts": {
    "invoke-lambda": "cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls invoke"
  }
}
```

実行してみると、`hello`関数が動いてるのが分かる。

```console
$ npm run invoke-lambda -- -f hello

> hello-serverless@1.0.0 invoke-lambda /Users/Aono/works/hello-serverless
> cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls invoke "-f" "hello"

{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":{}}"
}
```

### ログの確認
関数実行時のログもローカルから確認できる(`sls logs`)。

```json:package.json
{
  "scripts": {
    "logs-lambda": "cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls logs"
  }
}
```

```console
$ npm run logs-lambda -- -f hello

> hello-serverless@1.0.0 logs-lambda /Users/Aono/works/hello-serverless
> cross-env AWS_ACCESS_KEY_ID=$npm_package_config_aws_access_key_id AWS_SECRET_ACCESS_KEY=$npm_package_config_aws_secret_access_key sls logs "-f" "hello"

START RequestId: xxxxxxxxxxxxxxxxx Version: $LATEST
END RequestId: xxxxxxxxxxxxxxxxx
REPORT RequestId: xxxxxxxxxxxxxxxxx	Duration: 2.22 ms	Billed Duration: 100 ms 	Memory Size: 1024 MB	Max Memory Used: 7 MB
```

コード内の`console.log`などで出力した内容を見れたりするので、デバッグ時には欠かせないかな。

## SNSと連携
本題。

Serverless Frameworkでは、SNSトピックの購読も提供されている
https://serverless.com/framework/docs/providers/aws/events/sns/

`serverless.yml`を以下のように設定すれば、SNSトピックをLambdaのトリガーにした状態でデプロイできる。このデプロイによって、"lambda-test"という名前のSNSトピックも同時に新規作成。

```yaml:serverless.yml
functions:
  dispatcher:
    handler: dispatcher.dispatch
    events:
      - sns: lambda-test
```

さっきの`handler.js`をコピって、適当に`dispatcher.js`とexportする関数`dispatch`を作成。

```js:dispatcher.js
'use strict';

module.exports.dispatch = (event, context, callback) => {
  // 中身はhandler.jsといっしょ
};
```

SNSトピックが作成され、新しくデプロイされたLambdaファンクションのトリガーになっていることがコンソールを見ると分かる。

![スクリーンショット 2016-12-18 17.00.31.png](https://qiita-image-store.s3.amazonaws.com/0/1795/e68b6964-99b6-569a-58fc-54fd0a1b8b49.png)

このSNSトピックをCloudWatchのアラームと連携させる。試しに、とあるテスト環境のELBのHealthyHostCountに関するアラームの通知先に設定。

ロードバランサー下には1つのEC2が配置してある。正常なホスト数の合計が1を下回ったら警告を通知するようにする。わざとEC2で動いているアプリケーションサーバをストップすると、CloudWatchから通知を受けたSNSを通して、Lambdaが起動する。(LambdaのコンソールのMonitoringなどから確認できる）

## チャットワークに通知
起動トリガーとなっているSNSの通知内容（イベント）を、Lambdaを使ってチャットワークに通知する。もうイベントのsubscription自体はできているので、あとはそれを元にファンクションの中身を書く。先ほどの`dispatcher.js`を変更。	

（Lambdaはイベント駆動であるがゆえに、ロジックを書きながらデバッグしていくのが面倒。何か良い方法あるのだろうか。。）

```js:dispatcher.js
'use strict';

const https = require('https');
const querystring = require('querystring');

module.exports.dispatch = (event, context) => {
  const record = event.Records[0];

  if (record.Sns) {
    const message = JSON.parse(record.Sns.Message);
    const alarmStatus = message.NewStateValue;
    const postMessge = alarmStatus +
      ": " +
      message.Trigger.Namespace +
      " " +
      message.Trigger.MetricName +
      "\n" +
      message.NewStateReason;

    const postData = querystring.stringify({
      body: postMessge
    });
    const requestOptions = {
      host: 'api.chatwork.com',
      port: 443,
      method: 'POST',
      path: '/v1/rooms/xxxxxxxx/messages',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': postData.length,
        'X-ChatWorkToken': 'xxxxxxxxxxxxxxxx'
      }
    };

    const req = https.request(requestOptions, res => {
      if (res.statusCode === 200) {
        context.succeed(res);
      } else {
        context.fail(res.statusCode);
      }
    });

    req.on('error', e => {
      console.log("RequestError: " + e.message);
      context.fail(e.message);
    });

    req.write(postData);
    req.end();
  }
};
```

SNSから受けたイベントをパースしてメッセージ分を作成し、チャットワークのAPIにPOST。ちなみに、`event.Records`の中身はこんなんが来る。（内容は省略してます）

```
{ EventSource: 'aws:sns',
  EventVersion: '1.0',
  EventSubscriptionArn: 'arn:aws:sns:ap-northeast-1:xxxxxxxxxx,
  Sns:
   { Type: 'Notification',
     MessageId: 'xxxxx',
     TopicArn: 'arn:aws:sns:ap-northeast-1:xxxxx',
     Subject: 'OK: "lambda-test" in Asia Pacific - Tokyo',
     Message: <通知内容データ>,
     Timestamp: '2016-12-18T05:41:20.824Z',
     ... }
}
```

これでデプロイし直せば設定おわり。CloudWatchのアラームが来るタイミングで、以下のようにチャットワーク上で通知が来る。

<img width="841" alt="スクリーンショット 2016-12-18 15.42.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/1795/a2b827ce-4513-490d-c68f-8b6af27c3b15.png">

## おわり
簡単ではあるが、いったんお試し終わり。

Serverless Frameworkを利用することで、Lambdaファンクションの簡単なデプロイ & AWS各種サービス(今回のSNSだけでなく、API GatewayやS3 etc.)との連携が簡単に実行できるのが分かった。イベント連携などを統合してやってくれるのはありがたい。
また、Serverlessプロジェクトのファイル群をGitで管理すれば、Lambdaファンクションなどの変更履歴を管理することが可能になる。これもフレームワークの恩恵ですね。

Lambda周りは、AWSが[re:Invent](https://reinvent.awsevents.com/)でもめちゃくちゃ力入れてる感を出してたので、ちゃんと学んでギョームで作ってるシステムのアーキテクチャにも取り入れていきたいところ。