Nuxt.js で作成したアプリを CloudFront で配信する

# やること

- GitHub Actions で Nuxt.js で作成したアプリのビルド＆デプロイを行う
  - アプリケーションは（静的な）HTML にビルドする
  - デプロイ先は S3 にする
- CloudFront 経由で配信
  - SSL 証明書は Certificate Manager で管理
  - ドメイン管理は Route 53 を使う
  - WAF は使わない

# 完成版の GitHub Actions

先に完成版の Actions のコードを載せておきます

```yml
name: Deploy

on:
  push:
    branches:
      - 'main'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Cache dependencies
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-v1-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-v1-npm-
            ${{ runner.os }}-v1-
            ${{ runner.os }}-
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run generate
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-duration-seconds: 1200
      - name: Deploy
        env:
          DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
          DISTRIBUTION_ID: ${{ secrets.DISTRIBUTION_ID }}
        run: |
          aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
          aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*'
```

# アプリケーションを静的ビルド

静的サイトホスティングするための Nuxt 側の設定として、以下を`nuxt.config.js`に設定する

```js:nuxt.config.js
{
  target: 'static'
}
```

`npx create-nuxt-app`（TypeScript 利用）で環境構築を行うと、`@nuxt/typescript-runtime`パッケージがインストールされ、`nuxt-ts`というバイナリが使える
`./node_modules/.bin/nuxt-ts generate`を実行すると、ホスティングするとそのままブラウザが実行できる HTML へビルドしてくれる

上記は`npx create-nuxt-app`を使って環境構築した場合は、最初から npm-scripts でエイリアスされている

```json:package.json
{
  "scripts": {
    "generate": "nuxt-ts generate"
  }
}
```

## GitHub Actions で書く

ビルドまでを Actions で書くと以下のようになる（抜粋）

```yml
steps:
  - uses: actions/checkout@v2
  - name: Cache dependencies
    uses: actions/cache@v2
    with:
      path: ~/.npm
      key: ${{ runner.os }}-v1-npm-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-v1-npm-
        ${{ runner.os }}-v1-
        ${{ runner.os }}-
  - name: Install dependencies
    run: npm ci
  - name: Build
    run: npm run generate # nuxt-ts generate
```

# S3 にデプロイ

デプロイ用に IAM ユーザーを作成するが、今回このユーザーには AssumeRole の権限のみをアタッチし、実際に S3 を操作する権限は IAM ロールに持たせる

## S3 バケットを作成

AWS コンソール上でバケットを非公開で作成する
公開設定等はそのまま（デフォルト非公開になっている）で、バケット名だけ入力し、作成する

## IAM ロールを作成

今回は、`--delete`オプションが使える`s3 sync`コマンドを使って S3 にアップロードする
以下の S3 アクションのポリシーをアタッチした IAM ロールを作成する

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:ListBucket", "s3:DeleteObject"],
      "Resource": ["arn:aws:s3:::[バケット名]/*", "arn:aws:s3:::[バケット名]"]
    }
  ]
}
```

## IAM ユーザーを作成

以下の AssumeRole のポリシーをアタッチした IAM ユーザーを作成する

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": ["sts:AssumeRole", "sts:TagSession"],
      "Resource": "arn:aws:iam::[AWSアカウントID]:role/[上記で作ったロール名]"
    }
  ]
}
```

後述するが、Actions で[aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)を使って、AWS 認証を行っている
この中で AssumeRole をリクエストする際にセッションタグを渡しているため、`sts:TagSession`が必要になる

AssumeRole を使うと一時的な認証情報が発行されるが、この時に有効期限付きのセッションが生成される

ただ、`role-skip-session-tagging`というオプションを`true`にすれば、`sts:TagSession`は不要にできると思う

この辺のコードは以下
https://github.com/aws-actions/configure-aws-credentials/blob/master/index.js

## ロールの信頼関係の編集

