この記事を書いた背景
現在、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が動いている状態から説明。
ざっくりと下記の手順で作成する。
- GCPのCloud Builderにて、トリガーを設定。その際に、githubのレポジトリーを登録
- Cloud Builderで利用するDockerイメージを作成して、gcloudにsubmit
- cloudbuild.yaml に実行コマンドを登録して、githubにpushする
Dockerイメージを作成する
今回、cloud buildでデプロイする際に、下記を実施したかった。
- Slackでデプロイの成功/失敗を通知させる
- FirestoreのSecurity Rulesをjestで実行する
- 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
プロジェクトのルートディレクトリーから、下記コマンドで利用できるようになる。
$ 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
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の一部ですが、公開します。
{
"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を構築する上で、いくつか悩んだポイントがあるのですが、今後、時間が出来たら記載したいと思います。
この記事が誰かの役に立ったら、嬉しいです!