LoginSignup
2
0

More than 1 year has passed since last update.

DnsValidatedCertificateがdeprecatedになったのでCertificateに置き換えたらクロスリージョン対応が必要になったメモ

Last updated at Posted at 2023-05-20

概要

Route53とCloudFrontの紐づけとSSL証明書の発行をCDKを使って行ったメモAWS CDK v2を使ってSPA用のCloudFrontをデプロイしたメモ では、Route53とCloudFrontの紐づけのための証明書取得にDnsValidatedCertificateクラスを使っていたが、deprecatedになってしまった。
そのままCertificateを書いたら、regionの指定が無くて困ってしまった。(CloudFrontの証明書はus-east-1に必要)
クロスリージョンは下記で行えそうだったので、試してみた。

環境

  • cdk: 2.79.1

ソースコード

github - cdk

.envは変更なし
.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
bin/cdk.ts
#!/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を受け取るようにした以外変更なし
lib/RinnneCircleFrontCdkStack.ts
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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0