[AWS CDK] API Gateway の CloudWatch logs ログ出力用ロールを明示的に作成して、ログが出力されなくなる事故をあらかじめ防ぐ

2024.06.11

こんにちは、製造ビジネステクノロジー部の若槻です。

Amazon API Gateway では、REST API の アクセスログ および 実行ログ の 2 種類のログ出力を設定できます。

このログ出力を有効化する際、API Gateway が CloudWatch logs にログを出力するためのロールが意図しない設定の場合には、ログが出力されない事故が発生する可能性があります。

今回は、AWS CDKAPI Gateway の CloudWatch logs ログ出力用ロールを明示的に設定して、ログが出力されなくなる事故をあらかじめ防ぐようにしてみました。

こんな時には API Gateway のログ出力がされなくなる

まず API Gateway および AWS CDK の仕様として、REST API をスタックデプロイにより作成すると CloudWatch logs ログ出力用ロールの作成と API Gateway のアカウントおよびリージョン単位の設定への適用が暗黙的に行われます。ロールにアタッチされるのは AmazonAPIGatewayPushToCloudWatchLogs という AWS マネージドポリシーです。

そして厄介なのが、その CloudWatch logs ログ出力用ロールを作成および設定したスタックが削除された場合、ロールは削除されるが API Gateway の設定は残ったままとなることです。

API Gateway にリクエストを送信しているにも関わらずログが出力されない場合に、下記の設定画面を確認すると以前に削除したスタックにより作成されたロールが残っていた、なんて事故が起き得るのです。

CloudWatch logs ログ出力用ロールを明示的に作成して設定する

CDK コード

API Gateway への CloudWatch logs ログ出力用ロールの適用はCfnAccountコンストラクトクラスを使って行います。

lib/cdk-sample-stack.ts

import {
  aws_lambda,
  aws_lambda_nodejs,
  aws_apigateway,
  aws_logs,
  aws_iam,
  Stack,
  RemovalPolicy,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    /**
     * Lambda 関数を作成
     */
    const handler = new aws_lambda_nodejs.NodejsFunction(this, 'Handler', {
      architecture: aws_lambda.Architecture.ARM_64,
      runtime: aws_lambda.Runtime.NODEJS_20_X,
      entry: 'src/rest-api-router.ts',
      logGroup: new aws_logs.LogGroup(this, 'LambdaLogGroup', {
        removalPolicy: RemovalPolicy.DESTROY,
      }),
      tracing: aws_lambda.Tracing.ACTIVE, // AWS X-Ray によるトレースを有効化
    });

    /**
     * REST API アクセスログのロググループを作成
     */
    const restApiAccessLogGroup = new aws_logs.LogGroup(
      this,
      'RestApiAccessLogGroup',
      {
        removalPolicy: RemovalPolicy.DESTROY,
      }
    );

    /**
     * REST API を作成
     */
    new aws_apigateway.LambdaRestApi(this, 'LambdaRestApi', {
      handler,
      deployOptions: {
        // アクセスログの有効化
        accessLogDestination: new aws_apigateway.LogGroupLogDestination(
          restApiAccessLogGroup
        ),
        accessLogFormat:
          aws_apigateway.AccessLogFormat.jsonWithStandardFields(),

        // 実行ログの有効化
        dataTraceEnabled: true,
        loggingLevel: aws_apigateway.MethodLoggingLevel.INFO,
      },
    });

    /**
     * API Gateway の CloudWatch logs 出力用ロールを設定
     *
     * MEMO: アカウントおよびリージョン横断で使われるリソースおよび設定のため、独立した CDK スタックでの管理を推奨
     */
    const ApiGatewayCloudWatchLogRole = new aws_iam.Role(
      this,
      'ApiGatewayCloudWatchLogRole',
      {
        assumedBy: new aws_iam.ServicePrincipal('apigateway.amazonaws.com'),
      }
    );
    ApiGatewayCloudWatchLogRole.addManagedPolicy(
      aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
        'service-role/AmazonAPIGatewayPushToCloudWatchLogs'
      )
    );
    new aws_apigateway.CfnAccount(this, 'ApiGatewayCfnAccount', {
      cloudWatchRoleArn: ApiGatewayCloudWatchLogRole.roleArn,
    });
  }
}

Lambda ハンドラーコードも一応載せておきます。

クリックして展開

src/rest-api-router.ts

import serverlessExpress from '@codegenie/serverless-express';
import express, { Request, Response } from 'express';

const app = express();
app.use(express.json());

app.get('/companies', (_: Request, res: Response): void => {
  const now = new Date().toISOString();
  console.log(now);
  res.status(200).send({ message: 'Hello, World!' });
});

export const handler = serverlessExpress({ app });

動作確認

前述のスタックを CDK デプロイして作成した RESt API に対してリクエストを送信してみます。

$ curl -X GET ${REST_API_ENDPOINT}companies                                          
{"message":"Hello, World!"}

アクセスログが出力されています。

{
    "requestId": "a9661bd3-2bdd-4758-9103-850c3247c0d2",
    "ip": "104.28.211.105",
    "user": "-",
    "caller": "-",
    "requestTime": "10/Jun/2024:15:01:42 +0000",
    "httpMethod": "GET",
    "resourcePath": "/{proxy+}",
    "status": "200",
    "protocol": "HTTP/1.1",
    "responseLength": "27"
}

実行ログも出力されています。

おわりに

AWS CDK で API Gateway の CloudWatch logs ログ出力用ロールを明示的に設定して、ログが出力されなくなる事故をあらかじめ防ぐようにしてみました。

今回調査するまでこの仕様を今まで知らなかったため、REST API のログ出力の検証をする際にまんまとハマってしまいました。皆さんもお気をつけ下さい。

参考

以上