CloudBuild + Nuxt:SSR + Firebase でアプリをリリースする

### この記事を書いた背景

現在、FirebaseでWebアプリを作っており、CI/CDにCloud Buildを使って環境を構築した。
全体的にCloud Buildのドキュメントが少なく、ハマったポイントが多かったので、メモとして残すことにする。

### Cloud BuildをCI/CDで採用した理由

CircleCI、github Actions、Cloud Build で比較検討した結果、Cloud Buildを使うことにした。
主な理由としては、作成中のアプリではGCPの他サービスも使っており、それらと連携しやすい && 請求書を1つにまとめられるのが大きな理由。

個人的な見解としては、、、上記のような特に理由が無ければ、現時点ではCloud Buildはまだ使わない方がいいかも。Slackとの連携が面倒だったり、ドキュメントの数が他のCIサービスに比べると少ない。Google公式のドキュメントもあるが、テキストだけの説明なので、理解するのにハードルが高い。

2018年のブログ記事ですが、各種CI/CDのサービスを比較しているので、参考にするといいかも。
https://swet.dena.com/entry/2018/08/20/170836

### サービスの構成
Nuxt（SSR）===> github（pushがトリガー） ===> Cloud Build ===> Firebase ===> 公開

少し余談だが、今回はSSRでページを動的に表示させたかったので、buildしたNuxt Appをfunctions上に配置している。
詳しくは、この記事を参考に。
https://qiita.com/sychocola1/items/c3f329da3a14c85c3a73

### 構築の手順
下記の参考に実施すると分かりやすい。
https://yanou.jp/deploy-firebase-hosting-by-cloud-build/
前提として、Local環境ではNuxt Appが動いている状態から説明。
ざっくりと下記の手順で作成する。

1. GCPのCloud Builderにて、トリガーを設定。その際に、githubのレポジトリーを登録
1. Cloud Builderで利用するDockerイメージを作成して、gcloudにsubmit 
1. cloudbuild.yaml に実行コマンドを登録して、githubにpushする

### Dockerイメージを作成する
今回、cloud buildでデプロイする際に、下記を実施したかった。
1. Slackでデプロイの成功/失敗を通知させる
2. FirestoreのSecurity Rulesをjestで実行する
3. dotenvで利用している.env.XXXX ファイルを、GCPの「Secret Manager」経由で取得する（githubには、.gitignoreを設定してpushしていないので。）

2を実行するためには、Dockerのコンテナ上にfirestoreのエミュレーターをバックグランドで起動させる必要があるので、javaがインストールされている必要がある。

ポイントは、nodeとopenJDKのみがインストールされており、それ以外の不要なパッケージは存在しないミニマルなDockerコンテナが欲しかった。（不要なパッケージがあると、イメージのbuild時間に影響する。）
ちょうど、いい感じのDockerイメージを公開している人がいたので、今回はこちらを採用。
https://github.com/devayansarkar/maven-node-openjdk

プロジェクトのルートディレクトリーから、下記コマンドで利用できるようになる。


``` #nuxtapp
$ cd ROOT_DIRECTORY
$ git clone git@github.com:devayansarkar/maven-node-openjdk.git
$ cd maven-node-openjdk
$ gcloud builds submit --tag gcr.io/${PROJECT_ID}/maven-node-openjdk --project ${PROJECT_ID}
$ cd ..
$ rm -fr maven-node-openjdk
```


### cloud buildの成功/失敗をslackで通知する
下記の公式ドキュメントに通知方法が書いてあるのですが、Pub/Subを発行して、IAMを設定したりとかなり大変なので、下記の方法で簡単に実装。
https://qiita.com/tnagao3000/items/ff7dd2e89fd8cb42ad5a
自分で、slackに通知する文面やiconも変更できるので、特に困ることはない。

### 「Secret Manager」経由で環境変数を取得する
dotenvを利用して、production/staging/developmentの環境変数を使い分けており、local環境でbuildして、直接Firebaseにdeployする分には問題なかったのだが、github経由でfirebaseにdeployとなると、事前に登録した.env.xxxxファイルを、gcloudコマンドで取得することにした。
詳しくは、公式ドキュメントが参考になる。
https://cloud.google.com/cloud-build/docs/securing-builds/use-encrypted-secrets-credentials?hl=ja


