概要
Route53とCloudFrontの紐づけとSSL証明書の発行をCDKを使って行ったメモ や AWS CDK v2を使ってSPA用のCloudFrontをデプロイしたメモ では、Route53とCloudFrontの紐づけのための証明書取得にDnsValidatedCertificate
クラスを使っていたが、deprecatedになってしまった。
そのままCertificate
を書いたら、regionの指定が無くて困ってしまった。(CloudFrontの証明書はus-east-1
に必要)
クロスリージョンは下記で行えそうだったので、試してみた。
環境
- cdk: 2.79.1
ソースコード
.envは変更なし
PROJECT_ID=hoge
ROOT_DOMAIN=hoge.huga.co.jp
DEPLOY_DOMAIN=www.aaa.hoge.huga.co.jp
TAG_PROJECT_NAME=sample
BUCKET_NAME=aws-sample-cloundfront-s3-bucket
SUB_DIR_PATH_BUILDER=sample-builder
#!/usr/bin/env node
/* eslint-disable turbo/no-undeclared-env-vars */
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import * as dotenv from "dotenv";
import { RinnneCircleFrontCdkStack } from "../lib/RinnneCircleFrontCdkStack";
import { HostedZone } from "aws-cdk-lib/aws-route53";
import {
Certificate,
CertificateValidation,
} from "aws-cdk-lib/aws-certificatemanager";
dotenv.config();
const envList = [
"PROJECT_ID",
"ROOT_DOMAIN",
"DEPLOY_DOMAIN",
"TAG_PROJECT_NAME",
"BUCKET_NAME",
"SUB_DIR_PATH_BUILDER",
] as const;
for (const key of envList) {
if (!process.env[key]) throw new Error(`please add ${key} to .env`);
}
const processEnv = process.env as Record<(typeof envList)[number], string>;
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
const domainName = processEnv.ROOT_DOMAIN;
+ // 証明書はus-east-1リージョンで作成する必要がある
+ const usStack = new cdk.Stack(app, `${processEnv.PROJECT_ID}-FrontCdkUsStack`, {
+ env: { ...env, region: "us-east-1" },
+ crossRegionReferences: true,
+ });
+ const zone = HostedZone.fromLookup(usStack, `${domainName}-hosted-zone`, {
+ domainName: processEnv.ROOT_DOMAIN,
+ });
+ const cert = new Certificate(usStack, `${domainName}-certificate`, {
+ domainName: processEnv.DEPLOY_DOMAIN,
+ validation: CertificateValidation.fromDns(zone),
+ });
new RinnneCircleFrontCdkStack(app, `${processEnv.PROJECT_ID}-FrontCdkStack`, {
bucketName: processEnv.BUCKET_NAME,
identityName: `${processEnv.PROJECT_ID}-origin-access-identity-to-s3-bucket`,
defaultCachePolicyName: `${processEnv.PROJECT_ID}-cache-policy-default`,
functionName: `${processEnv.PROJECT_ID}-lambda-edge-ogp`,
distributionName: `${processEnv.PROJECT_ID}-distribution-cloudfront`,
rootDomain: processEnv.ROOT_DOMAIN,
deployDomain: processEnv.DEPLOY_DOMAIN,
projectNameTag: processEnv.TAG_PROJECT_NAME,
subDirectoryPath: {
builder: processEnv.SUB_DIR_PATH_BUILDER,
},
env,
+ crossRegionReferences: true,
+ zone,
+ cert,
});
lib/RinnneCircleFrontCdkStack.tsはDnsValidatedCertificateを削除して、zoneとcertを受け取るようにした以外変更なし
import * as cdk from "aws-cdk-lib";
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
import {
CachePolicy,
FunctionCode,
OriginAccessIdentity,
Function,
AllowedMethods,
ViewerProtocolPolicy,
CacheHeaderBehavior,
Distribution,
PriceClass,
FunctionEventType,
ResponseHeadersPolicy,
HeadersFrameOption,
HeadersReferrerPolicy,
IDistribution,
} from "aws-cdk-lib/aws-cloudfront";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import {
CanonicalUserPrincipal,
Effect,
PolicyStatement,
} from "aws-cdk-lib/aws-iam";
import {
ARecord,
AaaaRecord,
IHostedZone,
RecordTarget,
} from "aws-cdk-lib/aws-route53";
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
import { BlockPublicAccess, Bucket, IBucket } from "aws-cdk-lib/aws-s3";
import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
type SubDirectoryPath = {
builder: string;
};
interface Props extends cdk.StackProps {
bucketName: string;
identityName: string;
defaultCachePolicyName: string;
functionName: string;
distributionName: string;
rootDomain: string;
deployDomain: string;
projectNameTag: string;
subDirectoryPath: SubDirectoryPath;
+ zone: IHostedZone;
+ cert: Certificate;
}
export class RinnneCircleFrontCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
// CloudFront オリジン用のS3バケットを作成
const bucket = this.createS3(props.bucketName);
// CloudFront で設定する オリジンアクセスアイデンティティ を作成
const identity = this.createIdentity(bucket, props.identityName);
// S3バケットポリシーで、CloudFrontのオリジンアクセスアイデンティティを許可
this.createPolicy(bucket, identity);
- const zone = this.findRoute53HostedZone(props.rootDomain)
- const cert = this.createTLSCertificate(props.deployDomain, zone)
// CloudFrontディストリビューションを作成
const distribution = this.createCloudFront(
bucket,
identity,
props.cert,
props,
);
// // 指定したディレクトリをデプロイ
this.deployS3(
bucket,
distribution,
"../apps/front/dist",
props.subDirectoryPath.builder,
props.bucketName,
);
// route53 の CloudFrontに紐づくレコード作成
this.addRoute53Records(props.zone, props.deployDomain, distribution);
// 確認用にCloudFrontのURLに整形して出力
new cdk.CfnOutput(this, `${props.distributionName}-top-url`, {
value: `https://${distribution.distributionDomainName}/${props.subDirectoryPath.builder}`,
});
cdk.Tags.of(this).add("Project", props.projectNameTag);
}
- private findRoute53HostedZone(rootDomain: string) {
- return route53.HostedZone.fromLookup(this, `${rootDomain}-hosted-zone`, {
- domainName: rootDomain,
- })
- }
- private createTLSCertificate(
- deployDomain: string,
- hostedZone: route53.IHostedZone,
- ) {
- return new certManager.DnsValidatedCertificate(
- this,
- `${deployDomain}-certificate`,
- {
- domainName: deployDomain,
- hostedZone, // DNS 認証に Route 53 のホストゾーンを使う
- region: 'us-east-1', // 必ず us-east-1 リージョン
- validation: certManager.CertificateValidation.fromDns(),
- },
- )
- }
private createS3(bucketName: string) {
const bucket = new Bucket(this, bucketName, {
bucketName,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
// デフォルト = accessControl: BucketAccessControl.PRIVATE,
});
return bucket;
}
private createIdentity(bucket: Bucket, identityName: string) {
const identity = new OriginAccessIdentity(this, identityName, {
comment: `${bucket.bucketName} access identity`,
});
return identity;
}
private createPolicy(bucket: Bucket, identity: OriginAccessIdentity) {
const myBucketPolicy = new PolicyStatement({
effect: Effect.ALLOW,
actions: ["s3:GetObject", "s3:ListBucket"],
principals: [
new CanonicalUserPrincipal(
identity.cloudFrontOriginAccessIdentityS3CanonicalUserId,
),
],
resources: [bucket.bucketArn + "/*", bucket.bucketArn],
});
bucket.addToResourcePolicy(myBucketPolicy);
}
private createCloudFront(
bucket: Bucket,
identity: OriginAccessIdentity,
cert: Certificate,
props: {
defaultCachePolicyName: string;
distributionName: string;
deployDomain: string;
subDirectoryPath: SubDirectoryPath;
},
) {
const { defaultCachePolicyName, distributionName, deployDomain } = props;
const defaultPolicyOption = {
cachePolicyName: defaultCachePolicyName,
comment: "輪廻サークルポリシー",
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
};
const myCachePolicy = new CachePolicy(
this,
defaultCachePolicyName,
defaultPolicyOption,
);
const origin = new S3Origin(bucket, {
originAccessIdentity: identity,
});
const spaRoutingFunction = new Function(this, "SpaRoutingFunction", {
functionName: `rinne-circle-SpaRoutingFunction`,
// 拡張子が含まれないURLはSPAファイルにリダイレクト
code: FunctionCode.fromInline(`
function handler(event) {
var request = event.request;
if(request.uri.includes('.')){
return request;
}
if (request.uri.startsWith('/${props.subDirectoryPath.builder}')) {
request.uri = '/${props.subDirectoryPath.builder}/index.html';
} else {
request.uri = '/rinne-circle/index.html';
}
return request;
}
`),
});
cdk.Tags.of(spaRoutingFunction).add("Service", "Cloud Front Function");
const responseHeadersPolicy = this.createResponseHeadersPolicy();
const additionalBehaviors = {
"data/*": {
origin,
allowedMethods: AllowedMethods.ALLOW_ALL,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: new CachePolicy(
this,
`${distributionName}-data-cache-policy`,
{
cachePolicyName: `${distributionName}-data-cache-cache-policy`,
comment: "CloudFront データ部用ポリシー",
defaultTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.seconds(10),
headerBehavior: CacheHeaderBehavior.allowList("content-type"),
},
),
},
};
const d = new Distribution(this, distributionName, {
comment: "RinneCircle",
defaultRootObject: "/index.html",
priceClass: PriceClass.PRICE_CLASS_200,
defaultBehavior: {
origin,
cachePolicy: myCachePolicy,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
responseHeadersPolicy,
functionAssociations: [
{
eventType: FunctionEventType.VIEWER_REQUEST,
function: spaRoutingFunction,
},
],
},
additionalBehaviors,
certificate: cert,
domainNames: [deployDomain],
});
cdk.Tags.of(d).add("Service", "Cloud Front");
return d;
}
private createResponseHeadersPolicy() {
const responseHeadersPolicy = new ResponseHeadersPolicy(
this,
"ResponseHeadersPolicy",
{
securityHeadersBehavior: {
contentTypeOptions: { override: true },
frameOptions: {
frameOption: HeadersFrameOption.DENY,
override: true,
},
referrerPolicy: {
referrerPolicy: HeadersReferrerPolicy.SAME_ORIGIN,
override: true,
},
strictTransportSecurity: {
accessControlMaxAge: cdk.Duration.seconds(63072000),
includeSubdomains: true,
preload: true,
override: true,
},
xssProtection: {
protection: true,
modeBlock: true,
override: true,
},
},
customHeadersBehavior: {
customHeaders: [
{
header: "Cache-Control",
value: "no-cache",
override: true,
},
{
header: "pragma",
value: "no-cache",
override: true,
},
{
header: "server",
value: "",
override: true,
},
],
},
},
);
return responseHeadersPolicy;
}
private addRoute53Records(
zone: IHostedZone,
deployDomain: string,
cf: Distribution,
) {
const propsForRoute53Records = {
zone,
recordName: deployDomain,
target: RecordTarget.fromAlias(new CloudFrontTarget(cf)),
};
new ARecord(this, "ARecord", propsForRoute53Records);
new AaaaRecord(this, "AaaaRecord", propsForRoute53Records);
}
private deployS3(
siteBucket: IBucket,
distribution: IDistribution,
sourcePath: string,
bucketName: string,
basepath: string,
) {
new BucketDeployment(this, `${bucketName}-deploy-with-invalidation`, {
sources: [Source.asset(sourcePath)],
destinationBucket: siteBucket,
distribution,
distributionPaths: [`/${basepath}/*`],
destinationKeyPrefix: basepath,
});
}
}
確認
デプロイページを確認できた。
https://d11wfkhmuej13d.cloudfront.net/rinne-circle-builder/
Route53も効いていることを確認。
参考
現在はRemoteOutputsを使わなくてもよくなっている模様。
AWS CDK(cdk-remote-stack)でACMとCloudFrontのクロスリージョン参照を実装する
RemoteOutputsのおかげで好きなリージョンCloudFrontにWebACLを紐付けれる話
WS Solutions ConstructsでWAF+CloudFront+S3構成を作る
https://speakerdeck.com/watany/aws-solutions-constructs-dejing-de-hosuteingugale-ninaru?slide=20