Nuxt.js で作った SPA を Google Cloud Storage で公開する

# はじめに
皆さんは Nuxt.js で作ったアプリをどのようにデプロイしているでしょうか。私が関わったプロジェクトでは、Nuxt.js を SPA モードで動かして、それを GCP の Load Balancer を使いつつ Cloud Storage にデプロイしています。

特に何も考えなくてもうまく行けばそれで良かったのですが、若干ハマる箇所がありますのでそこを交えて詳しいやり方を解説していきたいと思います。

# 事前準備
言うまでもなく、Nuxt.js によるアプリケーションの準備は完了させておいてください。詳しいやり方についてはこの記事の範囲外になりますので説明しません。

また、GCP CLI をちょくちょく使っていきます。`Google Cloud SDK` をインストールして、ログインやデフォルトプロジェクトの設定などを済ませておいてください。macOS でのインストール例を掲載しておきます。

```bash
$ brew cask install google-cloud-sdk
```

# Nuxt.js 側の準備
## SPAモードへの切り替え
もしサーバーサイドレンダリングモードに設定している場合、SPA モードに切り替える必要があります。`nuxt.config.js` を開いて、`mode` 設定を `'spa'` に変更してください。

```typescript:nuxt.config.js
mode: 'spa',
```

## Trailing slash の有効化
まず、Nuxt 2.10 以上のバージョンを用意します。このバージョンから Trailing slash に関する設定項目が追加されているため、とにもかくにも古いバージョンを使っている場合バージョンアップが必須です。GCP のロードバランサーの挙動の都合で、Trailing slash を必ず有効にしておく必要があります。どのような挙動をするかは後ほど詳しく解説します。

バージョンの確認が済んだら `nuxt.config.js` を開き、`router` セクションに `trailingSlash: true` を追加して設定を有効化します。

```typescript:nuxt.config.js
router {
  trailingSlash: true
},
```

## Router によるプッシュの確認
プログラム中で Nuxt が提供する router を使って `push` している箇所がいくつかあるかと思います。Trailing slash を有効にすると末尾にスラッシュがないアクセスができなくなります（リダイレクトもしてくれません）ので、`push` している箇所は確実に末尾のスラッシュを付けるようにしてください。

```typescript
// 未ログイン時はログインページに戻す的な処理
if (!user) {
  // (Composition API の書き方ですがご容赦)
  context.root.$router.push('/login/')
}
```

# GCP 側の準備
## Cloud Storage
ひとまず Cloud Storage でデプロイ用の Bucket をひとつ作っておきます。任意の名前でOKですが、ドメインっぽい名前を付けるのは静的サイト用の設定がデフォルトでロードされてしまうので、今回は避けておきました。特にルーティング周りは全てロードバランサーにお任せする予定ですので。

続いて `permissions` ページで全ユーザー (`allUsers`) に対して閲覧権限を付与しておきます。

<img width="675" alt="allUsersに閲覧権限を付与" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53168/af876de3-bd2e-9f9f-cc24-3d9cb0e0216f.png">

もし CI でデプロイ予定であれば、適当な無権限のサービスアカウントを IAM で発行して、そのアカウントを操作する用の鍵をJSONでダウンロードしておき、同じく `permission` ページからそのサービスアカウントに対してストレージ管理者の権限を付与しておくと便利です。

権限設定が終わったらインデックスページの設定を `gsutil` コマンドを使って行います。このコマンドを実行することで、URLの末尾にスラッシュが来たときどのページをデフォルトで見に行くかを設定することができます。`<BUCKET_NAME>` の箇所をご自身の環境に変更してください。

```bash
$ gsutil web set -m index.html gs://<BUCKET_NAME>
```

## Load Balancing
続いてロードバランサーを設定していきます。といってもマウスでぽちぽちするだけですが。

まず Backend buckets を登録します。先ほど作成した Cloud Storage を指定して作るだけですね。

続いて Path Rule を設定します。`hosts` に利用予定のサブドメインを入力してから、`Paths` に `/*` と入力しておき、バックエンドは先ほど設定したバックエンドを選んでおくだけの簡単作業です。これで指定したホスト宛の通信は全て先ほどのバケットに届くようになります。

なおこの Path Rule が若干くせ者で、`/path` のように末尾にスラッシュがないアドレスにアクセスされたとき、自動的に `/path/index.html` にリダイレクトする挙動をします。しかし SPA では History API でページ遷移らしき動きを再現しているだけですので、そんなページは存在するはずもなく404が返ってきます。そういうわけで Nuxt 側できちんと末尾スラッシュへのアクセスを強制するように作っておかないとおかしなことになるわけです。

最後にどのポートで待ち受けるかを設定します。基本的には `HTTPS (443)` のみの受付で大丈夫だと思います[^1]。グローバルIPアドレスはGCPに確保してもらいつつ、SSL 証明書 (Let's encrypt) を発行する設定をささっと書いてしまえば完了です。

なお IP アドレスの付与と SSL 証明書の発行には幾分か時間がかかりますので、終わるまでお茶でも飲んでゆっくりしてください。ロードバランサーのグローバル IP を DNS の A レコードで使いたいドメインに紐付けておくこともお忘れなく。

# デプロイしてみる
バックエンドの準備が終わったらデプロイします。まずはビルドしましょう。yarn での例を掲載します。

```bash
$ yarn build
```

終わったら `dist` というディレクトリが生成されており、その中に SPA に必要なファイル一式が入っています。このディレクトリを一気にアップロードしてしまいましょう。`gsutil` コマンドで `rsync` を使うと良い感じに上げてくれます。`-R` オプションでディレクトリの中まで再帰的に同期をとってもらうようにすることを忘れないでください。

```bash
$ gsutil rsync -R dist gs://<BUCKET_NAME>
```

もし CI で自動化したい場合は、先ほど生成しておいたサービスアカウントのキーを使って SDK にログインして、同じようにビルドして `rsync` すれば万事解決です。

# おわりに
ざっくりとですが Nuxt.js で作成した SPA を GCP の Cloud Storage にデプロイする方法を解説してきました。実際にこれを構築していた際には特にリダイレクトの挙動周りでハマってしまいましたが、無事に Nuxt.js がバージョンアップで Trailing Slash に対応してくれたおかげで事なきを得ました。

勝手に `index.html` にリダイレクトしてくるようなロードバランサーのデフォルトの挙動はカスタマイズできないため、こうした若干トリッキーな解決方法をとらざるを得ないのですが、なんとか形にできて良かったです。

こうした細かいカスタマイズに不便な点がある一方、自分でメンテナンスしなければならない箇所が圧倒的に少なくできたり、CIを組んでデプロイを自動化できたりするなどメリットも十分にありますので、各プロジェクトにおいて比較検討をした上で導入してみると良いと思います。

[^1]: 大変残念なことに Load Balancing では HTTP から HTTPS へのリダイレクトはできないようです