上記で作ったロールの信頼関係タブから、以下を設定する

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[AWSアカウントID]:user/[上記で作ったユーザー名]"
      },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    }
  ]
}
```

## GitHub Actions で書く

デプロイまでを Actions で書くと以下のようになる（抜粋）

```yml
steps:
  - name: Build
    run: npm run generate # nuxt-ts generate
  # 以下を追加
  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: ap-northeast-1
      role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
      role-duration-seconds: 1200
  - name: Deploy
    env:
      DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
    run: aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
```

### GitHub の Secrets

GitHub リポジトリの`Settings -> Secrets`で以下を設定している

| キー名                | 設定値                     |
| --------------------- | -------------------------- |
| AWS_ACCESS_KEY_ID     | アクセスキー               |
| AWS_SECRET_ACCESS_KEY | シークレットアクセスキー   |
| DEPLOY_BUCKET         | デプロイ先の S3 バケット名 |
| AWS_ROLE_TO_ASSUME    | 上記で作成したロールの ARN |

### `role-duration-seconds`について

ロールの最大セッション時間は、デフォルト 1 時間になっている
これに対して、`configure-aws-credentials`のデフォルトのセッション時間は 6 時間となっている
よって、`role-duration-seconds`オプションを指定し、`configure-aws-credentials`のセッション時間を 1 時間以内にする必要がある
ここを超えるとバリデーションエラーで以下のようなエラーが出る

```
Error: The requested DurationSeconds exceeds the MaxSessionDuration set for this role.
```

# CloudFront の設定

AWS コンソールの CloudFront から Create Distribution をクリック

## Origin Settings

| 設定項目                         | 設定値                       | 備考                                                                                                                                                                                                                                                                                                    |
| -------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Origin Domain Name               | 上記で作成したバケットを選ぶ |                                                                                                                                                                                                                                                                                                         |
| Origin Path                      | -                            |                                                                                                                                                                                                                                                                                                         |
| Enable Origin Shield             | No                           | Edge とオリジン（ここでは S3 バケット）間にキャッシュレイヤーを設置する<br />リージョナルエッジキャッシュよりもオリジン寄りのキャッシュレイヤー<br />リージョナルエッジキャッシュからオリジンへのリクエストを集約できるので、オリジンの負荷を軽減できる<br />グローバル展開しているサービスだと使えそう |
| Origin ID                        | 自動設定される               |                                                                                                                                                                                                                                                                                                         |
| Restrict Bucket Access           | Yes                          |                                                                                                                                                                                                                                                                                                         |
| Origin Access Identity           | Create a New Identity        | CloudFront から S3 バケットへのアクセスを許可するための OAI を作成する                                                                                                                                                                                                                                  |
| Grant Read Permissions on Bucket | Yes, Update Bucket Policy    | S3 バケットに自動的に OAI のバケットポリシーを設定してくれる                                                                                                                                                                                                                                            |
| Origin Connection Attempts       | 3                            | オリジンへの接続試行回数                                                                                                                                                                                                                                                                                |
| Origin Connection Timeout        | 10                           | オリジンへの接続を確立する際のタイムアウト値（秒）                                                                                                                                                                                                                                                      |
| Origin Custom Headers            | -                            | オリジンへのリクエストのカスタムヘッダー                                                                                                                                                                                                                                                                |

### Origin Shield を有効化した場合の構成

Origin Shield を有効化すると、ユーザーからオリジンまでの通信は以下になる

`User -> Edge locations -> Regional edge caches -> Origin Shield -> Origin`

- オリジンへの通信はすべて Origin Shield に集約される
  - 世界中のリージョナルエッジキャッシュがそれぞれオリジンと通信していたのを Origin Shield に集約できる
- キャッシュ効率が良い
  - 世界中のリージョナルエッジキャッシュがそれぞれにキャッシュを保持し、キャッシュがない場合にオリジンへ通信するのに比べて、Origin Shield がある場合は、Origin Shield にキャッシュが集約できるので、キャッシュヒット率が上がる

## Default Cache Behavior Settings

| 設定項目                                                   | 設定値                                       | 備考                                                                                                                                                                                                                                               |
| ---------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Viewer Protocol Policy                                     | HTTPS Only                                   | HTTPS リクエストのみのアクセス可<br/>HTTP リクエストが来た場合は 403 エラー                                                                                                                                                                        |
| Allowed HTTP Methods                                       | GET, HEAD                                    | GET 以外の POST などのリクエストは 403 エラー                                                                                                                                                                                                      |
| Field-level Encryption Config                              | -                                            | HTTPS で POST された時に入力フォームなどフィールドレベルで暗号化するかどうか（今回は GET のみなので不要）                                                                                                                                          |
| Cache and origin request settings                          | Use a cache policy and origin request policy |                                                                                                                                                                                                                                                    |
| Cache Policy                                               | Managed-CachingOptimized                     | キャッシュの Default TTL が 24 時間のやつ                                                                                                                                                                                                          |
| Origin Request Policy                                      | -                                            | オリジンにリクエストできるヘッダー、Cookie、クエリストリングを制限できる                                                                                                                                                                           |
| Smooth Streaming                                           | No                                           |                                                                                                                                                                                                                                                    |
| Restrict Viewer Access (Use Signed URLs or Signed Cookies) | No                                           | アクセスを署名付き URL または署名付き Cookie のみに限定する                                                                                                                                                                                        |
| Compress Objects Automatically                             | Yes                                          | リクエストヘッダーに`Accept-Encoding: br`のように圧縮形式を指定してリクエストすると、CloudFront 側でよしなにコンテンツを圧縮して配信してくれる<br />圧縮されているかの確認は、レスポンスヘッダーに`Content-Encoding: br`などが付与されていたら成功 |
| Lambda Function Associations                               | -                                            | Lambda@Edge 関数の設定                                                                                                                                                                                                                             |
| Enable Real-time Logs                                      | No                                           | リアルタイムログの有効化                                                                                                                                                                                                                           |

## Distribution Settings

今回は Certificate Manager 上で SSL 証明書の発行はしません
事前に SSL 証明書を発行してもらい、Certificate Manager でインポートしておきます
CloudFront で証明書を使用するには、**Certificate Manager のリージョンをバージニア北部**にする必要がある

| 設定項目                        | 設定値                                                                           | 備考                                                                                                                                                                                                                                                                          |
| ------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Price Class                     | Use U.S., Canada, Europe, Asia, Middle East and Africa                           | どのエッジロケーション（CloudFront、Route 53、WAF が配置されているデータセンター）を使うかを設定                                                                                                                                                                              |
| AWS WAF Web ACL                 | None                                                                             | WAF の設定                                                                                                                                                                                                                                                                    |
| Alternate Domain Names (CNAMEs) | ドメイン名を設定                                                                 |                                                                                                                                                                                                                                                                               |
| SSL Certificate                 | Custom SSL Certificate<br />Certificate Manager でインポートした証明書を選択する |                                                                                                                                                                                                                                                                               |
| Custom SSL Client Support       | Clients that Support Server Name Indication (SNI)                                | SNI をサポートするクライアントのみかすべてのクライアントにコンテンツを提供するかの設定<br />2010 年以降にリリースされたブラウザは SNI がサポートされているみたいなので、SNI をサポートするクライアントのみで良さそう<br />すべてのクライアントにすると月 6 万の請求が発生する |
| Security Policy                 | TLSv1.2_2019                                                                     |                                                                                                                                                                                                                                                                               |
| Supported HTTP Versions         | HTTP/2, HTTP/1.1, HTTP/1.0                                                       |                                                                                                                                                                                                                                                                               |
| Default Root Object             | index.html                                                                       | Nuxt を静的ビルドすると、dist ディレクトリ直下に index.html が生成されるので、これがルートになる                                                                                                                                                                              |
| Standard Logging                | On                                                                               | CloudFront の標準ログを出力するかどうか                                                                                                                                                                                                                                       |
| S3 Bucket for Logs              | S3 バケット名を選択                                                              | CloudFront のログ出力用に S3 バケットを作成しておく                                                                                                                                                                                                                           |
| Log Prefix                      | 任意のディレクトリ名                                                             | ログのバケット内でディレクトリを分けて管理したいときに設定（たとえば本番用とステージング用で分けるとか）                                                                                                                                                                      |
| Cookie Logging                  | Off                                                                              | ログに Cookie を含めるかどうか                                                                                                                                                                                                                                                |
| Enable IPv6                     | On                                                                               | IPv6 アドレスからのリクエストに応答するかどうか                                                                                                                                                                                                                               |
| Comment                         | -                                                                                |                                                                                                                                                                                                                                                                               |
| Distribution State              | Enabled                                                                          | この Distribution の作成後、すぐに有効するかどうか                                                                                                                                                                                                                            |

Distribution の作成後、Status が Deployed になったら、使える状態になる
CloudFront のデフォルトドメイン`.cloudfront.net`でアクセスできたら成功

S3 バケットのリージョンをバージニア北部以外で作った場合、307 レスポンスが返ってくる可能性がある
しばらく待てば見れるようになるが、もし、すぐに確認したい場合は、以下のドキュメントで紹介されているようにオリジンドメイン名にリージョン名を追加するやり方で確認できる
https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/

自分の場合は、1 時間くらい放置しておいたら見れるようになっていた

## CloudFront のキャッシュをクリア

デプロイした際は、CloudFront のキャッシュをクリアしたいので、以下を Actions に追記

```yml
steps:
  - name: Deploy
    env:
      DEPLOY_BUCKET: ${{ secrets.DEPLOY_BUCKET }}
      DISTRIBUTION_ID: ${{ secrets.DISTRIBUTION_ID }}
    run: |
      aws s3 sync ./dist s3://$DEPLOY_BUCKET --delete
      aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths '/*'
