Amazon CloudWatch Synthetics使ってますか?
Amazon CloudWatch Synthetics をご存知でしょうか。
これまで、サードパーティーのサービスやCloudWatch Event + Lambda で行っていたURL監視を
1つのサービスで実現できるようになったAWSのサービスです。
2020年4月にGAになり、東京リージョンでも使えるようになっています。
Syntheticsには、ヘッドレスブラウザモジュール(Puppeteer)などが、標準で組み込まれていたり、
UIのフォームで入力するだけで、コードを生成してくれたりとURLを監視するのに便利な機能が詰まっています。
(内部的に、Lambdaを利用していますが、いろいろ普通のLambdaとは違います)詳しくは、以下を見て下さい。
- Amazon CloudWatch Synthetics の一般提供開始
- Canary を作成する - Amazon CloudWatch
- Canary のランタイムバージョン - Amazon CloudWatch
- Amazon CloudWatch Synthetics を試してみた - Qiita
- CloudWatch SyntheticsでHTTP監視をする – サーバーワークスエンジニアブログ
- [アップデート] Amazon CloudWatch Synthetics が一般利用可能になりました! | Developers.IO
API CanaryのBluePrintが、いろいろ・・・
実際に画面で、API Canaryを選択して、生成してみると以下のようなコードが生成されます。
単純なケースでは、これで十分ですが、少し複雑になると、いろいろ不便です。
- URL監視なのに、URLをそのまま変数で与えることが出来ない
- レスポンス結果を検証出来ない(ログに出すだけでchunkを捨ててる)
- トークンを取得してから、APIリクエストを投げるなど複数リクエストの連携が出来ない
理由として考えられるのは、API Canaryは非優先なのかな。。というのと
axios
や request
などのリッチな?モジュールを読み込んでおらず、ベーシックな http
, https
のみで頑張っているからかなとは思うんですが、
Lambda以外に追加料金を取られるサービスのBlueprintとしては、少し微妙ですかね。
(var とconst や functionとアローが混在したり・・・)
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です。
まだ、雑な感じですが、なにかの参考にして下さい。
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)"