LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-16

Amazon CloudWatch Synthetics使ってますか?

Amazon CloudWatch Synthetics をご存知でしょうか。

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

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

image.png

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

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

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

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

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

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です。

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

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";

    // リクエスト1
    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);
    // リクエスト2
    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]) を使って、
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)"
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1