Amazon API Gateway のゲートウェイレスポンスのカスタマイズを AWS CDK で実装してみた

Amazon API Gateway のゲートウェイレスポンスのカスタマイズを AWS CDK で実装してみた

Clock Icon2025.05.21

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

Amazon API Gateway のゲートウェイレスポンスは、デフォルトのレスポンス設定のままだとエラー原因が分かりづらいパターンがあり、下記の AWS CDK 実装例のように個別のレスポンスのカスタマイズにより分かりやすくすることが可能です。

https://dev.classmethod.jp/articles/change-default-dateway-response-configuration-for-api-gateway-reat-api/

一方で、下記の実装例のように 4XX および 5XX エラーのゲートウェイレスポンス全体を一括でカスタマイズすることもできます。

https://dev.classmethod.jp/articles/how-to-prevent-cors-error-on-api-level-error-with-api-gateway-gateway-response-in-aws-cdk/

今回は、API Gateway のゲートウェイレスポンスの全体および個別のカスタマイズを両立させる設定を AWS CDK で実装する方法を確認してみました。

やってみた

今回は下記の対応を試してみました。

  • ゲートウェイレスポンスのレスポンスヘッダーを全タイプでカスタマイズする(一律で CORS 対応ヘッダーを追加)
  • ゲートウェイレスポンス Invalid API key のレスポンスボディを個別にカスタマイズ(ボディおよびステータスコードを変更)

CDK コード

結論として、下記のような AWS CDK コードの実装により、Lambda プロキシ統合を設定した API Gateway REST API のゲートウェイレスポンスの全体および個別のカスタマイズを両立させることができました。

lib/main-stack.ts
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as aws_lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";

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

    /**
     * Lambda Function
     */
    const handler = new lambda_nodejs.NodejsFunction(this, "Handler", {
      entry: "src/rest-api-router.ts",
      tracing: aws_lambda.Tracing.ACTIVE, // AWS X-Ray によるトレースを有効化
    });

    /**
     * API Gateway REST API
     */
    const restApi = new apigateway.LambdaRestApi(this, "RestApi", {
      handler,
      deployOptions: {
        //実行ログの設定
        dataTraceEnabled: true,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        // アクセスログの設定
        accessLogDestination: new apigateway.LogGroupLogDestination(
          new cdk.aws_logs.LogGroup(this, "AccessLogGroup", {
            removalPolicy: cdk.RemovalPolicy.DESTROY,
          })
        ),
        tracingEnabled: true, // AWS X-Ray によるトレースを有効化
      },
      defaultMethodOptions: {
        apiKeyRequired: true, // API キー認証を必須にする
      },
    });

    /**
     * API キー認証の設定
     */
    const apiKey = restApi.addApiKey("ApiKey");
    const usagePlan = restApi.addUsagePlan("UsagePlan");
    usagePlan.addApiKey(apiKey);
    usagePlan.addApiStage({
      stage: restApi.deploymentStage,
    });

    /**
     * ゲートウェイレスポンス `Invalid API key` の個別カスタマイズ
     * @see https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-gateway-responses-in-swagger.html
     */
    restApi.addGatewayResponse("InvalidApiKeyResponse", {
      type: apigateway.ResponseType.INVALID_API_KEY,
      // ステータスコードのカスタマイズ(デフォルトの 403 Forbidden から 401 Unauthorized に変更)
      statusCode: "401",
      // ヘッダーのカスタマイズ(CORS 対応)
      responseHeaders: {
        "Access-Control-Allow-Origin": "'*'",
        "Access-Control-Allow-Headers": "'*'",
        "Access-Control-Allow-Methods": "'*'",
      },
      // ボディのカスタマイズ
      templates: {
        // application/json メディアタイプのレスポンスボディを指定
        "application/json": JSON.stringify({
          message: "Invalid API Key",
        }),
      },
    });

    /**
     * 4XX および 5XX エラーのゲートウェイレスポンス全体をカスタマイズ
     */
    restApi.addGatewayResponse("Default4xx", {
      type: apigateway.ResponseType.DEFAULT_4XX,
      // ヘッダーのカスタマイズ(CORS 対応)
      responseHeaders: {
        "Access-Control-Allow-Origin": "'*'",
        "Access-Control-Allow-Headers": "'*'",
        "Access-Control-Allow-Methods": "'*'",
      },
    });
    restApi.addGatewayResponse("Default5xx", {
      type: apigateway.ResponseType.DEFAULT_5XX,
      // ヘッダーのカスタマイズ(CORS 対応)
      responseHeaders: {
        "Access-Control-Allow-Origin": "'*'",
        "Access-Control-Allow-Headers": "'*'",
        "Access-Control-Allow-Methods": "'*'",
      },
    });
  }
}

