Amazon CloudWatch Synthetics で REST API を監視する

AWS

2020.7.28

Topics

こんにちは。データサイエンスチームの t2sy です。

Web アプリケーションのエンドポイントと API を監視できる Amazon CloudWatch Synthetics が 2020年4月に General Availability (GA) となりました。この記事では、Amazon CloudWatch Synthetics の API Canary を使用し、Amazon API Gateway の Lambda 統合で作成した REST API に対する監視を試してみたのでご紹介します。

Amazon CloudWatch Synthetics を用いることで、Web アプリケーションのエンドポイントと API の Synthetics Monitoring (合成監視) が行えます。Web アプリケーションのハートビート監視や、API の監視、リンク切れチェッカなどの Lambda 関数 (Canary スクリプト) の設計図 (Blueprint) が提供されており、簡単に監視を行うことができます。設計図を元に用途に合わせ Lambda 関数をカスタマイズすることもできます。

Amazon API Gateway の Lambda 統合で REST API を作成

最初に、Amazon CloudWatch Synthetics の API Canary で監視を行う対象となる REST API を作成します。

AWS Lambda コンソールから Lambda 関数を作成します。ランタイムは Python 3.7 としています。以下のコードを入力し保存します。

import json


def lambda_handler(event, context):
    param = event["myParam"]
    return {"statusCode": 200, "body": json.dumps(param)}

次に、Amazon API Gateway コンソールに移り、 REST API を作成します。
[プロトコルを選択する] で REST を選択します。今回、[API 名] は test-lambda-integration、[エンドポイントタイプ] はリージョンとしています。

次に、Amazon API Gateway のバックエンドとして Lambda 関数をアタッチします。
作成した API に子リソースを作成し、GET メソッドをこのリソースに追加します。[アクション] ドロップダウンメニューから [リソースの作成] を選択しリソースを作成します。今回、[リソース名] は my-resource としています。

続いて、[アクション] ドロップダウンメニューから、[メソッドの作成] から [GET] を選択します。
[統合タイプ] は Lambda 関数を選択、[Lambda 関数] に先ほど作成した Lambda 関数の名前を指定します。

次に、リクエストのクエリ文字列を Lambda 関数の入力イベントにマッピングします。
[統合リクエスト] を選択し、[マッピングテンプレート] を展開、 Content-type に application/json と入力し保存した後、テンプレート部分に以下を入力します。

{
    "myParam": "$input.params('myParam')"
}

作成した REST API をデプロイします。ナビゲーションペインから [ステージ] を選択します。今回、[ステージ名] は dev としています。
API のリソースに戻り [アクション] ドロップダウンメニューから、[APIのデプロイ] を選択します。[デプロイされるステージ] に作成したステージを指定し、[デプロイ] を選択します。

curl コマンドで作成した REST API に HTTP リクエストを送信し正常にレスポンスが返ることを確認します。

$ curl -X GET 'https://dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com/dev/my-resource?myParam=Hello%20from%20API%20Gateway!'
{"statusCode": 200, "body": "\"Hello from API Gateway!\""}

実際に試す場合は、上記のホスト名の dg5gbb74re を作成した API の ID に、ap-northeast-1 を API がデプロイされているリージョンに置き換える必要がある点にご注意ください。

ここまでの詳細な手順は 「Amazon API Gateway で Lambda 統合を使用して REST API を作成する」をご参照ください。

Amazon CloudWatch Synthetics

監視対象の REST API の準備ができたので、本題である Amazon CloudWatch Synthetics の API canary による REST API 監視に移ります。

Amazon CloudWatch のナビゲーションペインから Canaries を選択します。
[Canary を作成] を選択します。今回は設計図から Canary を作成するので [設計図を使用する] を選択します。

本記事の執筆時点で、以下の Canary タイプの設計図が提供されています。今回は API Canary を試したいので [API Canary] を選択します。

  • ハートビートのモニタリング
  • API Canary
  • リンク切れチェッカー
  • GUI ワークフロー ビルダー

[方法] は GET を選択し、[アプリケーションまたはエンドポイントURL] には、作成した Amazon API Gateway のエンドポイントURLを入力します。今回、 Canary の [名前] は test-api-get-canary としています。

今回は、クエリ文字列のみ設定しましたが、用途に合わせて HTTP ヘッダや HTTP メッセージボディに値を設定することもできます。
入力した内容はスクリプトエディタ上のコードに反映されます。 全体の Lambda 関数のコード (Node.js) は以下となります。

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 = {}
    headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' ');
    const requestOptions = {"hostname":"dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com","method":"GET","path":"/dev/my-resource?myParam=HelloFromCanary!","port":443}
    requestOptions['headers'] = headers;
    await verifyRequest(requestOptions);
};

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

実験のため [スケジュール] を [1 回実行] に設定します。最後に、[Canary を作成] を選択します。

数十秒経過後、コンソールを見ると成功していることが確認できます。

Amazon CloudWatch Synthetics と CloudWatch アラームの連携

次に、意図的にリクエストが失敗する状況を作り、Canary が失敗を検出、CloudWatch アラームが ALARM 状態に移行するか確認してみます。

