AWS CDKを使って、サーバレスにHTTPリダイレクトする環境を作る3つの方法
この記事では、AWS CDKを使って、サーバレスなHTTPリダイレクト環境を構築する3つの方法を説明する。
CDKの基本的な使い方は、次の記事で書いたのでここでは説明しない。
AWS CDK で CloudFront Function を使ったリダイレクト環境を構築する
CloudFrontと関数を実行するサービスを組み合わせてリダイレクトを実現する
リダイレクトを設定する状況としては、Webサーバーの移転などドメインごと引っ越しすることも多いため、構築する環境には独自ドメイン(カスタムドメイン)を設定したい。
AWSが提供するサーバレスなサービスのうち、カスタムドメインとSSL証明書を設定できるサービスにはCloudFrontとAWS API Gatewayがある。
この記事では、CloudFrontと、関数を実行するサービスを組み合わせた、次の3種類の構成をまとめる。
リダイレクトの場合GETメソッドで処理すればよいため、複数のHTTPメソッドで異なる処理を実行できるAPI Gatewayを使う必要はないからだ。
- CloudFront + CloudFront function
- CloudFront + AWS Lambda
- CloudFront + Lambda@Edge
動作を確認した環境
- Node.js v18.12.1
- AWS CDK v2.58.1
構成1:CloudFront + CloudFront Functions
HTTPリダイレクトのような、ヘッダーを付与して返すだけの軽い処理には、この構成が一番向いている。
CDKのソースコード
lib/cf-function-example-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
aws_s3 as s3,
aws_s3_deployment as s3deploy,
aws_cloudfront as cf,
aws_cloudfront_origins as cfo,
aws_iam as iam,
aws_certificatemanager as acm,
} from "aws-cdk-lib";
interface CFFunctionExampleStackProps extends cdk.StackProps {
env: {
region: string;
}
certArn: string;
customDomain: string;
}
export class CFFunctioExampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: CFFunctioExampleStackProps) {
super(scope, id, props);
// CloudFront Function を使う場合は、オリジンに S3 バケットを指定する
const bucket = new s3.Bucket(this, "S3Bucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const identity = new cf.OriginAccessIdentity(this, "OriginAccessIdentity", {
comment: `${bucket.bucketName} access identity`,
});
const bucketPolicyStatement = new iam.PolicyStatement({
actions: ["s3:GetObject"],
effect: iam.Effect.ALLOW,
principals: [identity.grantPrincipal],
resources: [`${bucket.bucketArn}/*`],
});
bucket.addToResourcePolicy(bucketPolicyStatement);
// CloudFront Function を作成する
const cfFunction = new cf.Function(this, "CloudFrontFunction", {
code: cf.FunctionCode.fromFile({
filePath: "src/index.js",
}),
});
const distribution = new cf.Distribution(this, "CloudFrontDistribution", {
comment: "cloudfront distribution",
defaultRootObject: "index.html",
defaultBehavior: {
allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cf.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new cfo.S3Origin(bucket, {
originAccessIdentity: identity,
}),
// CloudFront Function を指定する
functionAssociations: [
{
function: cfFunction,
eventType: cf.FunctionEventType.VIEWER_REQUEST,
},
],
},
priceClass: cf.PriceClass.PRICE_CLASS_100,
certificate: acm.Certificate.fromCertificateArn(
this,
'CustomDomainCertificate',
props.certArn
),
domainNames: [props.customDomain],
});
new s3deploy.BucketDeployment(this, "BucketDeployment", {
sources: [],
destinationBucket: bucket,
distribution: distribution,
distributionPaths: ["/*"],
});
}
}
src/index.js
function handler(event) {
var newUrl = `https://new.example.com`;
var response = {
statusCode: 302,
statusDescription: "Moved Permanently",
headers: { location: { value: newUrl } },
};
return response;
}
bin/cf-function-example.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CFFunctioExampleStack } from '../lib/cf-function-example-stack';
const app = new cdk.App();
const REGION = "ap-northeast-1";
// ドメインの SSL 証明書の Arn を指定する
const CERT = "arn:aws:acm:us-east-1-1:123456789012:certificate/1234abcd-..."
// カスタムドメイン名を指定する
const DOMAIN = "old.example.com";
new CFFunctioExampleStack(app, 'CFFunctioExampleStack', {
env: {
region: REGION,
},
certArn: CERT,
customDomain: DOMAIN,
});
Cloud Functionsが向いていないケース
CloudFront Functionsの最大関数サイズは10KBであるため、JavaScriptのソースコードが大きくなる場合はこの構成が難しくなる。
参照:CloudFront Functions に対する制限
たとえば、あるパスのときはそれに対応するパスへリダイレクトさせたいとする。
このようにリダイレクト先を切り替える場合、大量のURLマッピングが必要だとCloudFront Functionsのサイズ上限に達してしまう。
その場合には、後述の別構成を検討することになる。
また、CloudFront Functionはキャッシュを持たず、リクエストのたびに関数が実行される。
そのためキャッシュのヒット率が高い場合には、キャッシュヒットしていれば関数の処理コストがかからない、後述の構成の方が安く済むこともある。
参照:Lambda 関数URLを実戦投入してシンプルなリダイレクト処理を構築する(コスト編)
構成2:CloudFront + AWS Lambda
CloudFront + AWS Lambdaの構成では、CloudFront Functionの代わりに、AWS Lambdaで関数を実行する。
AWS Lambdaの場合の最大関数サイズは250MBなので、大量のURLマッピングにも耐えうる。
AWS Lambda Function URLs(Lambda関数URL)にはカスタムドメインを設定できないため、Lambda単独では、この記事のユースケースに対応できない。
そのため、Lambda関数URLをオリジンに指定したCloudFrontを配置する必要がある。
CDKのソースコード
lib/lambda-example-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
aws_lambda as lambda,
aws_iam as iam,
aws_cloudfront as cf,
aws_cloudfront_origins as cfo,
aws_certificatemanager as acm,
Fn,
} from "aws-cdk-lib";
interface LambdaExampleStackProps extends cdk.StackProps {
env: {
region: string;
},
certArn: string;
customDomain: string;
}
export class LambdaExampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: LambdaExampleStackProps) {
super(scope, id, props);
const executionRole = new iam.Role(this, "secureLambdaRole", {
roleName: "lambdaSecureExecutionRole",
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaBasicExecutionRole"
),
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess"),
],
});
const lambdaFunction = new lambda.Function(this, "Redirect", {
functionName: "redirect-lambda-function",
runtime: lambda.Runtime.NODEJS_18_X,
code: lambda.AssetCode.fromAsset("src"),
handler: "index.handler",
timeout: cdk.Duration.seconds(300),
role: executionRole,
environment: {
TZ: "Asia/Tokyo",
},
});
// Lambda 関数 URL を発行する
const lambdaUrl = lambdaFunction.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});
new cf.Distribution(this, "CloudFrontDistribution", {
comment: "cloudfront distribution",
defaultBehavior: {
allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cf.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
// CloudFront のオリジンに、発行した Lambda 関数 URL を指定する
origin: new cfo.HttpOrigin(Fn.select(2, Fn.split('/', lambdaUrl.url)))
},
priceClass: cf.PriceClass.PRICE_CLASS_100,
certificate: acm.Certificate.fromCertificateArn(
this,
'CustomDomainCertificate',
props.certArn
),
domainNames: [props.customDomain],
});
}
}
src/index.js
exports.handler = async (_event) => {
const newUrl = "https://new.example.com";
const response = {
statusCode: 302,
statusDescription: "Moved Permanently",
headers: {
Location: newUrl,
},
};
return response;
};
bin/lambda-example.js
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { LambdaExampleStack } from '../lib/lambda-example-stack';
const app = new cdk.App();
const REGION = "ap-northeast-1";
// ドメインの SSL 証明書の Arn を指定する
const CERT = "arn:aws:acm:us-east-1-1:123456789012:certificate/1234abcd-..."
// カスタムドメイン名を指定する
const DOMAIN = "old.example.com";
new LambdaExampleStack(app, 'LambdaExampleStack', {
env: {
region: REGION,
},
certArn: CERT,
customDomain: DOMAIN,
});
構成3:CloudFront + AWS Lambda@Edge acm
AWS Lambda@Edgeは、CloudFrontのエッジロケーションからコードを実行するLambda関数である。
他の2つの構成に比べ、アクセス数がとても大きくなるような場合は + Lambda@Edgeの構成のほうが低コストになることがあるようだった。
参照:CloudFront FunctionsはLambda@Edgeより安い。それ本当?!
CDKのソースコード
lib/lambda-edge-example-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
aws_s3 as s3,
aws_cloudfront as cf,
aws_lambda as lambda,
aws_cloudfront_origins as cfo,
aws_iam as iam,
aws_certificatemanager as acm,
} from "aws-cdk-lib";
interface LambdaEdgeExampleStackProps extends cdk.StackProps {
env: {
region: string;
},
certArn: string;
customDomain: string;
}
export class LambdaEdgeExampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: LambdaEdgeExampleStackProps) {
super(scope, id, props);
// Lambda@Edge を使う場合は、オリジンに S3 バケットを指定する
const bucket = new s3.Bucket(this, "S3Bucket", {
versioned: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const identity = new cf.OriginAccessIdentity(this, "OriginAccessIdentity", {
comment: `${bucket.bucketName} access identity`,
});
const bucketPolicyStatement = new iam.PolicyStatement({
actions: ["s3:GetObject"],
effect: iam.Effect.ALLOW,
principals: [identity.grantPrincipal],
resources: [`${bucket.bucketArn}/*`],
});
bucket.addToResourcePolicy(bucketPolicyStatement);
// Lambda@Edge 関数 を作成する
const edgeFunction = new cf.experimental.EdgeFunction(this, "LambdaEdge", {
code: lambda.Code.fromAsset("src"),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_18_X,
});
new cf.Distribution(this, "CloudFrontDistribution", {
comment: "cloudfront distribution",
defaultBehavior: {
allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cf.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new cfo.S3Origin(bucket, {
originAccessIdentity: identity,
}),
// Lambda@Edge Function を指定する
edgeLambdas: [
{
eventType: cf.LambdaEdgeEventType.ORIGIN_REQUEST,
functionVersion: edgeFunction.currentVersion,
},
],
},
priceClass: cf.PriceClass.PRICE_CLASS_100,
certificate: acm.Certificate.fromCertificateArn(
this,
'CustomDomainCertificate',
props.certArn
),
domainNames: [props.customDomain],
});
}
}
src/index.js
exports.handler = async (_event) => {
const newUrl = "https://new.example.com";
const response = {
statusCode: 302,
statusDescription: "Moved Permanently",
headers: {
location: [
{
key: "Location",
value: newUrl,
},
],
},
};
return response;
};
bin/lambda-edge-example.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { LambdaEdgeExampleStack } from "../lib/lambda-edge-example-stack";
const REGION = "us-east-1";
// ドメインの SSL 証明書の Arn を指定する
const CERT = "arn:aws:acm:us-east-1-1:123456789012:certificate/1234abcd-..."
// カスタムドメイン名を指定する
const DOMAIN = "old.example.com";
const app = new cdk.App();
new LambdaEdgeExampleStack(app, "LambdaEdgeExampleStack", {
env: {
region: REGION,
},
certArn: CERT,
customDomain: DOMAIN,
});
おまけ:Serverless Framework v3を使って、CloudFront + Lambdaの環境を作る
AWS Lambdaの場合、AWS CDKではなくServerless Frameworkを使った構築もできる。
CloudFrontと組み合わせる場合はserverless-api-cloudfrontというプラグインもあるが、Serverless Framework v3には対応していなかった。
そのためCloudFrontを構築するためのCloudFormationテンプレートをresources
プロパティに記述する。
Serverless Frameworkで、Lambda関数URLを使う場合のポイントは、次の2つ。
- Lambda関数URLを発行する
functions.<function name>.url
をtrue
にするfunctions: api: handler: src/index.handler url: true
- CloudFrontのオリジンに、発行したLambda関数URLを指定する
Lambda Function URLのArnを自動で取得するため、CloudFormation組み込み関数を使う。DomainName: !Select [2, !Split ["/", !GetAtt ApiLambdaFunctionUrl.FunctionUrl]]
Serverlessの設定ファイルの詳細は、次のとおり。
Lambda関数の中身は、構成 2のsrc/index.js
と同じなので割愛する。
serverless.yml
service: example
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
region: ap-northeast-1
functions:
api:
handler: src/index.handler
url: true
resources:
Resources:
DistributionForLambda:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: ""
HttpVersion: http2
PriceClass: PriceClass_100
Origins:
-
Id: LambdaFunctionURL
DomainName: !Select [2, !Split ["/", !GetAtt ApiLambdaFunctionUrl.FunctionUrl]]
OriginPath: ""
CustomOriginConfig:
OriginProtocolPolicy: https-only
OriginSSLProtocols:
- "TLSv1.2"
DefaultCacheBehavior:
TargetOriginId: LambdaFunctionURL
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
ForwardedValues:
QueryString: false
Headers:
- Accept
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https