Amazon CloudWatch Synthetics のAPI Canary-Blueprintを複数リクエスト対応して使いやすくしてみた！


# Amazon CloudWatch Synthetics使ってますか？
**Amazon CloudWatch Synthetics** をご存知でしょうか。

これまで、サードパーティーのサービスやCloudWatch Event + Lambda で行っていたURL監視を
１つのサービスで実現できるようになったAWSのサービスです。
2020年4月にGAになり、東京リージョンでも使えるようになっています。

Syntheticsには、ヘッドレスブラウザモジュール(Puppeteer)などが、標準で組み込まれていたり、
UIのフォームで入力するだけで、コードを生成してくれたりとURLを監視するのに便利な機能が詰まっています。
(内部的に、Lambdaを利用していますが、いろいろ普通のLambdaとは違います)詳しくは、以下を見て下さい。

- [Amazon CloudWatch Synthetics の一般提供開始](https://aws.amazon.com/jp/about-aws/whats-new/2020/04/amazon-cloudwatch-synthetics-generally-available/)
- [Canary を作成する - Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Create.html)
- [Canary のランタイムバージョン - Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html)
- [Amazon CloudWatch Synthetics を試してみた - Qiita](https://qiita.com/hoto17296/items/4c8562b51e1fb9d66e6a)
- [CloudWatch SyntheticsでHTTP監視をする – サーバーワークスエンジニアブログ](http://blog.serverworks.co.jp/tech/2020/01/29/cloudwatch_synthetics/)
- [\[アップデート\] Amazon CloudWatch Synthetics が一般利用可能になりました！ | Developers.IO](https://dev.classmethod.jp/articles/ga-amazon-cloudwatch-synthetics/)



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40049/30ed2b6f-8714-d8bc-79e6-93d6e53ab338.png)

# API CanaryのBluePrintが、いろいろ・・・

実際に画面で、API Canaryを選択して、生成してみると以下のようなコードが生成されます。

単純なケースでは、これで十分ですが、少し複雑になると、いろいろ不便です。

* URL監視なのに、URLをそのまま変数で与えることが出来ない
* レスポンス結果を検証出来ない（ログに出すだけでchunkを捨ててる）
* トークンを取得してから、APIリクエストを投げるなど複数リクエストの連携が出来ない

理由として考えられるのは、API Canaryは非優先なのかな。。というのと
`axios` や `request` などのリッチな？モジュールを読み込んでおらず、ベーシックな `http`, `https`
のみで頑張っているからかなとは思うんですが、
Lambda以外に追加料金を取られるサービスのBlueprintとしては、少し微妙ですかね。
(var とconst や functionとアローが混在したり・・・)

```js:API-CanaryのBlueprint（オリジナル）
var synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const https = require('https');
const http = require('http');

const apiCanaryBlueprint = async function () {
    const postData = "";

    const verifyRequest = async function (requestOption) {
      return new Promise((resolve, reject) => {
        log.info("Making request with options: " + JSON.stringify(requestOption));
        let req
        if (requestOption.port === 443) {
          req = https.request(requestOption);
        } else {
          req = http.request(requestOption);
        }
        req.on('response', (res) => {
          log.info(`Status Code: ${res.statusCode}`)
          log.info(`Response Headers: ${JSON.stringify(res.headers)}`)
          if (res.statusCode !== 200) {
             reject("Failed: " + requestOption.path);
          }
          res.on('data', (d) => {
            log.info("Response: " + d);
          });
          res.on('end', () => {
            resolve();
          })
        });

        req.on('error', (error) => {
          reject(error);
        });

        if (postData) {
          req.write(postData);
        }
        req.end();
      });
    }

    const headers = {"x-api-key":"hogehoge"}
    headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' ');
    const requestOptions = {"hostname":"example.com","method":"GET","path":"/test","port":80}
    requestOptions['headers'] = headers;
    await verifyRequest(requestOptions);
};

exports.handler = async () => {
    return await apiCanaryBlueprint();
};
```

# リファクタリングしてみた

実際に、トークンを取得して、リクエストを投げるようなコードになっています。
この状態であれば、多少複雑なリクエストでも、 `apiCanaryBlueprint` 内だけを修正すれば
いろんなAPIの検証に使えるかなと思います。
request関数は、オブジェクトを引数にしており
`url`, `method`, `auth`, `headers`, `postData` を必要に応じて指定すればOKです。

まだ、雑な感じですが、なにかの参考にして下さい。

```js:API-CanaryのBlueprint（改善後）
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const https = require('https');
const http = require('http');
const querystring = require('querystring');

const request = async ({ url, method = 'GET', auth, headers = {}, postData } = {}) => {
    const parseUrl = new URL(url);
    const httpx = parseUrl.protocol === 'https:' ? https : http;
    if (headers['User-Agent']) headers['User-Agent'] = synthetics.getCanaryUserAgentString();
    const params = {
        hostname: parseUrl.hostname,
        path: `${parseUrl.pathname}${parseUrl.search}`, // without hash(#)
        method,
        auth,
        headers,
    };

    return new Promise((resolve, reject) => {
        const req = httpx.request(params, res => { // https.request(url[, options][, callback]) is not work
            const message = `Status Code: ${res.statusCode} url: ${url} method: ${method}`;
            if (res.statusCode !== 200) {
                log.error(message);
                return reject(new Error(message));
            } else {
                log.info(message);
            }
            const data = [];

            res.on('data', chunk => {
                data.push(chunk);
            });
            res.on('end', () => resolve(Buffer.concat(data).toString()));
        });
        req.on('error', err => reject(err));
        if (postData) {
            req.write(postData);
        }
        req.end();
    });
};

const apiCanaryBlueprint = async () => {
    const AUTH_CLIENT_URL = 'http://example.com/oauth2/token';
    const AUTH_CLIENT_ID = 'ClientId';
    const AUTH_CLIENT_SECRET = 'ClinetSecret';
    const TARGET_URL = "http://example.com/test";

    // リクエスト１
    const authRequestParam = {
        url: AUTH_CLIENT_URL,
        method: 'POST',
        auth: `${AUTH_CLIENT_ID}:${AUTH_CLIENT_SECRET}`,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        postData: querystring.stringify({
            grant_type: 'client_credentials'
        })
    };
    const tokenString = await request(authRequestParam);
    const token = JSON.parse(tokenString);
    // リクエスト２
    const targetRequestParam = {
        url: TARGET_URL,
        headers: {
            "Authorization": `Bearer ${token.access_token}`
        }
    };
    const response = await request(targetRequestParam);
    return "Success";
};

exports.handler = async () => {
    return await apiCanaryBlueprint();
};
```

# 余談

当初は、Node.js 10.xなので、[https.request(url[, options][, callback])](https://nodejs.org/docs/latest-v10.x/api/https.html#https_https_request_url_options_callback) を使って、
urlを引数にして、そのままリクエストを投げようと思ったんですが、
ローカルでは、うまく動くものの、Syntheticsに上げると以下のエラーで、どうにも動かないので、諦めました。
(Syntheticsは、デバックがとてもしにくい。。。)

```
"TypeError [ERR_INVALID_ARG_TYPE]: 
The \"listener\" argument must be of type Function. Received type object Stack: 
TypeError [ERR_INVALID_ARG_TYPE]: The \"listener\" argument must be of type Function.
 Received type object\n    at checkListener (events.js:55:11)\n
    at ClientRequest.once (events.js:299:3)\n
    at new ClientRequest (_http_client.js:160:10)\n
    at Object.request (https.js:289:10)\n
    at Object.request (/opt/nodejs/node_modules/agent-base/patch-core.js:23:20)\n
    at Promise (/opt/nodejs/node_modules/apiCanaryBlueprint.js:17:25)\n
    at new Promise (<anonymous>)\n
    at request (/opt/nodejs/node_modules/apiCanaryBlueprint.js:16:12)\n
    at apiCanaryBlueprint (/opt/nodejs/node_modules/apiCanaryBlueprint.js:59:31)\n
    at Object.exports.handler (/opt/nodejs/node_modules/apiCanaryBlueprint.js:72:18)"
```