🌐

AWS CDK S3+CloudFront+Route53+ACM ウェブサイト構築 TypeScript #3

に公開

はじめに

OS:Windows11
ターミナル:PowerShell

以前、AWS-CDKでS3とCloudFrontを使用した静的ウェブサイトを構築しました。

AWS CDK S3静的ウェブサイトホスティング構築 TypeScript #1

AWS CDK S3+CloudFrontウェブサイト構築 Typescript #2

ここまででHTTPSによるセキュアなコンテンツ配信ができるようになりましたが、

AWSが自動設定するドメインとなっています。

実案件でウェブサイトをこのまま公開するわけにはいかないので

今回はRoute53とAWS Certificate Managerを使用して独自ドメインによるコンテンツ配信を構築していきます。

手順

今回はRoute53のホストゾーンで証明書を認証しACMからCloudFrontに証明書を渡します。

外部サービスで取得したドメインを使用しますので以下のような手順で構築をしていきます。

  1. ドメイン取得
  2. Route53 HostedZoneの構築
  3. ネームサーバー設定
  4. ACMの構築
  5. CloudFrontの更新

ドメイン取得

はじめに独自ドメインを使用します。

Route53上で購入可能ですが、学習用のドメインに料金を払いたくないので期間限定で無料ドメイン取得が可能なサービスを利用します。

私はお名前.comで初年度無料のドメインを購入しました。

構築

us-east-1リージョンでのcdk bootstrap

CloudFrontで独自ドメインの証明書を使用するのですが、CloudFrontにはus-east-1のACM証明書しか使用するとができません。

https://docs.aws.amazon.com/acm/latest/userguide/acm-overview.html

Amazon CloudFront で ACM 証明書を使用するには、米国東部(バージニア北部)リージョンで証明書をリクエストまたはインポートする必要があります。このリージョンで CloudFront ディストリビューションに関連付けられた ACM 証明書は、そのディストリビューションに設定されているすべての地理的ロケーションに配布されます。

なので証明書を管理するAWSリソースであるACMはus-east-1に構築する必要がありますし、

cdk bootstrapをus-east-1に対して行っていない場合は実行する必要があります。

cdk bootstrap aws://<accountID>/us-east-1

cdkのappはリージョンを跨いで複数のStackをデプロイ可能ですがStackはCloudFormationのスタックに対応しているのでリージョン毎に分ける必要あります。

なので今回ACM用のStackを記述するためのファイルを作成します。

また、Route53を先行してデプロイしますのでRoute53用のファイルも作成します。

(実際は1ファイル内に複数Stackを記述可能ですが見辛いので分けるのが無難)

New-Item -ItemType File -Path .\lib\certificate-stack.ts
New-Item -ItemType File -Path .\lib\route53-stack.ts

Route 53

route53-stack.tsにRoute53リソースを記述していきます。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.HostedZone.html

外部サービスで取得したドメインは外部サービスが管理しているのでそのドメイン管理をAWSに委任させるためパブリックホストゾーンを作成します。

lib/route53-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
    aws_route53 as route53,
} from "aws-cdk-lib";

// 外部から受け取るプロパティをまとめるPropsを作成
interface Route53StackProps extends cdk.StackProps {
    domainName: string;
}

export class Route53Stack extends cdk.Stack {
        // ホストゾーンを別ファイルから参照できるように設定
    public readonly hostedZone: route53.IHostedZone;

    constructor(scope: Construct, id: string, props: Route53StackProps) {
        super(scope, id, props);

        const hostedZone = new route53.HostedZone(this, 'HostedZone', {
                // 取得したドメイン名でホストゾーンを作成
            zoneName: props.domainName,           
        });
        // このホストゾーンは別ファイルから参照できるように定義
        this.hostedZone = hostedZone;
    }
}

このroute53-stack.tsファイルはプロジェクト初期化のタイミングで作成されたファイルではないのでapp側で記述する必要があります。

bin\s3_cloudfront.ts
import * as cdk from 'aws-cdk-lib';
import { S3CloudfrontStack } from '../lib/s3_cloudfront-stack';
import { Route53Stack } from '../lib/route53-stack';

const app = new cdk.App();
const domainName = app.node.tryGetContext('domainName');
const account = process.env.CDK_DEFAULT_ACCOUNT;