ポイントとしては、DEFAULT_4XX および DEFAULT_5XX による全ゲートウェイレスポンスタイプのカスタマイズは、INVALID_API_KEY の個別ゲートウェイレスポンスタイプのカスタマイズによりオーバーライドされるため、いずれにも共通のカスタマイズ(今回はヘッダーの CORS 対応)をしたい場合はそれぞれで設定する必要があります。

CDK デプロイを行うと、ゲートウェイレスポンスが想定通りのカスタマイズ設定になっていることが確認できました。

参考までに、Lambda プロキシ統合のハンドラーコードはこちら。
src/rest-api-router.ts
import serverlessExpress from '@codegenie/serverless-express';
import cors from 'cors';
import express, { Request, Response } from 'express';

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

app.get('/companies', (_: Request, res: Response): void => {
  res.status(200).send({ message: 'Hello, world!' });
});

export const handler = serverlessExpress({ app });

動作確認

INVALID_API_KEY となるリクエストを送信

不正な API キー実際のレスポンスを確認すると、ステータスコードは 401 、ボディは {"message":"Invalid API Key"}、ヘッダーが CORS 対応となっていることが確認できました。

$ curl -i https://xewxniez9b.execute-api.ap-northeast-1.amazonaws.com/prod/companies -H "x-api-key: ${INVALID_API_KEY}"
HTTP/2 401
content-type: application/json
content-length: 29
date: Wed, 21 May 2025 12:37:39 GMT
x-amzn-trace-id: Root=1-682dc913-5168612f0d1a21fd64f46cde
x-amzn-requestid: 09856525-4477-4e51-b732-d02bc1b2c335
access-control-allow-origin: *
access-control-allow-headers: Content-Type
x-amzn-errortype: ForbiddenException
x-amz-apigw-id: K6xbJEc2NjMEL4g=
access-control-allow-methods: OPTIONS,GET,POST
x-cache: Error from cloudfront
via: 1.1 3ddb4dce2b89d7a72c4f50520b1e211a.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P6
x-amz-cf-id: 9IsMlk76XLDXkGHo2BnapkV7b6zN87aLACHj1xibQ9WVWGOBaGFVTg==

{"message":"Invalid API Key"}

アクセスログの確認

API Gateway アクセスログを確認すると、メソッドが 403 ステータスを返している(Method completed with status: 403)のに対して、ゲートウェイレスポンスで 401 ステータスに変換されている(Gateway response type: INVALID_API_KEY with status code: 401)ことが確認できました。