```

以下のポリシーをアタッチ

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": ["cloudfront:CreateInvalidation"],
      "Resource": [
        "arn:aws:cloudfront::[AWSアカウントID]:distribution/[CloudFrontのディストリビューションID]"
      ]
    }
  ]
}
```

## Nuxt で複数のページコンポーネントがある場合

Nuxt で静的ビルドすると、ページコンポーネントごとに`index.html`が作成される
ただし、CloudFront の Default Root Object の設定では、ルートの`index.html`にしかこの設定は適用されないので、ルート（`/`）以外の`/setting`や`/report`などでアクセスすると CloudFront は 403 エラーを返す

403 エラーは、CloudFront のエラーページの設定で一応回避できる

| 設定項目                            | 設定値         | 備考 |
| ----------------------------------- | -------------- | ---- |
| HTTP Error Code                     | 403: Forbidden |      |
| Error Caching Minimum TTL (seconds) | 10             |      |
| Customize Error Response            | Yes            |      |
| Response Page Path                  | /index.html    |      |
| HTTP Response Code                  | 200: OK        |      |

CloudFront の 403 エラーを 200 にして、ルートの`index.html`を返している

# Route 53

CloudFront のデフォルトドメイン`.cloudfront.net`で公開しているので、Route 53 を設定し、任意のドメインで見れるようにする
任意のドメインはあらかじめ取得しておく

## ホストゾーンの作成

`Route 53 -> ホストゾーン`からホストゾーンの作成

ドメイン名を入力し、パブリックホストゾーンを選んで作成

## レコードを作成

上記で作成したホストゾーンを選択し、レコードを作成

| 設定項目                     | 設定値                                                                | 備考                                                                                                                                            |
| ---------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| ルーティングポリシー         | シンプルルーティング                                                  |                                                                                                                                                 |
| レコード名                   | -                                                                     | サブドメインをルーティングする場合は、サブドメイン名を入れる                                                                                    |
| エイリアス                   | On                                                                    | On にすると、対象の AWS リソースがデフォルトで持っているドメインを選択できる<br />Off にすると、対象の AWS リソースの IP アドレスを直接入力する |
| レコードタイプ               | A                                                                     |                                                                                                                                                 |
| トラフィックのルーティング先 | CloudFront<br />バージニア北部<br />CloudFront のデフォルトドメイン名 |                                                                                                                                                 |

しばらく待つと設定したドメイン名でアクセスできるようになる