Nuxt.js (SSR) を Cloud Run にデプロイ【環境分け対応】

# はじめに

はじめまして。レターファンの創業者でエンジニアの今成です。
全然情報がなくて困ったから書くしかない！と思って、気づいたら半年以上経ってしまいました。
はじめての Qiita なので温かな目で読んでください。

https://letterfan.jp

# 概要

**前提**

- Nuxt.js プロジェクトは既に存在し、ローカルで起動できる
- Nuxt.js は SSR(Server Side Rendering) モードである
- GCP のプロジェクトも既に存在する
- 上述の範囲のことや、IAM まわりについては触れません

**概略図**

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/470790/1975818a-cb89-ed11-f6c3-c2097f4acf21.png)

③ の Firebase Hosting は経由しなくても、まったく問題ありません。[番外編3](#3-firebase-hosting-の設定) で説明します。

**環境分けについて**

Dcokerfile や cloudbuild.yaml で環境変数を指定せずに、gcloud コマンドで環境を識別します。

# 準備

## 0. Google Cloud CLI をインストール

公式ドキュメントがわかりやすいので、リンクだけ貼っておきます。

[Cloud SDK のインストール  |  Google Cloud](https://cloud.google.com/sdk/docs/install?hl=ja)
[Cloud SDK の初期化  |  Cloud SDK のドキュメント  |  Google Cloud](https://cloud.google.com/sdk/docs/initializing?hl=ja)

## 1. 環境変数の設定

Nuxt.js における環境変数の設定方法は色々ありますが、Nuxt.js のデフォルトの `runtimeConfig` を使用します。
かつての `dotenv` を利用する方法などではうまくいかないので気をつけてください。

[Nuxt - RuntimeConfig プロパティ](https://nuxtjs.org/ja/docs/configuration-glossary/configuration-runtime-config/)

`dotenv` を使用している人は [こちらの記事](https://qiita.com/taai/items/cbc61b9b4a18aacad57e) で移行してください。

### nuxt.config.js の設定

環境変数は nuxt.config.js ファイルにすべて記述します。
その際、ランタイム環境変数(runtimeConfig)とは別にビルド時に必要な環境変数も指定します。
本記事では、実行環境を表す環境変数として `NODE_ENV` を利用します。

また、`server` オプションで、起動させるポートとホストも設定できるようにします。

```javascript
export default {
  // ビルド時に必要な環境変数を設定
  env: {
    NODE_ENV: process.env.NODE_ENV,
  },
  // ランタイム時に必要な環境変数を設定
  publicRuntimeConfig: {
    NODE_ENV: process.env.NODE_ENV,
    EXAMPLE: process.env.NODE_ENV !== "production" ? "開発環境" : "本番環境",
    // ...
  },
  server: {
    port: process.env.PORT || 3000,
    host: process.env.HOST || "0.0.0.0",
    timing: false,
  },
  // ...
}
```

## 2. Dockerfile の作成

nuxt.config.js と同じ階層に Dockerfile を作成します。
この Dockerfile を元に Container Registry にコンテナを作成します。

```Dockerfile
FROM node:14.4.0-alpine

ARG _NODE_ENV

WORKDIR /app

# タイムゾーンを東京に固定したい場合はこれを記述【★】
# RUN apk --no-cache add tzdata && \
#   cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
#   apk del tzdata

ENV NODE_ENV ${_NODE_ENV}
ENV LANG C.UTF-8
ENV PORT 8080
ENV HOST 0.0.0.0

COPY package.json ./
RUN npm install

COPY . .
RUN npm run build

CMD [ "npm", "run", "start" ]
```

### タイムゾーンの設定について

サーバーサイドで `Date.getHours()` 系のメソッドを利用すると、世界標準時を基準に取得されてしまいます。
それを日本標準時で行いたい場合は【★】を書いてください。

[【Docker】AlpineとMariaDBのtimezone設定](https://zenn.dev/saiki_toshiki/articles/8ffd31497b8098)

## 3. cloudbuild.yaml の作成

nuxt.config.js と同じ階層に cloudbuild.yaml を作成します。

```yaml
steps:
  - name: "gcr.io/cloud-builders/docker"
    args:
      [
        "build",
        "--build-arg",
        "_NODE_ENV=$_NODE_ENV",
        "-t",
        "asia.gcr.io/$PROJECT_ID/$_SERVICE_NAME:$_TAG_NAME",
        ".",
      ]

substitutions:
  _NODE_ENV: "production"
  _TAG_NAME: "latest"
  _SERVICE_NAME: "任意のサービス名"
images: ["asia.gcr.io/$PROJECT_ID/$_SERVICE_NAME:$_TAG_NAME"]
```

### 変数値の置換

`$PROJECT_ID` はデフォルトで Cloud Build が提供する変数値です。GCPのプロジェクトIDが自動的に置換されます。

変数値（環境変数）を自作したい場合は、`_` から始まる変数値を用います。（例：`_NODE_ENV`）
この変数値を置換する方法は次の2通りがあり、

1. cloudbuild.yaml で substitutions として指定する
1. gcloud コマンドの引数として、ビルド実行時に指定する

2つ目の方が優先されます。したがって、1つ目の方法でデフォルト値を指定しておくことをおすすめします。

[変数値の置換  |  Cloud Build のドキュメント  |  Google Cloud](https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values?hl=ja)

#### 例：実行環境 `_NODE_ENV` の伝達

`--build-arg _NODE_ENV=$_NODE_ENV` によって、ビルド時の環境を `substitutions` (変数値の置換) の `_NODE_ENV` から Dockerfile の ARG の `NODE_ENV` を経由して nuxt.config.js 内の `process.env.NODE_ENV` に（ビルド時の環境変数として）パスしています。

## 4. .gcloudignore の設定

不要なファイルも一緒にビルドされないよう、.gcloudignore というファイルを作成します。
内容は .gitignore とまったく同じで問題ないです。

```bash
cp .gitignore .gloudignore
```

# デプロイ

gcloud コマンドを実行する前に、適切なGCPプロジェクトを選択しているか確認してください。

## 1. Cloud Build でビルド

cloudbuild.yaml と同じ階層で以下のコマンドを実行し、コンテナを Container Registry に配置します。
5〜10分程度かかります。

```bash
gcloud builds submit --config=cloudbuild.yaml --substitutions=_NODE_ENV=環境名,_TAG_NAME=バージョン番号,_SERVICE_NAME=サービス名
```

cloudbuild.yaml の substitutions にデフォルト値として指定したものを利用する場合、コマンドライン引数 --substitutions に含める必要はありません。

## 2. Cloud Run にデプロイ

以下のコマンドを実行し、Cloud Run にデプロイします。

```bash
gcloud run deploy --platform managed --port 8080 --region asia-northeast1 --image=asia.gcr.io/プロジェクトID/サービス名:バージョン番号
```

プロジェクトIDには実行中のGCPのプロジェクトIDを、サービス名には `_SERVICE_NAME` として指定したものを、バージョン番号には `_TAG_NAME` として指定したものを入れてください。

数秒〜数十秒後に、サービス名(Service name)が聞かれますので、Enterを押してください。
次に、初回だけ、未認証のアクセスを許可するか聞かれます。通常、サイトを公開するときは許可するので `y` を入力してください。
※ もし後から未認証のアクセスを許可したい場合は、[番外編2](#2-未認証のアクセスを後から許可する) を見てください。

```bash
Service name (サービス名):
# -> Enterを押す
Allow unauthenticated invocations to [サービス名] (y/N)?  
# -> y を入力し、Enterを押す
```

そうすると数十秒後に完了し、アクセスできるURLが出力されます。このURLにアクセスすると、サイトが確認できるはずです。

# 番外編

## 1. コールドスタート対策

王道は最小インスタンス数を設定することです。ただ、この方法はインスタンスが最低1つ起動しているため、インスタンス料金が常に発生します。
そこで、CGPのモニタリングの稼働時間チェックというサービスを利用します。

方法は [この記事](https://sleepless-se.net/2021/04/19/keep-cloud-run-always-warm/) に載っています。

私の場合、Check Frequency を 10 minutes、Response Timeout を 5 seconds に指定しています。

## 2. 未認証のアクセスを後から許可する

[Cloud Run のコンソール](https://console.cloud.google.com/run?hl=ja) を開き、許可したいサービスを選択後、右上の「情報パネルを表示」から「権限」の「プリンシパルを追加」から `allUsers` に `Cloud Run 起動元` のロールを付与します。

## 3. Firebase Hosting の設定

Firebase Hosting のリダイレクトの設定は最初に一度だけ行います。

### 3-0. Firebase プロジェクトを設定

.firebaserc や firebase.json ファイルが存在しない場合、この節の設定が必要です。
[公式ドキュメント](https://firebase.google.com/docs/cli?hl=ja#initialize_a_firebase_project) や [Qiita](https://qiita.com/mtkmr/items/8e3e560d0575ddbecea1) などを読み、nuxt.config.js と同じ階層で Firebase CLI を実行してください。

ただし、Hosting のセットアップで `What do you want to use as your public directory?` と聞かれますが、使用しないので適当に答えて構いません。（デフォルト値の public で問題ないです）

### 3-1. Cloud Run へのリダイレクトを記述

以下のように `"rewrites"` を追加します。これで、`serviceId` に指定した Cloud Run に自動でリダイレクトされます

```json
{
  "hosting": {
    "rewrites": [{
      "source": "**",
      "run": {
        "serviceId": "cloudbuild.yamlで設定した _SERVICE_NAME",
        "region": "asia-northeast1"
      }
    }]
  }
}

```

### 3-2. デプロイターゲット：Firebaseの環境分け

開発環境と本番環境で別のGCPプロジェクトを扱う場合、コマンドライン引数で環境を区別してデプロイすることができます。
本記事では詳しくは触れません。[公式ドキュメント](https://firebase.google.com/docs/cli/targets?hl=ja) や [Qiita](https://qiita.com/yuki0n0/items/632fe9293bdebf7fb67f) などを読んでください。

以降の説明は、.firebaserc が以下のようになっている場合を例に説明します。

```json
{
  "projects": {
    "dev": "example-dev",
    "prod": "example-prod"
  },
  "targets": {
    "example-dev": {
      "hosting": {
        "main": [
          "example-dev"
        ]
      }
    },
    "example-prod": {
      "hosting": {
        "main": [
          "example-prod"
        ]
      }
    }
  }
}
```

### 3-3. Firebase Hosting をデプロイ

以下のコマンドを実行し、デプロイします。

```bash
firebase deploy --only hosting --project dev
```

`dev` の部分は、.friebaserc の projects で指定した値で識別します。

デプロイが完了すると、Cloud Run から提供されるドメイン以外に、Firebase Hosting で設定したドメインでもアクセスできるようになります。

# 最後に

レターファンは、憧れのタレントやアスリートからビデオレターが届くサービスを提供しています。
Webエンジニアだけでなく、Flutterエンジニアなど絶賛募集中です！
ご興味がありましたら是非こちらのサイトをご覧ください！

https://letterfan.notion.site/Career-7346a8183d9a4f538e1c40f7d2a7df6c

# 参考記事

[Nuxt(SSR)をCloud Runに自動デプロイする(GitHub連携) - くらげになりたい。](https://www.memory-lovers.blog/entry/2020/09/04/110000)
[SSRモードのNuxt製アプリをCloud Runでデプロイする方法 | ぐるたかログ](https://gurutaka-log.com/nuxt-cloud-run-deploy)
[SSRなNuxt.jsを環境ごとにDocker化する時の環境変数の扱いについて - Qiita](https://qiita.com/aokabin/items/21107dad8d5fd5d80974)