AWS CDK v2でLambda Function URLのEndpointにカスタムドメイン利用のCloudFrontを経由してアクセスする構成を作る
目次
はじめに
Lambda Function URL の Endpoint にカスタムドメイン利用の CloudFront を経由してアクセスする構成を作ります。 似たような構成として
- Lambda + API Gateway
- Lambda@Edge
というのもありますが、今回はこちらで作ってみます。
ポイント
- Lambda Function URL の Endpoint URL を自動で取得して CloudFront の Origin に設定する(やや面倒)。
- Lambda Function は DynamoDB にアクセスするだけの単純なものとします。
- カスタムドメインの新しい A レコードを作成して、その A レコードを CloudFront の Alias に設定します。
- カスタムドメインは Route53 の Hosted Zone にあるとします。
- SSL 証明書も ACM で発行します。
Version
aws-cdk
:2.46.0
書き方
こんな感じで書きます。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
const NAME = "MyAwesomeResource";
const domainName = "mydomain.example.com";
const cloudFrontHostName = "my-awesome-resource.mydomain.example.com";
export class ExternalMasterStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const { functionUrl, lambdaFunction } = this.createLambdaFunction();
// get token for CloudFront Origin
const lambdaHostName = cdk.Lazy.uncachedString({
produce: (context) => {
const resolved = context.resolve(functionUrl.url);
return { "Fn::Select": [2, { "Fn::Split": ["/", resolved] }] } as any;
},
});
this.createCloudFront(lambdaHostName);
}
// create lambda function
createLambdaFunction(): {
functionUrl: lambda.FunctionUrl;
lambdaFunction: lambda.Function;
} {
// NodejsFunction
const lambdaFunction = new nodejs.NodejsFunction(this, `${NAME}Lambda`, {
functionName: `${NAME}-function`,
entry: "src/lambda/index.ts", // lambda function source code
handler: "handler",
memorySize: 128,
bundling: {
minify: true,
sourceMap: true,
target: "es2020",
externalModules: ["aws-sdk"],
},
runtime: lambda.Runtime.NODEJS_16_X,
});
// add function url
const functionUrl = lambdaFunction.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE, // no auth
cors: {
allowedMethods: [lambda.HttpMethod.GET], // only GET
allowedOrigins: ["*"], // allow all origins for CORS
},
});
// add tags for lambda function
cdk.Tags.of(lambdaFunction).add("Name", `${NAME}Lambda`);
cdk.Tags.of(lambdaFunction).add("Project", NAME);
// allow lambda function to get dynamodb items in tables that start with "my-awesome-resource."
lambdaFunction.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["dynamodb:GetItem"],
resources: [
`arn:aws:dynamodb:${this.region}:${this.account}:table/my-awesome-resource.*`,
],
})
);
return { lambdaFunction, functionUrl };
}
createCloudFront(originHost: string) {
// Hosted Zone
const hostedZoneId = route53.HostedZone.fromLookup(this, "hostedZoneId", {
domainName: domainName,
});
// SSL Certificate
const certificateManagerCertificate = new acm.DnsValidatedCertificate(
this,
"CertificateManagerCertificate",
{
domainName: cloudFrontHostName,
hostedZone: hostedZoneId,
region: "us-east-1",
validation: acm.CertificateValidation.fromDns(),
}
);
// CloudFront
const cloudFront = new cloudfront.CloudFrontWebDistribution(
this,
`${NAME}CloudFront`,
{
comment: `${NAME} CloudFront`,
originConfigs: [
{
behaviors: [
{
isDefaultBehavior: true,
allowedMethods:
cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS, // only GET HEAD OPTIONS
compress: true,
forwardedValues: {
queryString: true, // forward query string
// no cookies
cookies: {
forward: "none",
},
},
},
],
// Lambda Function URL
customOriginSource: {
domainName: originHost,
},
},
],
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PriceClass.html
priceClass: cloudfront.PriceClass.PRICE_CLASS_200, // Price Class 200.
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
certificateManagerCertificate,
{
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
aliases: [cloudFrontHostName],
}
),
}
);
// add tags to CloudFront
cdk.Tags.of(cloudFront).add("Name", `${NAME}CloudFront`);
cdk.Tags.of(cloudFront).add("Project", NAME);
// Route53
new route53.ARecord(this, `${NAME}ARecord`, {
recordName: cloudFrontHostName,
zone: hostedZoneId,
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(cloudFront)
),
});
}
}
さいごに
Lambda@Edge があるのであまりこういう構成は必要ないかもしれませんが、 部分的には参考になるコードもあると思うので、メモ代わりに残しておきます。