timestamp,message
1747754918565,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Extended Request Id: K33iFF7SNjMEW4A=
1747754918565,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Verifying Usage Plan for request: e58ef675-e540-4a74-ae30-f5dc8e720ff5. API Key:  API Stage: xewxniez9b/prod
1747754918565,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Extended Request Id: K33iFF7SNjMEW4A=
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) API Key  not authorized because method 'ANY /{proxy+}' requires API Key and API Key is not associated with a Usage Plan for API Stage xewxniez9b/prod: API Key was required but not present
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Method completed with status: 403
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Gateway response type: INVALID_API_KEY with status code: 401
1747754918567,"(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Gateway response body: {""message"":""Invalid API Key""}"
1747754918567,"(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Gateway response headers: {Access-Control-Allow-Headers=Content-Type, Access-Control-Allow-Origin=*, Access-Control-Allow-Methods=OPTIONS,GET,POST}"
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) X-ray Tracing ID : Root=1-682c9fa6-3c3e69b266bcd4565952f65f
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) API Key  not authorized because method 'ANY /{proxy+}' requires API Key and API Key is not associated with a Usage Plan for API Stage xewxniez9b/prod: API Key was required but not present
1747754918567,(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Gateway response type: INVALID_API_KEY with status code: 401
1747754918567,"(e58ef675-e540-4a74-ae30-f5dc8e720ff5) Gateway response headers: {Access-Control-Allow-Headers=Content-Type, Access-Control-Allow-Origin=*, Access-Control-Allow-Methods=OPTIONS,GET,POST}"

実行ログの確認

API Gateway 実行ログでも、API Gateway によるレスポンスが 401 ステータスになっていることが確認できます。

104.28.211.105 - - [20/May/2025:15:28:38 +0000] "GET /{proxy+} HTTP/1.1" 401 29 e58ef675-e540-4a74-ae30-f5dc8e720ff5

X-Ray トレースの確認

AWS X-Ray のトレースを確認すると、プロキシ統合された Lambda までリクエストは行われず、API Gateway が直接 401 ステータスを返していることが確認できます。

API キー認証が成功した場合のレスポンスを確認

確認の為、正しい API キーを指定してリクエストを送信すると、Lambda からのレスポンスが返ってきました。

curl -i https://xewxniez9b.execute-api.ap-northeast-1.amazonaws.com/prod/companies -H "x-api-key: ${API_KEY}"
HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 27
date: Wed, 21 May 2025 12:39:41 GMT
x-amzn-trace-id: Root=1-682dc98d-5405f06136b7fd3834d74df7
x-amzn-requestid: 735cb302-cb18-4d17-9ce0-3c3713abcd21
access-control-allow-origin: *
x-amzn-remapped-content-length: 27
x-amz-apigw-id: K6xuHF3tNjMEuNg=
etag: W/"1b-tDnArHZ232y12bSoTiMc+/cz4as"
x-powered-by: Express
x-cache: Miss from cloudfront
via: 1.1 1ad587d5c6df10748fe99709f6a85cc6.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P6
x-amz-cf-id: k9yG4PL-SQ_FJZUGWHFLqEm5bEyXNB8uLpTQ2Wpthld-TFlsIsVNjQ==

{"message":"Hello, world!"}%

個別のゲートウェイレスポンスでレスポンスヘッダーの設定をしない場合

念の為、INVALID_API_KEY の個別のゲートウェイレスポンスタイプのヘッダーを設定しない場合は、全体のゲートウェイレスポンスタイプのヘッダーは適用されないことが確認できました。

個別のゲートウェイレスポンスタイプのヘッダーを設定しない場合の CDK コード
lib/main-stack.ts
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as aws_lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";

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

    /**
     * Lambda Function
     */
    const handler = new lambda_nodejs.NodejsFunction(this, "Handler", {
      entry: "src/rest-api-router.ts",
      tracing: aws_lambda.Tracing.ACTIVE, // AWS X-Ray によるトレースを有効化
    });

    /**
     * API Gateway REST API
     */
    const restApi = new apigateway.LambdaRestApi(this, "RestApi", {
      handler,
      deployOptions: {
        //実行ログの設定
        dataTraceEnabled: true,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        // アクセスログの設定
        accessLogDestination: new apigateway.LogGroupLogDestination(
          new cdk.aws_logs.LogGroup(this, "AccessLogGroup", {
            removalPolicy: cdk.RemovalPolicy.DESTROY,
          })
        ),
        tracingEnabled: true, // AWS X-Ray によるトレースを有効化
      },
      defaultMethodOptions: {
        apiKeyRequired: true, // API キー認証を必須にする
      },
    });

    /**
     * API キー認証の設定
     */
    const apiKey = restApi.addApiKey("ApiKey");
    const usagePlan = restApi.addUsagePlan("UsagePlan");
    usagePlan.addApiKey(apiKey);
    usagePlan.addApiStage({
      stage: restApi.deploymentStage,
    });

    /**
     * ゲートウェイレスポンス `Invalid API key` の個別カスタマイズ
     * @see https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-gateway-responses-in-swagger.html
     */
    restApi.addGatewayResponse("InvalidApiKeyResponse", {
      type: apigateway.ResponseType.INVALID_API_KEY,
      // ステータスコードのカスタマイズ(デフォルトの 403 Forbidden から 401 Unauthorized に変更)
      statusCode: "401",
      // ボディのカスタマイズ
      templates: {
        // application/json メディアタイプのレスポンスボディを指定
        "application/json": JSON.stringify({
          message: "Invalid API Key",
        }),
      },
    });

    /**
     * 4XX および 5XX エラーのゲートウェイレスポンス全体をカスタマイズ
     */
    restApi.addGatewayResponse("Default4xx", {
      type: apigateway.ResponseType.DEFAULT_4XX,
      // ヘッダーのカスタマイズ(CORS 対応)
      responseHeaders: {
        "Access-Control-Allow-Origin": "'*'",
        "Access-Control-Allow-Headers": "'*'",
        "Access-Control-Allow-Methods": "'*'",
      },
    });
    restApi.addGatewayResponse("Default5xx", {
      type: apigateway.ResponseType.DEFAULT_5XX,
      // ヘッダーのカスタマイズ(CORS 対応)
      responseHeaders: {
        "Access-Control-Allow-Origin": "'*'",
        "Access-Control-Allow-Headers": "'*'",
        "Access-Control-Allow-Methods": "'*'",
      },
    });
  }
}

上記デプロイ後のゲートウェイレスポンスの設定です。これが意図通りの設定では無い場合は、INVALID_API_KEY のレスポンスヘッダーを設定する必要があります。

おわりに

今回は、API Gateway のゲートウェイレスポンスの全体および個別のカスタマイズを両立させる実装を AWS CDK で行ってみました。

どなたかの参考になれば幸いです。

参考

https://dev.classmethod.jp/articles/amazon-api-gateway-lambda-proxy-integration-timeout-error-response/

以上

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.