[aws-cdk]S3 + CloudFrontで静的コンテンツ配信する

S3 + CloudFrontの構成で静的コンテンツ配信する構成をCDKで作ってみた。

YAMLから開放されて非常に良い・・・CDKはいいぞ！:relaxed:


# 前提
- aws-cdkの実行環境は構築済み
    - 今回は [TypeScript](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html) で構築してます
- Route53の設定済み
- ACMで証明書を発行済み

# 準備
不足しているパッケージを追加します。

```sh
$ npm i -D @aws-cdk/aws-s3 @aws-cdk/aws-cloudfront @aws-cdk/aws-route53-targets
```

# コード

## ディレクトリ構成
今回は以下の構成で動かすことを想定しています。ｌ

```
./
├ bin/
│   └ cdk.ts
└ lib/
    └ sample/
        └ index.ts
```

## bin/cdk.ts
`sample/index.ts`のpropsに値を渡してあげるだけです。

```js
import * as cdk from '@aws-cdk/core';
import { SampleStack } from '../lib/sample';

const app = new cdk.App();

new SampleStack(app, "sample-stack", {
    // 公開したいドメイン名を指定（バケット名にもなる）
    domain: "sample.xxxxxxx.com",
    // 設定済みのRoute53の情報
    route53HostedZoneId: "xxxxxxxxx",
    route53HostedZoneName: "xxxxxxxxxxx",
    // 作成済みの証明書のARN
    acmCertificateArn: "arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    // CloudFrontに設定するTTL
    cfTtl: cdk.Duration.minutes(5),
});
```

## Stack

```js
import * as core from "@aws-cdk/core";
import * as s3 from "@aws-cdk/aws-s3";
import * as cf from "@aws-cdk/aws-cloudfront";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";

export interface SampleStackProps extends core.StackProps {
    readonly domain: string;
    readonly route53HostedZoneId: string;
    readonly route53HostedZoneName: string;
    readonly acmCertificateArn: string;
    readonly cfTtl: core.Duration;
}

export class SampleStack extends core.Stack {
    constructor(scope: core.Construct, id: string, props: SampleStackProps) {
        super(scope, id, props);

        const bucket = new s3.Bucket(this, "S3Bucket", {
            bucketName: props.domain,
            accessControl: s3.BucketAccessControl.PRIVATE,
            removalPolicy: core.RemovalPolicy.DESTROY,
        });

        const identity = new cf.OriginAccessIdentity(this, "OriginAccessIdentity", {
            comment: `${bucket.bucketName} access identity`,
        });

        const distribution = new cf.CloudFrontWebDistribution(this, "Distribution", {
            enableIpV6: true,
            httpVersion: cf.HttpVersion.HTTP2,
            viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
            viewerCertificate: {
                aliases: [props.domain],
                props: {
                    acmCertificateArn: props.acmCertificateArn,
                    sslSupportMethod: cf.SSLMethod.SNI,
                    minimumProtocolVersion: "TLSv1.2_2019",
                },
            },
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: bucket,
                        originAccessIdentity: identity,
                    },
                    behaviors: [
                        {
                            isDefaultBehavior: true,
                            allowedMethods: cf.CloudFrontAllowedMethods.GET_HEAD,
                            cachedMethods: cf.CloudFrontAllowedCachedMethods.GET_HEAD,
                            defaultTtl: props.cfTtl,
                            maxTtl: props.cfTtl,
                            minTtl: props.cfTtl,
                            forwardedValues: {
                                queryString: false,
                            },
                        },
                    ],
                },
            ],
        });

        new route53.ARecord(this, "Route53ARecordSet", {
            zone: route53.HostedZone.fromHostedZoneAttributes(this, "Route53HostedZone", {
                hostedZoneId: props.route53HostedZoneId,
                zoneName: props.route53HostedZoneName,
            }),
            recordName: props.domain,
            target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
        });
    }
}
```

# うごかす
しばらく待ちます・・・:coffee: 

```sh
$ cdk deploy sample-stack
sample-stack: deploying...
sample-stack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (7/7)

 ✅  sample-stack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/sample-stack/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```

## 確認
とりあえず試したいだけなので、S3にはコンソールから適当なファイルをアップロード済みです

`bin/cdk.ts`で`SampleStack`に渡したドメインを開くと、ページが表示されるかと思います！

```sh
$ open sample.xxxxxxx.com
```

![スクリーンショット 2021-03-01 0.13.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206915/44a5aec6-abde-743e-7c61-0976e40df069.png)

## 破棄
今回作った構成は不要なので消しておきます:bomb: 
バケットの中身は事前に空にしておきます:bomb:

```sh
$ cdk destroy sample-stack
Are you sure you want to delete: sample-stack (y/n)? y
sample-stack: destroying...

 ✅  sample-stack: destroyed
```