実際のユースケースとして、アプリケーションに機能追加や変更を行いデプロイしたときに、リソースに対する POST メソッドを処理する部分に不具合があり内部エラーで失敗するという場合を想定しています。

先ほど作成した Lambda 関数の一部を変更し、POST メソッドのリクエストの場合に例外を投げるようにします。

import json


def lambda_handler(event, context):
    param = event["myParam"]
    http_method = event["httpMethod"]

    if http_method == "POST":
        raise Exception("internal server error")

    return {"statusCode": 200, "body": json.dumps(param)}

次に、Amazon API Gateway コンソールに移り、先ほど作成した REST API を選択します。

my-resource リソースに対して、[アクション] ドロップダウンメニューの [メソッドの作成] から [POST] を選択しメソッドを追加します。

HTTP リクエストメソッドを変更後の Lambda 関数の入力イベントにマッピングする設定を追加します。[統合リクエスト] を選択し、[マッピングテンプレート] を展開、Content-type に application/json と入力し保存した後、テンプレート部分に以下を入力します。

{
    "myParam": "$input.params('myParam')",
    "httpMethod": "$context.httpMethod"
}

続いて、バックエンドの Lambda 関数が例外で終了した場合に 500 ステータスコードを返すようにマッピングします。
まず、[メソッドレスポンス] の [レスポンスの追加] を選択し、500 と入力し保存します。次に、[統合レスポンス] の [統合レスポンスの追加] を選択、[Lambda エラーの正規表現] に Lambda 関数が例外時に返す出力に含まれる internal server error を入力します。[メソッドレスポンスのステータス] には追加した 500 を選択、[コンテンツの処理] はパススルーを選択し保存します。

変更を反映させるため、[アクション] ドロップダウンメニューから [APIのデプロイ] を選択します。[デプロイされるステージ] に作成したステージを指定し、[デプロイ] を選択します。

curl コマンドで更新した REST API に POST メソッドで HTTP リクエストを送信し 500 ステータスコードが返ることを確認してみます

 $ curl -v -X POST 'https://dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com/dev/my-resource?myParam=Hello%20from%20API%20Gateway!'
 
 ...
 
 > POST /dev/my-resource?myParam=Hello%20from%20API%20Gateway! HTTP/1.1
 > Host: dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com
 > User-Agent: curl/7.68.0
 > Accept: */*
 >
 * Mark bundle as not supporting multiuse
 < HTTP/1.1 500 Internal Server Error
 < Date: Mon, 20 Jul 2020 08:43:54 GMT
 < Content-Type: application/json
 < Content-Length: 201
 < Connection: keep-alive
 < x-amzn-RequestId: eff3e42c-ea81-4c88-98b8-221ea7be0d10
 < x-amz-apigw-id: P9rjuGWWNjMFe5A=
 < X-Amzn-Trace-Id: Root=1-5f15594a-5893ef9e81d530b239f91df7;Sampled=0
 <
 * Connection #0 to host dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com left intact
 {"errorMessage": "internal server error", "errorType": "Exception", "stackTrace": ["  File \"/var/task/lambda_function.py\", line 6, in lambda_handler\n    raise Exception('internal server error')\n"]}

これで HTTP リクエストメソッド が POST メソッドの場合に必ず失敗する REST API ができました。

Amazon CloudWatch Synthetics の Canary で失敗を検出し、 CloudWatch アラームのメトリクスアラームが ALARM 状態に移行するか確認してみます。

Amazon CloudWatch コンソールのナビゲーションペインから Canaries を選択し、先ほど作成した test-api-get-canary を選択します。
[アクション] ドロップダウンメニューから [クローン] を選択し、複製された Canary の名前を test-api-post-canary とします。

次に、[スクリプトエディタ] で Lambda 関数のコードを一部変更します。

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 = {}
    headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' ');
    const requestOptions = {"hostname":"dg5gbb74re.execute-api.ap-northeast-1.amazonaws.com","method":"POST","path":"/dev/my-resource?myParam=HelloFromCanary","port":443}
    requestOptions['headers'] = headers;
    await verifyRequest(requestOptions);
};

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

[スケジュール] を [継続的に実行] に変更し間隔を設定します。

CloudWatch アラームと連携するために、[CloudWatch アラーム – オプション] を展開し、[新しいアラームの追加] を選択します。今回は、アラームの条件はデフォルト値のままとします。最後に [Canary を作成] を選択します。

作成後、数十分経過してから、Canary を確認するとメトリクスアラームの状態が ALARM 状態となっていることが確認できます。

アラームの通知はシステム監視において重要な機能のひとつです。CloudWatch アラーム作成時に Amazon SNS と連携し、アラームの状態が変わったときに通知が送信されるように設定することができます。

おわりに

今回は、Web アプリケーションのエンドポイントと API を監視できる Amazon CloudWatch Synthetics の API Canary を使用して、Amazon API Gateway の Lambda 統合で作成した REST API に対して監視する方法をご紹介しました。API Canary 以外の設計図の用途や使い方は Canary 設計図の使用 をご参照ください。

t2sy

2016年11月、データサイエンティストとして中途入社。時系列分析や異常検知、情報推薦に特に興味があります。クロスバイク、映画鑑賞、猫が好き。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら