AWS CDK で API Gateway の 4XX/5XX エラーを表示する CloudWatch Dashboard をつくってみた

はじめに

おひさしぶりです。アノテーション の中野です。

7 月からクラスメソッドの CX 事業本部 Delivery 部 LINE グループの保守運用のお手伝いをしながらアノテーション内の新規業務立ち上げにチャレンジしています。

今回は、CloudWatch 上で API Gateway のエラー状況をすぐに見られるようなダッシュボードを CDK で作ってみました。
以下の内容を表示できるようにしました。

  • API Gateway の特定のステージで 4XX/5XX が発生
  • CloudWatch Dashboard に 4XX/5XX の指定時間内の合計値を数値ウィジェットに表示
  • CloudWatch Dashboard に 4XX/5XX の指定時間内の推移を線ウィジェットに表示

環境情報

  • node 16.17.1
  • npm 8.19.2
  • typescript 4.8.4
  • aws-cdk 2.49.1
  • aws-cdk-lib 2.49.1
  • constructs 10.1.146
  • aws-lambda 1.0.7

完成形 ダッシュボード

表示する完成形のダッシュボードです。

ダッシュボードに表示するメトリクスとして、API Gateway の 4XXError と 5XXError を利用します。
どちらのメトリクスもデフォルトで CloudWatch へ取得されます。

CDK ソースコード

コード全体

すぐコード読んで動かしたいという方向けです。
作成したソースコードは、こちらのリポジトリに push していますので、クローンして自身の環境で試してみてください。

CDK アプリ

App には、API 専用の Stack(apiStack) と CloudWatch Dashboard 専用の Stack(cloudwatchStack) を定義します。
また、CloudWatch 用 Stack が API 専用 Stack に依存するようにしています。
後述しますが、apiStack でエクスポートしたパラメータを cloudwatchStack で利用したいため、このように定義しています。

bin/app.ts

import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { ApiStack } from '../lib/aws-cdk-api-stack'
import { CloudWatchStack } from '../lib/aws-cdk-cloudwatch-dashboard'

const app = new cdk.App()

const apiStack = new ApiStack(app, 'ApiStack')

const cloudwatchStack = new CloudWatchStack(app, 'CloudWatchStack')

cloudwatchStack.addDependency(apiStack)

API スタック

次に、ApiStack のクラス内で Lambda と API Gateway を定義します。
API Gateway は、GET メソッドで /text のパスにリクエストを受け付けられるようにしています。ステージは、dev としました。

また、前述の通り、new cdk.CfnOutput() の箇所でスタック間でパラメータを参照できるようにしています。
理由は、ApiStack で作成された API Gateway のステージ名と API 名のメトリクス値を、CloudWatchStack で参照してダッシュボードに設定するためです。
このような参照のことをクロススタック参照といいますが、以下のブログがわかりやすかったので、詳しくはこちらを読んでみてください。

lib/aws-cdk-api-stack.ts

import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'
import { Construct } from 'constructs'

export class ApiStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props)

        const handler = new lambdaNodejs.NodejsFunction(this, 'ErrorHandler', {
            runtime: lambda.Runtime.NODEJS_16_X,
            entry: 'lambda/handler.ts',
        })

        const api = new apigateway.RestApi(this, 'api_error_endpoint', {
            restApiName: 'api_error_endpoint',
            deployOptions: {
                stageName: 'dev',
            },
        })

        api.root
            .addResource('text')
            .addMethod('GET', new apigateway.LambdaIntegration(handler))

        new cdk.CfnOutput(this, 'apigatewayStageNameOutPut', {
            value: api.deploymentStage.stageName,
            exportName: 'StageName',
        })

        new cdk.CfnOutput(this, 'apigatewayNameOutPut', {
            value: api.restApiName,
            exportName: 'ApiName',
        })
    }
}

CloudWatch スタック

ダッシュボードは、aws-cloudwatch モジュールの SingleValueWidget と GraphWidget を利用して、数値ウィジェット線ウィジェットを作成します。
14-15行目で、ApiStack でエクスポートした値を参照しています。

SingleValueWidget のsetperiodtotimerangetrue に設定することで、時間範囲で指定すると全時間範囲の値を表示してくれるようになります。

lib/aws-cdk-cloudwatch-dashboard.ts

import * as cdk from 'aws-cdk-lib'
import * as cloudwatch from 'aws-cloudwatch'
import {
    GraphWidget,
    Metric,
    SingleValueWidget,
} from 'aws-cdk-lib/aws-cloudwatch'
import { Construct } from 'constructs'

export class CloudWatchStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props)

        const stageName = cdk.Fn.importValue('StageName')
        const apiName = cdk.Fn.importValue('ApiName')

        const single4XXWidget = new SingleValueWidget({
            title: 'APIGW 4XX Error Count',
            height: 4,
            width: 12,
            setPeriodToTimeRange: true,
            metrics: [
                new Metric({
                    namespace: 'AWS/ApiGateway',
                    metricName: '4XXError',
                    dimensionsMap: {
                        Stage: stageName,
                        ApiName: apiName,
                    },
                    statistic: 'Sum',
                }),
            ],
        })

        const single5XXWidget = new SingleValueWidget({
            title: 'APIGW 5XX Error Count',
            height: 4,
            width: 12,
            setPeriodToTimeRange: true,
            metrics: [
                new Metric({
                    namespace: 'AWS/ApiGateway',
                    metricName: '5XXError',
                    dimensionsMap: {
                        Stage: stageName,
                        ApiName: apiName,
                    },
                    statistic: 'Sum',
                }),
            ],
        })

        const graph4XXWidget = new GraphWidget({
            title: 'APIGW 4XX Error',
            width: 24,
            left: [
                new Metric({
                    namespace: 'AWS/ApiGateway',
                    metricName: '4XXError',
                    dimensionsMap: {
                        Stage: stageName,
                        ApiName: apiName,
                    },
                    statistic: 'Sum',
                }),
            ],
        })

        const graph5XXWidget = new GraphWidget({
            title: 'APIGW 5XX Error',
            width: 24,
            left: [
                new Metric({
                    namespace: 'AWS/ApiGateway',
                    metricName: '5XXError',
                    dimensionsMap: {
                        Stage: stageName,
                        ApiName: apiName,
                    },
                    statistic: 'Sum',
                }),
            ],
        })

        new cloudwatch.Dashboard(this, 'APIDashboard', {
            dashboardName: 'APIDashBoard',
            periodOverride: cloudwatch.PeriodOverride.AUTO,
            widgets: [
                [single4XXWidget, single5XXWidget],
                [graph4XXWidget],
                [graph5XXWidget],
            ],
        })
    }
}

上記コードを実装しているときに、以下 2 つの箇所で躓きました。

躓いた箇所1: ウィジェットの配置がうまくいかない

ダッシュボード内にウィジェットを配置する際に、うまく表示できませんでした(各ウィジェットが左端にすべて配置される)。
88-92行目の widgets のプロパティで配置を配列で指定することでいい感じに収まってくれました。
以下の Issue の報告を参考に設定してみました。

[cloudwatch] position of the widget in a dashboard cannot be changed · Issue #8938 · aws/aws-cdk

躓いた箇所2: Metric のdimensionsMap に指定する値が不明

aws-cloudwatch モジュールの Metric をつかってメトリクス値を取得しようとしたところ、dimensinsMap に指定すべき値がわかりませんでした(VSCode で赤下線がでる….泣)。
CDK の GitHub Issue などに他の人の報告がないか調査しましたが、具体的な指定方法はわかりませんでした。
一方、以下の CDK 本家のコードを読んでいると、dimensinsMap には、ApiName と StageName を指定すればよさそうなことがわかりました。

もし公式のドキュメントを読んで直ぐに設定値がわからない場合は、CDK のコードを探してみても良さそうです。

aws-cdk/stage.ts at main · aws/aws-cdk

Lambda コード

API Gateway のバックエンドに設定する Lambda のコードです。

API にリクエストがあった際に、意図的に 4XX 系のエラーと 5XX 系のエラーを出すために、400 と 500 のステータスコードの配列を用意します。
配列はリクエストの度、毎回ランダムに選ばれて変数に設定されます。
Lambda の戻り値に status を渡して、API Gateway でエラーとなり 4XXError や 5XXError のメトリクスを取得します。

lambda/handler.ts

import {
    APIGatewayEventRequestContext,
    APIGatewayProxyResult,
} from 'aws-lambda'

exports.handler = async (
    event: APIGatewayEventRequestContext
): Promise<APIGatewayProxyResult> {
    console.log(JSON.stringify(event, undefined, 2))

    // 擬似的に毎回ランダムなステータスを返す
    const statusArray: Array<number> = [400, 500]
    const status = statusArray[Math.floor(Math.random() * statusArray.length)]

    return {
        statusCode: status,
        headers: { 'Content-Type': 'text/plain' },
        body: `Error: ${event.path} StatusCode: ${status}\n`,
    }
}

デプロイ

それでは、ビルドして AWS 環境へデプロイします。

$ npm run build
$ npm run test
$ cdk deploy --all

デプロイ完了後に作成された API Gateway のエンドポイントへリクエストを送ります。
ブラウザ、もしくは、curl コマンドにてリクエストを送ります。

5XX 系の場合

$ curl -i https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/text
HTTP/2 500 
content-type: text/plain
content-length: 29
date: Fri, 16 Sep 2022 07:09:31 GMT
x-amzn-requestid: 3025215f-xxxx-xxxx-xxxxx-xxxxxxxx
x-amz-apigw-id: Yixxxxxxxxxxx
x-amzn-trace-id: Root=1-6xxxxxxxxxxxxxxxx
x-cache: Error from cloudfront
via: 1.1 1xxxxxxxxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: Kxxxxxxx
x-amz-cf-id: J_sxxxxxxxxxxxxxxxxx

Error: /text StatusCode: 500

4XX 系の場合

$ curl -i https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/text
HTTP/2 400 
content-type: text/plain
content-length: 29
date: Fri, 16 Sep 2022 07:11:15 GMT
x-amzn-requestid: fb4304b5-xxxx-xxxx-xxxxx-xxxxxxxx
x-amz-apigw-id: Yixxxxxxxxxxx
x-amzn-trace-id: Root=1-6xxxxxxxxxxxxxxxx
x-cache: Error from cloudfront
via: 1.1 1xxxxxxxxxxxx.cloudfront.net (CloudFront)
x-amz-cf-pop: Nxxxxxxx
x-amz-cf-id: yMxxxxxxxxxxxxxxxxxx

Error: /text StatusCode: 400

このように、API Gateway のバックエンドに設定した Lambda がランダムにステータスコードを返却します。
しばらく時間をおいて、以下のようなメトリクスが表示されることを確認します。

さいごに

今回の内容は、単純に API のエラーを CloudWatch Dashboard に表示するだけの内容でした。
API のエラーが即時に見れると API の運用している担当者も楽になるし、開発者もわかりやすいのではないでしょうか。
次回は、こちらの内容を応用して、CDK で 4XX/5XX のエラー率をダッシュボード上に出すための解説記事を書きます。

参考情報

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社 WEB サイトをご覧ください。