AQ Tech Blog

CloudFormationによるCloudWatch Syntheticsの構築(プライベートエンドポイントモニタリング) | AQ Tech Blog

作成者: takeshi.yoshida|2022年12月15日

当記事の対象者

CloudWatch Syntheticsについてある程度の知識があり、実際にCloudFormationで構築しようという段階の人を対象としております。

当記事ではプライベートエンドポイントモニタリングをメインに解説していきます。

当記事の執筆理由

現在参画させていただいている案件で、初めてCloudWatch Syntheticsを構築しました。

ネットで調べながら構築したところ、現在のネット上のナレッジはパブリックにあるウェブページのモニタリングが多く、プライベートにあるAPIのモニタリングに関しては少ないように感じました。特にCloudFormationによる構築を説明した記事はあまりない印象です。

(当記事執筆時点(2022年10月)、AWS SAMによるSyntheticsのカナリーデプロイは非対応です。)

参考:Support for AWS::Synthetics::Canary deploy

これからCloudFormationでCloudWatch Synthetics、特にプライベートエンドポイントモニタリングを行うカナリーを構築しようとしている方にお役に立つことができればと思い、汎用的な部分を解説した記事を執筆させていただきました。

CloudWatch Syntheticsの導入理由・目的

業務アプリケーションを定期的に模擬実行し、一連の業務が問題なく処理できるか能動的に監視するためです。AWSの公式ブログでも、CloudWatch Syntheticsでプライベートエンドポイントを監視すべき理由を解説されていましたので、その理由の一つを引用します。

1.Many modern applications are built such that various elements on a single webpage are powered by different underlying microservices and stacks. (省略)You want to detect and correlate the internal or external failure the instant it occurs and fix it before your customers send in a complaint or file this issue with support.

 

(和訳)

最近のアプリケーションの多くは、1つのWebページ上のさまざまな要素が、基盤となるさまざまなマイクロサービスやスタックによって動かされるように構築されています。(省略)内部または外部の障害が発生した瞬間にそれを検出して関連付け、顧客が苦情を送ったり、サポートにこの問題を報告したりする前に修正したいと思います。

引用:Monitor your private internal endpoints 24×7 using CloudWatch Synthetics | AWS Cloud Operations & Migrations Blog

まさしく今回の案件の環境がサーバレスマイクロサービスでしたので、上記の監視理由のユースケースと合致しています。

アーキテクチャ図

当記事執筆にあたり、弊社の検証用AWSアカウントで以下のアーキテクチャを構築しました。プライベートにあるAPIのモニタリングの基本的な要素は抑えた構成だと思います。

CloudFormationのテンプレート例

次にClouodFormationのテンプレート例です。

Synthetics、IAMポリシー、IAMロール以外のリソースに関しては割愛させていただきます。ご了承ください。

AWSTemplateFormatVersion: 2010-09-09
Description: 'Cloudformation template for Cloudwatch Synthetics'

Resources:
# Sytnhetics用IAMポリシー作成
  IAMPolicyForSynthetics:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: 'iam-policy-for-canary'
      Path: '/'
      PolicyDocument:
        !Sub
            |
            {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Action": [
                            "s3:PutObject",
                            "s3:GetObject"
                        ],
                        "Resource": [
                            "arn:aws:s3:::アーティファクト配置用S3バケット/*"
                        ]
                    },
                    {
                        "Effect": "Allow",
                        "Action": [
                            "s3:GetBucketLocation"
                        ],
                        "Resource": [
                            "arn:aws:s3:::アーティファクト配置用S3バケット"
                        ]
                    },
                    
                    {
                        "Effect": "Allow",
                        "Action": [
                            "logs:CreateLogStream",
                            "logs:PutLogEvents",
                            "logs:CreateLogGroup"
                        ],
                        "Resource": [
                            "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/cwsyn-canary-*"
                        ]
                    },
                    {
                        "Effect": "Allow",
                        "Action": [
                            "s3:ListAllMyBuckets",
                            "xray:PutTraceSegments"
                        ],
                        "Resource": [
                            "*"
                        ]
                    },
                    {
                        "Effect": "Allow",
                        "Resource": "*",
                        "Action": "cloudwatch:PutMetricData",
                        "Condition": {
                            "StringEquals": {
                                "cloudwatch:namespace": "CloudWatch Synthetics"
                            }
                        }
                    },
                    {
                        "Effect": "Allow",
                        "Action": [
                            "ec2:CreateNetworkInterface",
                            "ec2:DescribeNetworkInterfaces",
                            "ec2:DeleteNetworkInterface"
                        ],
                        "Resource": [
                            "*"
                        ]
                    }
                ]
            }

# Sytnhetics用IAMロール作成
  IAMRoleForSynthetics:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'iam-role-for-canary'
      Path: '/'
      AssumeRolePolicyDocument:
        |
        {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
      MaxSessionDuration: 3600
      ManagedPolicyArns:
      - !Ref IAMPolicyForSynthetics


  Synthetics:
    Type: AWS::Synthetics::Canary
    Properties:
      ArtifactS3Location: s3://アーティファクト配置用S3バケット/(任意)prefix
      Code:
        Handler: apiCanaryBlueprint.handler
        S3Bucket: コード格納S3バケット
        S3Key: canary.zip
      DeleteLambdaResourcesOnCanaryDeletion: true
      ExecutionRoleArn: !GetAtt IAMRoleForSynthetics.Arn
      Name: canary
      RuntimeVersion: syn-nodejs-puppeteer-3.7
      Schedule:
        Expression: cron(0,30 * * * ? *)
      StartCanaryAfterCreation: true
      VPCConfig:
        SecurityGroupIds:
          - カナリー用セキュリティグループ
        SubnetIds:
          - サブネットa
          - サブネットc
          - サブネットd

順に解説します。

 

IAMポリシー

大元は公式ドキュメントです。

参考URLの「AWS KMS を使用しないが Amazon VPC アクセスを必要とする Canary」の内容を元にしております。

ポイントを2つピックアップして解説します。

参考:Canary に必要なロールとアクセス許可 - Amazon CloudWatch

 

ロググループ

カナリーのロググループは以下の形式で作成されます。

 /aws/lambda/cwsyn-{カナリー名}-{カナリーID(ランダムID)} 

カナリーIDは、カナリー作成前に知ることはできないので「*」で対処します。

今回のカナリーの名前はcanaryですので、logsのアクション許可のリソースの記載は以下のようになります。

 "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/cwsyn-canary-*" 

s3:ListAllMyBuckets

Syntheticsを動かすにあたって、許可しなければならないアクションの一つにs3:ListAllMyBucketsがあります。このs3:ListAllMyBucketsは、リソースレベルのアクセス許可をサポートしてしないため、リソースは「*」で指定する必要があります。

参考:Amazon S3 のアクション、リソース、条件キー - サービス認証リファレンス

 IAMポリシーからは話が逸れますが、S3のVPCエンドポイントにてカスタムポリシーでアクセスを絞る際も、このs3:ListAllMyBucketsを許可する必要があります。

今回の構成だと以下の内容のカスタムポリシーとなります。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:ListAllMyBuckets",
			"Resource": "*"
		},
		{
			"Effect": "Allow",
			"Principal": "*",
			"Action": "*",
			"Resource": [
				"arn:aws:s3:::アーティファクト配置用S3バケット",
				"arn:aws:s3:::アーティファクト配置用S3バケット/*"
			]
		}
	]
}

今回の例はシンプルな構成なため、ドキュメント記載のポリシーに追加した箇所はほとんどありません。実際の案件では、APIを叩く前処理としてカナリーが他のAWSリソースにアクセスする事がありましたので、そのAWSリソースのアクセス許可を追加しております。

このようにカナリーが利用するAWSリソースに合わせて、ポリシーをカスタマイズしてください。

 

IAMロール

こちらも公式ドキュメントに記載されているIAMロールを元にしております。

参考:の Canary に必要なロールとアクセス許可 - Amazon CloudWatch

 

Synthetics

Synthteticsも公式ドキュメントを下地にしてます。いくつかポイントをピックアップします。

参考:AWS::Synthetics::Canary - AWS CloudFormation

Code

CodeプロパティのScriptプロパティで、直接テンプレート内にコードを記述することができますが、テンプレートが長くなりますので、コードをS3に格納したZIPファイルから展開する方が良いと思います。

 

Handlerプロパティにはスクリプトエントリポイントを指定します。

スクリプトエントリポイントは以下の形式に合わせる必要があります。

  カナリーファイル名.関数名 

AWSマネージドコンソール上のスクリプトエントリポイントの説明を引用します。

Canary スクリプトのエントリポイントとして機能するファイル名と、エクスポートされたハンドラー関数を「filename.handler」形式で入力します。

たとえば、「index.handler」は index.js の export「handler」を呼び出します。

後述する、AWSが用意しているNode.jsのテンプレートスクリプトの内容に合わせて、

カナリーファイル名をapiCanaryBlueprint、関数名をhandlerとしましたので、

スクリプトエントリポイントは「apiCanaryBlueprint.handler」となります。

 

もしここでスクリプトエントリポイントの指定に誤りがある場合、

カナリー実行ログに「Cannot find module」エラーが出力されます。

S3KeyプロパティにはZIPファイル名を指定します。ZIPファイルの中身は、以下のフォルダ構成である必要があります。(Node.jsの場合)

  nodejs/node_modules/カナリーファイル名.js  

 

今回の例ですと、以下のフォルダ構成になります。

  nodejs/node_modules/apiCanaryBlueprint.js  

参考:Node.js Canary スクリプトの記述 - Amazon CloudWatch

DeleteLambdaResourcesOnCanaryDeletion、StartCanaryAfterCreation

DeleteLambdaResourcesOnCanaryDeletionはカナリー削除時にLambdaリソースを削除するかを、StartCanaryAfterCreationはカナリーを作成した後すぐにカナリーを実行するかをいずれもboolean型で設定できます。

 

RuntimeVersion

ランタイムの説明は公式ドキュメントから引用します。

Synthetics ランタイムは、スクリプトハンドラーを呼び出す Synthetics コードと、バンドルされた依存関係の Lambda レイヤーを組み合わせたものです。

引用:Synthetics のランタイムバージョン - Amazon CloudWatch

当記事執筆時点(2022年10月)では、コードを実行する言語にNode.js、Pythonがサポートされています。慣れている方で良いと思います。AWSマネージドコンソールで設定する際は、Node.js用のランタイムであるsyn-nodejs-puppeteerがデフォルトで指定されています。ネット上のナレッジもsyn-nodejs-puppeteerを利用している事が多いので、Node.jsの方がやりやすいかもしれません。

ランタイムのバージョンによって対応しているNode.jsのバージョンも変わるので、

以下の公式ドキュメントのページを参照してください。

参考:Node.js と Puppeteer を使用するランタイムバージョン - Amazon CloudWatch

 

Schedule

Expressionプロパティで、実行間隔や実行時間を指定する事が可能です。

当記事執筆時点(2022年10月)では、実行間隔を指定するrate式と、実行時間を指定するcron式を利用する事ができます。

ただrate式の場合、カナリーを更新したり、カナリーを停止起動するたびに実行時間がズレてしまうので、cron式でスケジュールを組んだ方が無難かと思います。

今回の例では、毎時0分、30分にカナリーが実行されます。

cron式の書き方は公式ドキュメントを参考にしてください。

参考:cron を使用して Canary 実行をスケジュールする - Amazon CloudWatch

 

VPCConfig

カナリーをVPC内に配置する場合に必要となります。カナリー配置用のサブネットとセキュリティグループを指定してください。Lambdaにも同様のプロパティがあるのですが、

LambdaはVpcConfig

SyntheticsはVPCConfig

と微妙にVPCの表記が異なります。気をつけてください。

カナリー用コード

カナリー用コードですが、AWSが用意したテンプレートスクリプトを下地に作成するのが楽だと思います。

 

テンプレートスクリプト(API Canary)

AWSマネージドコンソールから「CloudWatch」→「CloudWatch Synthetics」→「Canaryを作成」とクリックすると、カナリーの作成ページに飛びます。

このページで「設計図を使用する」を選択すると、AWSが用意したテンプレートスクリプトを利用することができます。当記事執筆時点(2022年10月)では、以下の6つの設計図が用意されています。

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

それぞれの設計図の詳細内容は以下のページを参考にしてください。

参考:Canary 設計図の使用 - Amazon CloudWatch

 

今回はAPIを監視しますので、「API Canary」を選択します。

「Amazon API Gateway APIを使用しています」にチェックを入れると、

アカウントにあるAPI Gatewayとステージを選択する事ができます。

対象のAPI Gatewayを選択すると、自動的にエンドポイントURLを入力してくれるので便利です。しかし、この方法だと後述するHTTPリクエストを設定する際、HTTPメソッドがGETしか選択することができません。また、複数ステップ設定時に別のAPIを監視する事ができません。GET以外のHTTPメソッドを利用したい、あるいは1つのカナリーで複数のAPIを監視したい場合は、「Amazon API Gateway APIを使用しています」のチェックを外して進んでください。

 

 

HTTPリクエスト

HTTPリクエストを追加します。カナリーの1ステップにつき1HTTPリクエストを送信する事ができます。