const route53Stack = new Route53Stack(app, 'Route53Stack', {
    env: {
        region: 'ap-northeast-1',
        account: account
    },
    // propsに渡す値を指定
    domainName: domainName,
})

また、コード上で取得したドメイン名を指定する必要があるのですが

いちいちハードコードする訳にもいかないのでtryGetContextでcdk.jsonから取得しています。

そのためcdk.json内に今回取得したドメインを記述する必要があります。

cdk.json
  "context": {
    "domainName": "yourdomain.com",
    以下省略

これでRoute53をデプロイする準備ができましたので先にデプロイしていきます。

デプロイ-1

作成したスタックをAWSにデプロイしていきます。

今回複数のスタックを作成していきますが、これらスタックは異なるリージョンにリソースを構築します。

Route53Stack
・Route53(グローバル)

CertificateStack
・ACM(us-east-1)

S3CloudfrontStack
・CloudFront(グローバル)
・S3などの他リソース(ap-northeast-1)

本来CDKではリソースのデプロイ順を制御することができますがリージョンを跨ぐ場合はできないようです。

また、依存関係を自動的に解決してくれますが確実にデプロイするためにスタック単位で個別のデプロイを行います。

> cdk list
Route53Stack
CertificateStack
S3CloudfrontStack

>cdk deploy Route53Stack

今回はACMの証明書認証をRoute53で行うためネームサーバーをRoute53に設定する必要があります。

そのため最初にRoute53Stackのみデプロイします。

デプロイが完了したら[Route 53]→[ホストゾーン]から作成したホストゾーンを選択し

タイプ:NSとなっているレコードの値を確認します。

この値をこの後のネームサーバー設定に使用します。

ネームサーバーの変更

外部サービスでドメインを取得した場合はドメインとサーバーを紐づけるネームサーバーがそのサービス側で管理されている場合があります。

これはこれで問題ないのですが、今回はAWS上で管理したいので必要に応じて設定を行います。

私がドメインを取得したお名前.comのネームサーバー管理方法はこちらをご参照ください。

設定が反映されているかは以下コマンドで確認できます。

>nslookup -type=NS yourdomain.com
サーバー:  RT-AX55-BEC8
Address:  192.168.50.1

# Route53の値が出ているのを確認
権限のない回答:
yourdomain.com     nameserver = ns-***.awsdns-**.co.uk
yourdomain.com     nameserver = ns-***.awsdns-**.com
yourdomain.com     nameserver = ns-***.awsdns-**.net
yourdomain.com     nameserver = ns-***.awsdns-**.org

AWS Certificate Manager(ACM)

CloudFrontに独自ドメインを使用させるためにはドメインの証明書を取得・管理する必要があります。

証明書をRoute53に対してリクエストし発行された証明書を保管・管理するためにACMを構築します。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_certificatemanager.Certificate.html

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_certificatemanager.CertificateValidation.html

lib\certificate-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import {
    aws_certificatemanager as acm,
    aws_route53 as route53,
} from "aws-cdk-lib";

interface CertificateStackProps extends cdk.StackProps {
    domainName: string;
    hostedZone: route53.IHostedZone;
}

export class CertificateStack extends cdk.Stack {
    public readonly certificateArn: string;

    constructor(scope: Construct, id: string, props: CertificateStackProps) {
        super(scope, id, props);

        const certificate = new acm.Certificate(this, 'WebSiteCert', {
            certificateName: `website-cert`,
                // 証明書を発行してもらうドメインを指定
            domainName: props.domainName,
            subjectAlternativeNames: [`*.${props.domainName}`,],
                // 認証方式をDNSに指定(今回ドメインが1つだけなのでfromDnsを使用)
            validation: acm.CertificateValidation.fromDns(props.hostedZone),
        });
            // 証明書のARNをCloudFrontのスタックで使用するための設定
        this.certificateArn = certificate.certificateArn;
    }
}

CloudFront

ACMの準備ができたのでCloudFront側のコードも更新していきます。

CloudFrontディストリビューションで独自ドメインを使用するように指定し

Route53にレコードを作成しドメインとディストリビューションを関連付けます。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.ARecord.html

lib\s3_cloudfront-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  aws_s3 as s3,
  aws_s3_deployment as s3_deployment,
  aws_cloudfront as cloudfront,
  aws_cloudfront_origins as cloudfront_origins,
  aws_route53 as route53,
  aws_route53_targets as route53_targets,
  aws_certificatemanager as acm,
} from 'aws-cdk-lib';

