この記事はGoodpatch Advent Calendar 2020 - Qiitaの17日目の記事でやんす
わたくし @yahharo は主にSaaSサービスのバックエンドやAWSのインフラを整備したり、GCP/FirebaseでのFunctionsやSecurityRulesなどバックエンド寄りの開発をしています
これは何?
ここ1年以上FirebaseでのWebプロジェクトを開発・運用を経験する中で、様々な改善を行ってきました。
その中でも「デプロイ」に限ってやってきたことなどを段階的に紹介できたらと思います。
だいぶニッチな内容になりますが、Firebaseを使った運用側の知見はまだまだ少ないように感じるので、どなたかの参考になれば幸いです
今回の構成例
簡略化するために、例えば以下のような場合の構成を例として考えてみます。
- Firebase Hosting(SPAとして)
- Cloud Firestore (DBとして)
- Cloud Functions (APIとして)
ディレクトリ構成はこんな感じでpackageはそれぞれで管理しているようなWebアプリケーションです。
├── firebase.json
├── .firebaserc
├── firestore
│ ├── package.json
│ ├── firestore.indexes.json
│ └── firestore.rules
├── functions
│ ├── package.json
│ ├── lib
│ └── index.js
└── hosting
├── package.json
└── dist
└── index.html
- ここではFirestoreのデプロイとは、Firestore Security Rules, Indexのデプロイを指します。)
- 途中このフォルダにないCloudBuildやWebpackの話が出てきますが、一般的な内容とほとんど同じなので細かい説明は省きます。
- 仮にこのプロジェクトを「Go!Go!やっはー!」と名付け、
go-go-yahharo-dev
としてFirebaseプロジェクトを作成しました。
$ firebase projects:create go-go-yahharo-dev
さぁデプロイしてみましょう!
全部デプロイ!(普通のデプロイ)
公式ドキュメントにもあるとおり、普通にデプロイするならこのコマンドですよね。
$ firebase deploy
これでデプロイは確かにできますね。
しかし、これではHostingもFunctionsもFirestore Security Rulesも全てデプロイされてしまいます。
リソース別にデプロイするぞ
開発していて、全部出したい!というケースはあまりないと思うので、リソース別にデプロイできるようにしたいと思います。
firebase deploy
コマンドは--only ***
をつけることで対象を絞ることができるので、それぞれの package.json
でコマンド npm run deploy
で動くように定義したいと思います。
{
"scripts": {
"deploy": "firebase deploy --only functions"
}
}
{
"scripts": {
"deploy": "firebase deploy --only firestore"
}
}
{
"scripts": {
"deploy": "firebase deploy --only hosting"
}
}
これで、例えばフロントエンドのちょっとした修正だけデプロイしたい場合でも
$ cd hosting && npm run deploy
とすればHostingに対してだけデプロイされ、Functions, Firestore Security Rulesはデプロイされないようになりました
開発環境と本番環境のデプロイを分けたいぞ
通常、 firebase deploy
コマンドで指定されるFirebaseプロジェクトは .firebaserc
での default
のあるプロジェクトになります。
{
"projects": {
"default": "go-go-yahharo-dev"
}
}
firebase deploy --project=PROJECT_ID_or_ALIAS
とすれば、デプロイするプロジェクトを分けることができます。
エイリアスの作成
go-go-yahharo-production
のプロジェクトを作成し初期設定した後、.firebaserc
に参照しやすくするためにエイリアスとして足しておきましょう
{
"projects": {
"default": "go-go-yahharo-dev",
"production": "go-go-yahharo-production"
}
}
- エイリアスについては Firebase CLI リファレンス を参照ください
デプロイコマンド、環境変数の追加
そして本番環境用のコマンドをそれぞれ用意しておきます。(例としてHostingだけ)
その際に、エラー通知など、開発と本番で挙動を変えたいこともあると思うので、アプリケーション側でも環境を判定できるようにYAHHARO_ENV=production
のようにアプリの環境名を指定しておきましょう。
{
"scripts": {
"deploy": "YAHHARO_ENV=dev firebase deploy --only hosting",
"deploy:production": "YAHHARO_ENV=production firebase deploy --only hosting --project=production"
}
}
これで、本番環境専用のデプロイコマンドができました!
$ cd firestore && npm run deploy:production
と思ったら・・・
フロントエンドでのFirebaseの初期化時の環境切り替え
複数のプロジェクトを構成する | Firebase にも記載があるように、firebase.initializeApp(options)
で設定する projectId
などの値を切り替えなくてはなりません。
単純にやれば、
const firebaseDevConfig = {
projectId: 'go-go-yahharo-dev',
apiKey: '************',
authDomain: 'go-go-yahharo-dev.web.app',
// appIdなどもあるけど省略
};
const firebaseProductionConfig = {
projectId: 'go-go-yahharo-production',
apiKey: '************',
authDomain: 'go-go-yahharo.app',
// appIdなどもあるけど省略
};
const YAHHARO_ENV = process.env.YAHHARO_ENV;
// build時の環境変数を元に適用するオプションを切り替え
const firebaseOption =
YAHHARO_ENV === 'production' ? firebaseProductionConfig : firebaseDevConfig;
firebase.initializeApp(firebaseOption);
のように渡すようにした環境変数で判定して切り替えれば良いのかもしれません。
ただ、buildされて公開された時、このファイルがユーザーから見えてしまい開発環境のURLなどがわかってしまうのはあまりよろしくありません
(漏洩したところで色々な設定で制御すれば大きな問題にはならないと思いますが、ここでの説明は省きます。)
んじゃbuild前に設定ファイルを書き換えればいいじゃん
なんとかしてbuildされるパッケージに該当の環境のファイルだけが含まれるようにすれば良いので、Hosting周りで環境を判定する時に必ず設定が記載されたjsファイルをコピーするようにしました
(iniファイルとかにしても良かったのですが、firebase apps:sdkconfig
で出力されたファイルをそのまま使えるようにしたかったのです)
要は
- 環境変数を判定してbuild前に設定ファイルを特定の場所にコピー
- アプリケーションではその場所からファイルをimportしておく
(コピー先は.gitignore
に追加しておく)
とすれば、他の環境の設定が入ることはありません
例えば以下のように環境別にファイルを用意し、それぞれの設定を書いておきます。
├── environments
│ ├── dev
│ │ └── firebaseConfig.js
│ └── production
│ └── firebaseConfig.js
└── src
├── configs
│ └── .gitkeep
│ └── firebaseConfig.js (git ignored)
└── initialize.js
で、以下のコピー関数と
const fs = require('fs-extra');
const path = require('path');
const TARGET_FILE_PATH = 'hosting/src/configs/firebaseConfig.js';
module.exports = {
copyConfig: (env, orverwrite) => {
if (!env | (env === '')) {
env = 'dev';
}
env = path.normalize(env.replace(/[\.|\/]/gi, ''));
const sourceFilePath = `environments/${env}/firebaseConfig.js`;
console.log(
`Copy config files for [${env}] overwrite[${orverwrite.toString()}] from[${sourceFilePath}] to[${TARGET_FILE_PATH}]`
);
fs.copy(sourceFilePath, TARGET_FILE_PATH, {
overwrite: orverwrite,
errorOnExist: false,
});
},
};
node
コマンドなどで実行可能なCLIを作っておきます。
const { copyConfig } = require('./copyConfig');
copyConfig(process.env.YAHHARO_ENV, false);
実行例:ローカルの実行やデプロイの時:Webpackでbuildする前に
// 途中色々省略
const EventHooksWebpackPlugin = require('event-hooks-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { copyConfig } = require('./environments/copyConfig');
const YAHHARO_ENV = !process.env.YAHHARO_ENV
? 'dev'
: process.env.YAHHARO_ENV;
const config = {
plugins: [
new EventHooksWebpackPlugin({
environment: () => {
copyConfig(YAHHARO_ENV, true);
},
}),
],
};
実行例:Cloud Buildの前に実行できるように
steps:
## for frontend
- name: 'node:12.20.0'
id: 'hosting-install'
args: ['npm', 'install']
dir: hosting
waitFor: ['-']
- name: 'node:12.20.0'
id: 'hosting-copy_config'
waitFor: ['hosting-install']
dir: hosting
args: ['node', './environments/copyConfigCli.js']
- name: 'node:12.20.0'
id: 'hosting-lint'
waitFor: ['hosting-copy_config']
dir: hosting
args: ['npm', 'run', 'lint']
こんな感じでやったら環境別にデプロイできました!
さらに開発者が増えたのでそれぞれ個人の環境を作成するよ!
チームで開発していくと、どうしてもFirestoreのRuleやIndexなども1つの開発環境では並行して進めるのが難しい場合があります。
そこで、環境を以下のように定義して、開発者個人環境を整備することにしました。
- 本番環境:
production
- 共通開発環境:
dev
- 開発者環境:
each-developer
例えば masa
さんが個人環境を作成して go-go-yahharo-dev-masa
というプロジェクトを作成したとしましょう。
開発者環境と個々のproject_idの紐付け
それぞれのShellの ~/.zshrc
などに YAHHARO_DEVELOPER_ENV
の環境設定をしておきます。
$ echo 'export YAHHARO_DEVELOPER_ENV="go-go-yahharo-dev-masa"' >> ~/.zshrc
個々の環境
環境ファイルは開発者によって異なるので ignoreしておきます。
environments
├── copyConfig.js
├── copyConfigCli.js
├── dev
│ └── firebaseConfig.js
├── each-developer
│ └── .gitkeep
│ └── firebaseConfig.js (git ignored)
└── production
└── firebaseConfig.js
ローカルでの開発
環境を分けたのでserveも個人環境でできたりします。
{
"scripts": {
"serve:developer": "YAHHARO_ENV=each-developer npm run build && npm run firebase serve --only hosting --project=$YAHHARO_DEVELOPER_ENV"
}
}
デプロイコマンド
ここで、 each-developer
の --project
は先ほどの環境変数を設定します
{
"scripts": {
"deploy": "YAHHARO_ENV=dev firebase deploy --only firestore",
"deploy:production": "YAHHARO_ENV=production firebase deploy --only firestore --project=production",
"deploy:each-developer": "YAHHARO_ENV=each-developer firebase deploy --only firestore --project=$YAHHARO_DEVELOPER_ENV"
}
}
これで
$ cd firestore && npm run deploy:each-developer
で自分の環境にデプロイできるようになりました!
本番環境と開発環境のデプロイコマンド同じで大丈夫?
そんな中、問題が発生しました
開発中に個人環境にSecurity Rulesなどを適用していたエンジニアが、誤って開発中のRuleを本番にデプロイしてしまいました・・・
幸いすぐにエラーが頻発したため気がつくことができ、戻しましたが、そもそも、いくつかの問題がありました。
-
deploy:production
deploy:each-developer
はコマンドのヒストリーなどから実行した場合に間違えやすい - 本番へのデプロイはローカルからの
firebase deploy
ではなくCloudBuildなどの同一環境から行うべき - 本番と開発環境のデプロイ権限を分けるべき
などなど
すぐにできる対応として deploy:production
deploy:each-developer
のコマンドを実行時に区別できるようにしました。
本番環境へのデプロイについてのみ、確認を促す
こんな感じの確認が出るようにしました。
$ npm run deploy:production
$ /bin/bash deploy.sh --project=production
CONFIRM: deploy to production: (y/N)
ポイントは 開発環境へのデプロイでは確認を出さない という点です。
確認用の関数を作る
########################
# deploy functions
########################
## Check argument
function checkProject() {
for ARGUMENT in "$@"; do
KEY=$(echo $ARGUMENT | cut -f1 -d=)
VALUE=$(echo $ARGUMENT | cut -f2 -d=)
case "$KEY" in
--project) project=${VALUE} ;;
*) ;;
esac
done
if [ ! $project ]; then
echo "Need set project '--project=YOUR_PROJECT_ID'"
exit 1
fi
}
## confirmation
function confirmationDeploy() {
DO_IT="N"
NEED_CONFIRM_ENV=('production')
### Ask at prompt
if [[ ${NEED_CONFIRM_ENV[@]} =~ ${project} ]]; then
read -p $'\e[31mCONFIRM\e[0m'': deploy to '$'\e[33m'$project$'\e[0m: (y/N)' DO_IT
else
DO_IT="y"
fi
}
## Execute only when answer is 'y'
function executeDeploy() {
if [ "$DO_IT" = "y" ]; then
echo -e "\033[1m\033[32mGo deploy to '$project'\033[0m!"
deploy
exit 0
else
echo -e $'\e[36mDeploy canceled.\e[0m'
fi
}
それぞれのリソースにてデプロイシェルを作成
#!/bin/bash
## Deploy command
deploy(){
npm run firebase deploy --only firestore --project=$project
}
source ../deployFunctions.sh
checkProject $@
confirmationDeploy
executeDeploy
この設定を追加して以降はデプロイのミスはなくなりました
これでもう怖くない
まだまだあります
と、ここまでデプロイの話を書きましたが、まだまだ足りないことがたくさんあります。
以下はここに書ききれないことを、記載しておきます。
ビルドをローカルから行わないでCloudBuildに任せる
- 環境の差異や、たまたまローカルのコードに混入してた場合のリスクを減らせる
- デプロイの権限を集約できる
Hosting, Functionsのリリースが遅い
- ビルド済みのリソースを以下のAPIなどを使えば迅速に適用できる
リリース時に内部のメンバーに自動的に告知したい
- 万が一のデプロイミスや、周知しきれていないリリースなどがあった場合でも、確実に内部のメンバーに通知し、リスクを減らしたい
- GCPの監査ログを設定すれば変更された場合にロギングされるので、それをPub/SubのSinkに噛ませてSlack通知を設定するなど
- Cloud Audit Logs | Cloud Logging | Google Cloud
- ログのエクスポートの概要 | Cloud Logging | Google Cloud
などなどなど!
まとめ
- Firebaseのデプロイはリソース別、環境別に設定できる
- 運用に合わせて少しづつ変えていこう
- 俺たちの理想のデプロイはまだ始まったばかりだ・・・
ということで、Firebase超絶良いので使っていきまっしょい!
ちなみに今回作成した画像はすべて Strap を用いて作成しました。
全部で15分くらいで作れたので情報の整理やちょっとした作図にもおすすめです
トライアル是非試してみてください!(大事!)
わっしょい!
明日の Goodpatch Advent Calendar 2020 - Qiita は @enpipi がいいこと書いてくれるそうです