[AWS CDK] API Gateway REST API で Preflight request のレスポンスヘッダに Access-Control-Max-Age を設定する
こんにちは、CX事業本部 Delivery部の若槻です。
Web ブラウザなどのクライアントから CORS(Cross-Origin Resource Sharing) リクエストが行われる際には、Preflight request によりサーバー側が CORS リクエストを受け付けるかどうかの判断が行われます。
この Preflight request は、通常は本来行いたいリクエストの前に毎回行われるのですが、サーバー側で Access-Control-Max-Age レスポンスヘッダを設定することで、Preflight request の結果をブラウザ側にキャッシュさせることができます。
今回は、Amazon API Gateway Rest API で preflight request のレスポンスヘッダに Access-Control-Max-Age を AWS CDK で設定する方法を確認してみました。
試してみた
Access-Control-Max-Age を設定しない場合
AWS CDK で REST API に CORS Preflight を構成する場合は LambdaRestApi(RestApi のextends)の defaultCorsPreflightOptions
を設定します。
import { aws_apigateway, } from "aws-cdk-lib"; // 中略 export class ApiConstruct extends Construct { constructor(scope: Construct, id: string, props: ApiConstructProps) { super(scope, id); // 中略 new aws_apigateway.LambdaRestApi(this, "RestApi", { handler: restApiFunc, defaultCorsPreflightOptions: { allowOrigins: ["https://example.com"], allowMethods: ["POST"], allowHeaders: ["authorization", "content-type"], }, deployOptions: { stageName: "v1", }, defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer }, }); } }
CDK コード全文はこちら
import { aws_lambda, aws_lambda_nodejs, aws_dynamodb, aws_apigateway, aws_cognito, } from "aws-cdk-lib"; import { Construct } from "constructs"; interface ApiConstructProps { companiesTable: aws_dynamodb.Table; cognitoUserPool: aws_cognito.UserPool; } export class ApiConstruct extends Construct { constructor(scope: Construct, id: string, props: ApiConstructProps) { super(scope, id); const { companiesTable, cognitoUserPool } = props; const restApiFunc = new aws_lambda_nodejs.NodejsFunction( this, "RestApiFunc", { architecture: aws_lambda.Architecture.ARM_64, entry: "../server/src/lambda/handlers/api-gateway/rest-api-router.ts", environment: { COMPANIES_TABLE_NAME: companiesTable.tableName, }, }, ); companiesTable.grantReadWriteData(restApiFunc); const cognitoUserPoolsAuthorizer = new aws_apigateway.CognitoUserPoolsAuthorizer( this, "CognitoUserPoolsAuthorizer", { cognitoUserPools: [cognitoUserPool], }, ); new aws_apigateway.LambdaRestApi(this, "RestApi", { handler: restApiFunc, defaultCorsPreflightOptions: { allowOrigins: ["https://example.com"], allowMethods: ["POST"], allowHeaders: ["authorization", "content-type"], }, deployOptions: { stageName: "v1", }, defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer }, }); } }
前述の CDK コードをデプロイして構築した API に対して、次のようなテストコードで Preflight request を確認をしみます。CORS 用のレスポンスヘッダーのうち Access-Control-Max-Age のみ 設定されていない ことを確認しています。
import Axios, { AxiosResponse, AxiosError } from "axios"; import { test, describe, expect, beforeAll } from "vitest"; const REST_API_ENDPOINT = process.env.REST_API_ENDPOINT || ""; describe("/companies", (): void => { describe("OPTIONS", (): void => { let response: AxiosResponse | undefined = undefined; beforeAll(async (): Promise<void> => { response = await Axios({ url: `${REST_API_ENDPOINT}/companies`, method: "OPTIONS", headers: { "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "authorization", Origin: "https://example.com", }, }).catch((err: AxiosError) => err.response); }); test("Preflight Request が成功すること", (): void => { expect(response!.status).toBe(204); expect(response!.headers).toMatchObject({ "access-control-allow-headers": "authorization,content-type", "access-control-allow-methods": "POST", "access-control-allow-origin": "https://example.com", }); expect(response!.headers["access-control-max-age"]).toBeUndefined(); }); }); });
テストを実行すると、Access-Control-Max-Age が未設定であることが確認できました。
$ npx vitest run packages/e2e/api/companies_api.test.ts RUN v0.34.3 /Users/wakatsuki.ryuta/projects/cm-cxlabs/cdk-lambda-dynamodb-sample ✓ packages/e2e/api/companies_api.test.ts (1) 975ms ✓ /companies (1) 974ms ✓ OPTIONS (1) 974ms ✓ Preflight Request が成功すること Test Files 1 passed (1) Tests 1 passed (1) Start at 02:47:46 Duration 2.08s (transform 130ms, setup 0ms, collect 177ms, tests 975ms, environment 0ms, prepare 144ms)
Access-Control-Max-Age を設定してみる
Access-Control-Max-Age を設定するには、LambdaRestApi の defaultCorsPreflightOptions
に maxAge
を設定します。下記では 5 分(300 秒)で設定しています。
import { aws_apigateway, Duration, } from "aws-cdk-lib"; // 中略 export class ApiConstruct extends Construct { constructor(scope: Construct, id: string, props: ApiConstructProps) { super(scope, id); // 中略 new aws_apigateway.LambdaRestApi(this, "RestApi", { handler: restApiFunc, defaultCorsPreflightOptions: { allowOrigins: ["https://example.com"], allowMethods: ["POST"], allowHeaders: ["authorization", "content-type"], maxAge: Duration.minutes(5), }, deployOptions: { stageName: "v1", }, defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer }, }); } }
CDK コード全文はこちら
import { aws_lambda, aws_lambda_nodejs, aws_dynamodb, aws_apigateway, aws_cognito, Duration, } from "aws-cdk-lib"; import { Construct } from "constructs"; interface ApiConstructProps { companiesTable: aws_dynamodb.Table; cognitoUserPool: aws_cognito.UserPool; } export class ApiConstruct extends Construct { constructor(scope: Construct, id: string, props: ApiConstructProps) { super(scope, id); const { companiesTable, cognitoUserPool } = props; const restApiFunc = new aws_lambda_nodejs.NodejsFunction( this, "RestApiFunc", { architecture: aws_lambda.Architecture.ARM_64, entry: "../server/src/lambda/handlers/api-gateway/rest-api-router.ts", environment: { COMPANIES_TABLE_NAME: companiesTable.tableName, }, }, ); companiesTable.grantReadWriteData(restApiFunc); const cognitoUserPoolsAuthorizer = new aws_apigateway.CognitoUserPoolsAuthorizer( this, "CognitoUserPoolsAuthorizer", { cognitoUserPools: [cognitoUserPool], }, ); new aws_apigateway.LambdaRestApi(this, "RestApi", { handler: restApiFunc, defaultCorsPreflightOptions: { allowOrigins: ["https://example.com"], allowMethods: ["POST"], allowHeaders: ["authorization", "content-type"], maxAge: Duration.seconds(300), }, deployOptions: { stageName: "v1", }, defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer }, }); } }
前述の CDK コードをデプロイしたら、次のようなテストコードで Preflight request を確認をしみます。今度はレスポンスヘッダーに Access-Control-Max-Age が 設定されている ことを確認しています。
import Axios, { AxiosResponse, AxiosError } from "axios"; import { test, describe, expect, beforeAll } from "vitest"; const REST_API_ENDPOINT = process.env.REST_API_ENDPOINT || ""; describe("/companies", (): void => { describe("OPTIONS", (): void => { let response: AxiosResponse | undefined = undefined; beforeAll(async (): Promise<void> => { response = await Axios({ url: `${REST_API_ENDPOINT}/companies`, method: "OPTIONS", headers: { "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "authorization", Origin: "https://example.com", }, }).catch((err: AxiosError) => err.response); }); test("Preflight Request が成功すること", (): void => { expect(response!.status).toBe(204); expect(response!.headers).toMatchObject({ "access-control-allow-headers": "authorization,content-type", "access-control-allow-methods": "POST", "access-control-allow-origin": "https://example.com", "access-control-max-age": "300", }); }); }); });
テストを実行すると、Access-Control-Max-Age が設定されていることが確認できました。
$ npx vitest run packages/e2e/api/companies_api.test.ts RUN v0.34.3 /Users/wakatsuki.ryuta/projects/cm-cxlabs/cdk-lambda-dynamodb-sample ✓ packages/e2e/api/companies_api.test.ts (1) ✓ /companies (1) ✓ OPTIONS (1) ✓ Preflight Request が成功すること Test Files 1 passed (1) Tests 1 passed (1) Start at 02:30:30 Duration 525ms (transform 74ms, setup 0ms, collect 93ms, tests 184ms, environment 0ms, prepare 68ms)
おわりに
今回は、AWS CDK で REST API に CORS Preflight を構成する際に、Access-Control-Max-Age を設定する方法を確認しました。
このレスポンスヘッダーが無くても CORS は動作するので忘れがちですが、パフォーマンス向上のためのチューニングの一環として是非設定しておきましょう。
以上