こんにちは、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
を設定します。
lib/constructs/api.ts
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 コード全文はこちら
lib/constructs/api.ts
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 のみ 設定されていない ことを確認しています。
companies_api.test.ts
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 秒)で設定しています。
lib/constructs/api.ts
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 コード全文はこちら
lib/constructs/api.ts
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 が 設定されている ことを確認しています。
companies_api.test.ts
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 は動作するので忘れがちですが、パフォーマンス向上のためのチューニングの一環として是非設定しておきましょう。
以上