// 外部から取得するパラメータを定義
interface S3CloudfrontStackProps extends cdk.StackProps {
  domainName: string;
  certificateArn: string;
  hostedZone: route53.IHostedZone;
}

export class S3CloudfrontStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: S3CloudfrontStackProps) {
    super(scope, id, props);
    
    const s3Bucket = new s3.Bucket(this, 'WebSiteBucket', {
      bucketName: `s3-bucket-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`,
      autoDeleteObjects: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      publicReadAccess: false,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    // ACM証明書の読み込み(別スタックから)
    const certificate = acm.Certificate.fromCertificateArn(this, 'WebSiteCert', props.certificateArn);

    const oac = new cloudfront.S3OriginAccessControl(this, 'OAC', {
      originAccessControlName: `${id}-oac`,
      signing: cloudfront.Signing.SIGV4_ALWAYS,
    }) 

    const distribution = new cloudfront.Distribution(this, 'Distribution', {
      defaultBehavior: {
        origin: cloudfront_origins.S3BucketOrigin.withOriginAccessControl(
          s3Bucket,{
            originAccessLevels: [cloudfront.AccessLevel.READ],
          },
        ),          
      },
      defaultRootObject: 'index.html',
        // 独自ドメインの指定
      domainNames: [props.domainName],
        // 証明書の指定
      certificate: certificate,
    });

    // Route53レコード作成
    new route53.ARecord(this, 'AliasRecord', {
        // ホストゾーン指定
      zone: props.hostedZone,
        // レコード指定
      recordName: '',
        // ターゲット指定
      target: route53.RecordTarget.fromAlias(new route53_targets.CloudFrontTarget(distribution)),
    });

    new s3_deployment.BucketDeployment(this, 'DeploymentIndex', {
      destinationBucket: s3Bucket,
      sources: [s3_deployment.Source.asset('./website')],
      destinationKeyPrefix: '',
    });

    const url = new cdk.CfnOutput(this, 'URL', {
      value: `https://${props.domainName}`,
      description: 'Website URL'
    });
  }
}

各Stackのコードを作成したので

Appのコードも修正していきます。

bin\s3_cloudfront.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { S3CloudfrontStack } from '../lib/s3_cloudfront-stack';
import { CertificateStack } from '../lib/certificate-stack';
import { Route53Stack } from '../lib/route53-stack';

const app = new cdk.App();
const domainName = app.node.tryGetContext('domainName');
const account = process.env.CDK_DEFAULT_ACCOUNT;

const route53Stack = new Route53Stack(app, 'Route53Stack', {
    env: {
        region: 'ap-northeast-1',
        account: account
    },
    // propsに渡す値を指定
    domainName: domainName,
})

const certificateStack = new CertificateStack(app, 'CertificateStack', {
    env: { 
        region: 'us-east-1',
        account: account,
    },
    domainName: domainName,
    hostedZone: route53Stack.hostedZone,
    crossRegionReferences: true,
});

new S3CloudfrontStack(app, 'S3CloudfrontStack', {
    env: {
        region: 'ap-northeast-1',
        account: account,
    },
    domainName: domainName,
    certificateArn: certificateStack.certificateArn,
    hostedZone: route53Stack.hostedZone,
    crossRegionReferences: true,
});

デプロイ-2

ネームサーバーの設定変更が完了したらACMとCloudFrontをデプロイしていきます。

ACMで取得した証明書を使用するのでACM→CloudFrontの順番でデプロイします。

cdk deploy CertificateStack
cdk deploy S3CloudfrontStack

デプロイが完了したらhttps://<ドメイン名>でウェブページが表示されるのを確認しましょう。

おわり

今回は取得したドメインをRoute53とACMを使って証明しCloudFrontで配信するコンテンツのドメインを設定しました。

CloudFrontを使用したコンテンツ配信についてはここまででひと段落とし

今後はセキュリティやモニタリング設定を追加していきたいと思います。

それでは、ここまでお読みいただきありがとうございました。

ソースコード

https://github.com/michinoku-YoRHa/awscdk-s3-cloudfront/tree/v3-cloudfront-custom-domain

モニタリングかセキュリティ

Discussion