AWS SDK for JavaScript で Promiseを使う

2012年頃に スマホゲームのバックエンドを Node.js で開発した際には外部サーバやDBサーバとの通信を行うたびに コールバックを大量に書いて疲れていました。当時も非同期のライブラリがありましたが、当時は(も？)自分のJavaScriptレベルが低くて時間の制約から使うの諦めました。

今回、CloudWatchのカスタムメトリクスを登録する Lambda function を作るために Promise を勉強しました。

## 参考にしたサイト

- javascript promise 使い方 [http://azu.github.io/promises-book/](http://azu.github.io/promises-book/)

断片的な How To ではないので、最も理解できたコンテツでした。


## AWS SDK for JavaScript のバージョン

- 今回利用した SDK のバージョンは `2.309.0` です。
- SDKは v2.3.0 でPromiseに対応しています。 
[https://aws.amazon.com/jp/blogs/developer/support-for-promises-in-the-sdk/](https://aws.amazon.com/jp/blogs/developer/support-for-promises-in-the-sdk/)
- 公式マニュアル [https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/using-promises.html](https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/using-promises.html)
- Node.js は v8.10 を使いました。

## 実装したもの

SQSに作成した２つのキューから、キュー数を取得して CloudWatchのカスタムメトリクスに登録します。

### ソースコード
利用する環境変数は下の２つです。
1.  `queue` : キュー名は環境変数  に JSONの配列で記載します。例えば　`["my-sqs1","my-sqs2"]` です。
2.  `env` : メトリクスの Dimension に使います。

``` sqs_queues.js
'use strict'

// export env_name=dev

const AWS = require('aws-sdk');

AWS.config.update({region: 'ap-northeast-1'});

// Create CloudWatch service object
const SQS = new AWS.SQS({apiVersion: '2012-11-05'});
const CloudWatch = new AWS.CloudWatch({apiVersion: '2010-08-01'});

// 参照するSQSのキュー
const SOURCE_SQS_QUEUE_NAMES = JSON.parse(process.env.queue);
const SOURCE_SQS_METRICS_NAME = 'ApproximateNumberOfMessages';

// 合算したキュー数を書き込む CloudWatchのメトリクス
const DIST_METRICS_NAMESPACE = 'my-metrics-namespace';
const DIST_METRICS_NAME = 'my-custom-metrics';
const ENV_NAME = process.env.env_name; // dev, stage, prod ....


// メモ 2018年09月03日 時点
// 一度登録したカスタムメトリクスを コンソールやSDKで消すことはできない。
// 定められた保存期間を過ぎると削除される
// https://aws.amazon.com/jp/cloudwatch/faqs/

async function main(SOURCE_SQS_QUEUE_NAMES) {

    Promise.all(SOURCE_SQS_QUEUE_NAMES.map(function (queue_name) {
        // SQSのキューのURLを取得
        return queue_url(queue_name);
    })).then(function(urls){
        // キューを取得できたら、SQSの属性「ApproximateNumberOfMessages」を取得
        return Promise.all(urls.map(function (url) {
            return queue_number_of_messages(url);
        }))
    }).then(function(counts){
        // 各キューの ApproximateNumberOfMessages を合算して、メトリクスを登録
        let metrics_value = 0;
        counts.forEach(function (v) {
            console.log(v);
            metrics_value = metrics_value + v.size;
        });
        return put_metrics(metrics_value);
    }).then(function(putMetricsResponse){
        // メトリクスの登録に成功した
        console.log(JSON.stringify(putMetricsResponse));
    }).catch(function(err){
        // 上のいずれかでエラーが発生した。
        console.log(err);
    });
}

/**
 * SQSのキューURLを取得する
 * @param queue_name　取得する名
 * @returns {Promise<any>}
 */
function queue_url(queue_name) {
    const param = {
        QueueName: queue_name,
    };

    return SQS.getQueueUrl(param)
        .promise()
        .then(function (value) {
            return value.QueueUrl;
        });
}

/**
 * SQSのキュー数(ApproximateNumberOfMessages) を取得する
 * @param queue_url 取得するキューのURL
 * @returns {Promise<any>}
 */
function queue_number_of_messages(queue_url) {
    const param = {
        QueueUrl: queue_url,
        AttributeNames: [SOURCE_SQS_METRICS_NAME]
    };
    return SQS.getQueueAttributes(param)
        .promise()
        .then(function (value) {
            return {
                queue_url: queue_url,
                size: parseInt(value.Attributes.ApproximateNumberOfMessages),
            }}
        );
}

/**
 * カスタムメトリクスを登録する。登録されるメトリクスは下の通りになる
 * - Namespace: DIST_METRICS_NAMESPACE
 * - メトリクス名: DIST_METRICS_NAME
 * - ディメンジョン： Environment：環境名(実行時の環境変数 env_name の値 )
 * - 値： 引数 value
 *
 * @param value メトリクスの値。
 * @returns {Promise<any>}
 */
function put_metrics(value) {
    const metrics = {
        MetricData: [
            {
                Dimensions: [
                    {
                        Name: 'Environment',
                        Value: ENV_NAME
                    },
                ],
                MetricName: DIST_METRICS_NAME,
                Timestamp: new Date(),
                Value: "" + value,
                Unit: 'Count',
            },
        ],
        Namespace: DIST_METRICS_NAMESPACE
    };
    return CloudWatch.putMetricData(metrics)
        .promise()
        .then(function(data){
            return {metrics: metrics, put_metrics_result: data};
        });
}

main(SOURCE_SQS_QUEUE_NAMES);

```

### 処理イメージ

![NodejsでPromise.png](https://qiita-image-store.s3.amazonaws.com/0/145742/f6ee39fe-7ea9-242e-f28d-05baee1779b9.png)


### やっていること
Promiseや非同期を調べていて、やり方を見つけられなかったのは、並列実行の結果を、次の直列実行に結果を渡す方法でした。

直列実行では `Promise.then()` の引数に指定した関数が return することで次の　`Promise.then()` の引数に指定した関数が実行される時の引数になります。これと、 `Promise.all()` で並列実行する方法と組み合わせる方法に辿りつくまでに時間がかかりました。 

辿り着いた方法は下の通りです。

1.  AWSへの処理は AWS SDKで Promiseを作成して、 `then` で実行される関数で直列実行で次に渡したい値を return する。 今回は`queue_url`関数と `queue_number_of_messages` 関数です。
2.  並列実行は 配列の各要素に対して行うので、 配列の `map` 関数で 前述の 関数を実行する Promise の配列を作る。作った配列は `Promise.all()` で並列実行します。
3.  メトリクスの登録は並列実行しないでの、`put_metrics` は単独で実行します。 `Promise.all()` を使いません。
4. `then()` は結果を確認できるように `putMetrics` の結果を出力します。

### エラー処理

Promiseの `catch()` は最後の１つだけです。途中でエラーになった場合は、すべてここで処理します。