詳細ページでHTTPメソッド、URL、ヘッダー、ボディなどを設定していきます。

画像は「Amazon API Gateway APIを使用しています」のチェックを外した状態の詳細ページです。チェックを入れている場合、URLの入力は不要です。

URLを入力した後、ステップ名が自動で「検証 {入力したAPIエンドポイントURL}」と入力されます。

しかし、ステップ名に日本語があるとCloudWatchメトリクスデータ送信時にエラーが発生します。注意してください。

このステップ名は監視するAPIの機能名などわかりやすい形で英語で書くと良いでしょう。

1つ目のステップを作成後、「HTTPリクエストを追加」をクリックすると2つ目のステップを作成できます。複数ステップによる監視は、下記のAWS公式ブログやクラスメソッドさんの記事が参考になります。複数のAPIを監視する際は、1つのカナリーにまとめたほうが料金が安くなるのでおすすめです。

参考:Amazon CloudWatch Synthetics を使用して 複数ステップの API を監視する

参考:CloudWatch Synthetics で 1つの Canary だけで複数のエンドポイントをモニタリングしてみた | DevelopersIO

 

コード例

HTTPリクエストを作成すると、スクリプトエディタの内容が作成したHTTPリクエストの内容に合わせて自動的に変更されます。

当記事執筆時点(2022年10月)の最新ランタイムバージョンである、syn–nodejs-puppeteer-3.7だと、下記の通りになります。このエディタの内容をコピーして、Node.jsのファイルを作成してください。

var synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();


const apiCanaryBlueprint = async function () {


    syntheticsConfiguration.setConfig({
        restrictedHeaders: [], // Value of these headers will be redacted from logs and reports
        restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports
    });
    
    // Handle validation for positive scenario
    const validateSuccessful = async function(res) {
        return new Promise((resolve, reject) => {
            if (res.statusCode < 200 || res.statusCode > 299) {
                throw res.statusCode + ' ' + res.statusMessage;
            }
     
            let responseBody = '';
            res.on('data', (d) => {
                responseBody += d;
            });
     
            res.on('end', () => {
                // Add validation on 'responseBody' here if required.
                resolve();
            });
        });
    };
    


    // Set request option for ステップ名
    let requestOptionsStep1 = {
        hostname: 'API GatewayのURL',
        method: 'GET',
        path: '/APIのステージ',
        port: '443',
        protocol: 'https:',
        body: "",
        headers: {}
    };
    requestOptionsStep1['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep1['headers']['User-Agent']].join(' ');
   
 // Set step config option for ステップ名    let stepConfig1 = {         includeRequestHeaders: true,         includeResponseHeaders: true,         includeRequestBody: true,         includeResponseBody: true,         continueOnHttpStepFailure: true     };     await synthetics.executeHttpStep('ステップ名', requestOptionsStep1, validateSuccessful, stepConfig1)      }; exports.handler = async () => {     return await apiCanaryBlueprint(); };

青字にした  handler  がハンドラーの関数名にあたります。

syn-nodejs-puppeteer-3.4以降のランタイムを使用する場合は、関数名を任意の名前に変更できます。変更する際は、スクリプトエントリポイントも併せて変更してください。

syn-nodejs-puppeteer-3.4 より前のランタイムを使用している場合は、functionName は handler である必要があります。syn-nodejs-puppeteer-3.4 以降を使用している場合、ハンドラーとして任意の関数名を選択できます。

引用:Node.js Canary スクリプトの記述 - Amazon CloudWatch

赤字にした定数  apiCanaryBlueprint  の箇所が、カナリーが実行する処理内容となります。APIを叩く前に何かしらの処理をする場合は、HTTPリクエストより前の箇所に処理内容を追加してください。実際の案件では、もう少し込み入った処理を追記しております。

当記事で作成したSynthetics

上手く設定する事が出来れば、以下の画像のように動作するかと思います。

 

またCloudWatchのメトリクスで、

「カスタム名前空間」→「CloudWatch Synthetics」→「Canaryとステップ別」と行けば

ステップごとのSuccessPercentが見れます。

SuccessPercentが100より低くなると通知するアラームを、ステップごとに作成すると良いでしょう。

最後に

案件で構築したSyntheticsに関しましては、サービスがリリース前なので実際に運用していくのはこれからです。

定期的に業務処理を監視する事によって、素早い障害検知に役立つ事だと信じております。またCloudFormationで構築したことにより、管理しやすくなっていると思います。

これからSyntheticsを構築しようとしている方に、当記事が少しでもお役に立てたのなら幸いです。