AWS CDKでReactをCloudFront+S3にデプロイする


:::note warn
この記事は AWS CDK v1 を使用しています。
2022/09/03 現在は AWS CDK v2 がリリースされており、内容が古くなっている可能性があります。
:::


## 本記事でやること

- AWS 上に [CloudFront + S3 の構成](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/getting-started-secure-static-website-cloudformation-template.html) で静的ウェブサイトをホスティングします
- [create-react-app](https://create-react-app.dev/) で作成した React のアプリケーションを題材にします
- [AWS CDK](https://docs.aws.amazon.com/cdk/index.html) を使用して、CloudFront や S3 をプロビジョニングし、アプリケーションのデプロイまで行います <br> AWS CDK を使用すると、アプリの S3 へのアップロードから CloudFront の Invalidation まで、デプロイ作業がすべて自動化できます！すごい！

![cloudfront.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529254/89c85445-a468-4f5b-5a38-30014579f5f5.png)

いいからコードを！という方はこちらをご参照ください。

https://github.com/somq14/react-aws-cdk

## 手順

### AWS のアクセスキーの設定

aws コマンドを使用して、アクセスキーなどの AWS のアクセス情報を設定しておきます。
これにより、AWS CDK が AWS のサービスへアクセスできるようになります。
アクセスキーは AWS マネジメントコンソールの IAM から発行できます。

```bash
% aws configure --profile AWSプロファイル名
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX # アクセスキーを指定
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXX # シークレットアクセスキーを指定
Default region name [None]: ap-northeast-1 # お好みで。ここでは東京リージョンを指定
Default output format [None]: json
```

ここで指定した AWS プロファイル名は後程利用します。

### React のアプリケーション作成

デプロイ対象の React アプリケーションを作成します。
こちらに変更を加えていきます。

```bash
% yarn create react-app --template typescript react-aws-cdk
% cd react-aws-cdk
```

### 依存ライブラリのインストール

必要なライブラリをインストールします。

```bash
yarn add -D \
  @aws-cdk/aws-cloudfront \
  @aws-cdk/aws-cloudfront-origins \
  @aws-cdk/aws-s3 \
  @aws-cdk/aws-s3-deployment \
  @aws-cdk/core \
  aws-cdk \
  ts-node \
  cross-env
```

package.json に以下のような差分が生じます。
`@aws-cdk/*` のパッケージはバージョンが同じでないと不整合を起こすことがあります。
この場合は大丈夫ですが、あとから依存ライブラリを追加するときは要注意です。

```json:package.json
  "devDependencies": {
    "@aws-cdk/aws-cloudfront": "^1.118.0",
    "@aws-cdk/aws-cloudfront-origins": "^1.118.0",
    "@aws-cdk/aws-s3": "^1.118.0",
    "@aws-cdk/aws-s3-deployment": "^1.118.0",
    "@aws-cdk/core": "^1.118.0",
    "aws-cdk": "^1.118.0",
    "cross-env": "^7.0.3",
    "ts-node": "^10.2.0"
  }
```

### AWS CDK のセットアップ

AWS CDK を使用するには、事前セットアップが必要です。
AWS CDK が使用する S3 バケットを作成するなどが行われるようです。

```bash
% yarn cdk bootstrap --profile AWSプロファイル名 aws://AWSアカウントID/リージョン名

# 例えば、以下のとき
# AWSプロファイル名: hoge
# AWSアカウントID: 999999999999
# リージョン名: ap-northeast-1
% yarn cdk bootstrap --profile hoge aws://999999999999/ap-northeast-1
```

### AWS CDK のコードを作成

TypeScript の設定ファイルを作成します。
React と AWS CDK では異なる設定が必要なため、ここで作成しておきます。

```ts:./deployment/tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "moduleResolution": "node"
  }
}
```

`app.ts` は AWS CDK のエントリーポイントです。
AWS CloudFormation のスタック名を環境変数で指定できるようにしています。
作成するリソースの定義は後述の `spa-stack.ts` に記述していきます。

```ts:./deployment/app.ts
import * as cdk from "@aws-cdk/core";
import { exit } from "process";
import { SpaStack } from "./spa-stack";

const stackName = process.env["STACK_NAME"];
if (!stackName) {
  console.error("missing environment variable STACK_NAME");
  exit(1);
}

const app = new cdk.App();
new SpaStack(app, stackName, {});
```

`spa-stack.ts` に作成するリソースを定義します。
設定内容はコードのコメントを参照してください。

```ts:./deployment/spa-stack.ts
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import { Duration } from "@aws-cdk/core";
import * as origins from "@aws-cdk/aws-cloudfront-origins";

export type SpaStackProps = {};

export class SpaStack extends cdk.Stack {
  constructor(
    scope: cdk.Construct,
    id: string,
    props?: cdk.StackProps & SpaStackProps
  ) {
    super(scope, id, props);

    // 配信するコンテンツを配置する S3 バケット
    const originBucket = new s3.Bucket(this, "OriginBucket");

    // CloudFront のログを配置する S3 バケット
    const logBucket = new s3.Bucket(this, "LogBucket");

    const distribution = new cloudfront.Distribution(this, "Distribution", {
      defaultBehavior: {
        // 配信するコンテンツを指定する
        origin: new origins.S3Origin(originBucket),
        // HTTP でアクセスされたら HTTPS へリダイレクトする
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      // ログの出力先を設定する
      logBucket: logBucket,

      // ルートへのアクセスに対して返却するコンテンツを設定する
      defaultRootObject: "index.html",
      errorResponses: [
        // S3 に指定されたファイルが存在しないとき、S3 は 403 エラーを返すが、
        // CloudFront により エラーを index.html の返却に置き換える
        // (SPA では、どの画面についても、index.html により表示を行うため)
        // https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-website-cloudfront-error-403/
        {
          httpStatus: 403,
          responseHttpStatus: 200,
          responsePagePath: "/index.html",
          ttl: Duration.minutes(5),
        },
      ],

      // 日本を含む地域のエッジロケーションからコンテンツを配信する
      // https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PriceClass.html
      priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
    });

    // build ディレクトリを S3 にアップロードして、アプリケーションをデプロイする
    new s3deploy.BucketDeployment(this, "Deployment", {
      sources: [s3deploy.Source.asset("./build")],
      destinationBucket: originBucket,
      // CloudFront のキャッシュを削除し、コンテンツを最新化する
      distribution: distribution,
    });
  }
}
```

### npm scripts にデプロイコマンドを追加し、実行する

`package.json` の scripts にデプロイ用のコマンドを追加します。
デプロイは `cdk deploy` コマンドであり、 オプション `-a` でエントリーポイントを指定します。
作成する CloudFormation スタック名は環境変数から取得することにしているので、コマンド内に指定しています。

```diff:package.json
   "start": "react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "eject": "react-scripts eject",
+    "deploy": "cross-env STACK_NAME=react-aws-cdk-dev cdk -a \"ts-node ./deployment/app.ts\" deploy"
   },
```

アプリケーションをビルドし、次にデプロイします。

```bash
% yarn build
% yarn deploy --profile AWSプロファイル名
```

### デプロイ結果の確認

AWS マネジメントコンソールで CloudFront にアクセスし、URL を確認します。
![deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529254/3258b126-3070-4c82-a7ab-3a5f104a2959.png)

## おわりに

初めて AWS CDK を使いましたが、おもしろいです！
CloudFormation と比較すると…

- プログラミング言語を使用するから、for も if も使い放題 <br> → 柔軟性・拡張性が高い
- 各リソースのプロパティに AWS 様おすすめのデフォルト値が割り当てられている <br> → 記述負荷が低く、意識せずともベストプラクティスに従える
- s3-deployment などのリソース作成ではなく、ワークフローを担う機能を利用できる

今回は実施しませんでしたが、以下ができるとよりよそうです。

- Lambda@Edge や CloudFront Functions で レスポンスに HTTP ヘッダーを付与する
- ACM, Route53 と連携させて、ドメイン設定を行う

## 参考

作成にあたり公式ドキュメントの他、DevelopersIO様を参考にさせていただきました。
本記事では、比較的新しいクラスの `cloudfront.Distribution` を利用して、記述をブラッシュアップしました。
明示的に OAI (Origin Access Identity)を記述しなくても、しっかりと OAI が生成されていい感じです。

https://dev.classmethod.jp/articles/aws-cdk-deploy-react/

以上です。
ありがとうございました。