### 完成したcloudbuild.yaml

``` cloudbuild.yaml
steps:
  - id: "Watch:slackbot"
    name: "gcr.io/$PROJECT_ID/slackbot"
    args:
      [
        "--build",
        "$BUILD_ID",
        "--webhook",
        "https://hooks.slack.com/services/XXXXXXXX",
      ]
  - id: "Install:npm_packages"
    name: "gcr.io/$PROJECT_ID/maven-node-openjdk"
    args: ["npm", "install"]
    dir: "src"
    waitFor: ["-"]
  - id: "Install:functions_npm_packages"
    name: "gcr.io/$PROJECT_ID/maven-node-openjdk"
    args: ["npm", "install"]
    dir: "functions"
    waitFor: ["-"]
  - id: "Fetch:dotenv"
    name: gcr.io/cloud-builders/gcloud
    entrypoint: "bash"
    args:
      [
        "-c",
        "gcloud secrets versions access latest --secret=dotenv_file --format='get(payload.data)' --project $PROJECT_ID | tr '_-' '/+' | base64 -d > .env.${_FIREBASE_ENV}",
      ]
    dir: "src/config"
    waitFor: ["Watch:slackbot"]
  - id: "Run:test"
    name: "gcr.io/$PROJECT_ID/maven-node-openjdk"
    args: ["npm", "run", "ci-test"]
    dir: "src"
    waitFor: ["Install:npm_packages", "Install:functions_npm_packages"]
  - id: "Build:App"
    name: "gcr.io/$PROJECT_ID/maven-node-openjdk"
    args: ["npm", "run", "build-nuxt:${_FIREBASE_ENV}"]
    dir: "src"
  - id: "Deploy:Firebase"
    name: "gcr.io/$PROJECT_ID/maven-node-openjdk"
    args: ["npm", "run", "ci-deploy:${_FIREBASE_ENV}"]
    dir: "src"
timeout: 1200s

```
${_FIREBASE_ENV} は、githubをトリガー登録した際に、変数として独自に登録。
production or staging の文字列が入る。

src/package.jsonの一部ですが、公開します。

``` src/package.json
{
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "copy-nuxt": "rm -rf ../functions/.nuxt && cp -R .nuxt/ ../functions/.nuxt && cp nuxt.config.js ../functions/src/nuxt.config.js && rm -rf ../public/* && mkdir -p ../public/_nuxt && cp -R .nuxt/dist/client/ ../public/_nuxt && cp -a static/. ../public/ && cp -R ./config/ ../functions/src/config",
    "build-nuxt:production": "NODE_ENV=\"production\" nuxt build && npm run copy-nuxt",
    "build-nuxt:staging": "NODE_ENV=\"staging\" nuxt build && npm run copy-nuxt",
    "start:production": "NODE_ENV=\"production\" nuxt start",
    "start:staging": "NODE_ENV=\"staging\" nuxt start",
    "generate": "nuxt generate",
    "test": "jest",
    "ci-test": "firebase emulators:start --only firestore & sleep 10 && jest",
    "ci-deploy:staging": "firebase deploy --non-interactive --force --project staging",
    "ci-deploy:production": "firebase deploy --non-interactive --force --project production"
  },
}
```
ここでのポイントは、firestoreのエミュレーターをバックグランドで立ち上げた後に10秒待ってから、テストを実行していること。エミュレーターが立ち上がるまで数秒かかるので。
この辺りのノウハウは、下記の記事を参考にすると分かりやすい。
https://rightcode.co.jp/blog/information-technology/test-dirven-firestore-security-rules-ci

また、普通にfirebaseをdeployすると、不要なファイルを削除する？などのメッセージが出るので、 --force を付けて、強制的にYesにしています。
```
Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. (y/N) 
```

### 最後に 。。。
これ以外にも、cloud buildを構築する上で、いくつか悩んだポイントがあるのですが、今後、時間が出来たら記載したいと思います。
この記事が誰かの役に立ったら、嬉しいです！