LoginSignup
5

More than 1 year has passed since last update.

posted at

updated at

FirebaseのWebプロジェクトでデプロイを少しづつ良くしていこうの会

この記事はGoodpatch Advent Calendar 2020 - Qiitaの17日目の記事でやんす:wink:
わたくし @yahharo は主にSaaSサービスのバックエンドやAWSのインフラを整備したり、GCP/FirebaseでのFunctionsやSecurityRulesなどバックエンド寄りの開発をしています :muscle_tone2:

これは何?

ここ1年以上FirebaseでのWebプロジェクトを開発・運用を経験する中で、様々な改善を行ってきました。
その中でも「デプロイ」に限ってやってきたことなどを段階的に紹介できたらと思います。
だいぶニッチな内容になりますが、Firebaseを使った運用側の知見はまだまだ少ないように感じるので、どなたかの参考になれば幸いです:hugging:

今回の構成例

簡略化するために、例えば以下のような場合の構成を例として考えてみます。

  • Firebase Hosting(SPAとして)
  • Cloud Firestore (DBとして)
  • Cloud Functions (APIとして)

go-go-yahharo-structure.png

ディレクトリ構成はこんな感じで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

さぁデプロイしてみましょう!

全部デプロイ!(普通のデプロイ)

公式ドキュメントにもあるとおり、普通にデプロイするならこのコマンドですよね。

deploy-1.png

$ firebase deploy

これでデプロイは確かにできますね。
しかし、これではHostingもFunctionsもFirestore Security Rulesも全てデプロイされてしまいます。

リソース別にデプロイするぞ

開発していて、全部出したい!というケースはあまりないと思うので、リソース別にデプロイできるようにしたいと思います。
deploy-2.png

firebase deploy コマンドは--only *** をつけることで対象を絞ることができるので、それぞれの package.json でコマンド npm run deployで動くように定義したいと思います。

functions/package.json
{
  "scripts": {
    "deploy": "firebase deploy --only functions"
  }
}
firestore/package.json
{
  "scripts": {
    "deploy": "firebase deploy --only firestore"
  }
}
hosting/package.json
{
  "scripts": {
    "deploy": "firebase deploy --only hosting"
  }
}

これで、例えばフロントエンドのちょっとした修正だけデプロイしたい場合でも

$ cd hosting && npm run deploy

とすればHostingに対してだけデプロイされ、Functions, Firestore Security Rulesはデプロイされないようになりました :tada:

開発環境と本番環境のデプロイを分けたいぞ

deploy-3.png

通常、 firebase deploy コマンドで指定されるFirebaseプロジェクトは .firebaserc での defaultのあるプロジェクトになります。

".firebaserc"
{
  "projects": {
    "default": "go-go-yahharo-dev"
  }
}

firebase deploy --project=PROJECT_ID_or_ALIAS とすれば、デプロイするプロジェクトを分けることができます。

エイリアスの作成

go-go-yahharo-production のプロジェクトを作成し初期設定した後、.firebaserc に参照しやすくするためにエイリアスとして足しておきましょう

".firebaserc"
{
  "projects": {
    "default": "go-go-yahharo-dev",
    "production": "go-go-yahharo-production"
  }
}

デプロイコマンド、環境変数の追加

そして本番環境用のコマンドをそれぞれ用意しておきます。(例としてHostingだけ)
その際に、エラー通知など、開発と本番で挙動を変えたいこともあると思うので、アプリケーション側でも環境を判定できるようにYAHHARO_ENV=production のようにアプリの環境名を指定しておきましょう。

hosting/package.json
{
  "scripts": {
    "deploy": "YAHHARO_ENV=dev firebase deploy --only hosting",
    "deploy:production": "YAHHARO_ENV=production firebase deploy --only hosting --project=production"
  }
}

これで、本番環境専用のデプロイコマンドができました! :tada:

$ cd firestore && npm run deploy:production

と思ったら・・・:sob:

フロントエンドでのFirebaseの初期化時の環境切り替え

複数のプロジェクトを構成する  |  Firebase にも記載があるように、firebase.initializeApp(options) で設定する projectId などの値を切り替えなくてはなりません。
単純にやれば、

hosting/src/initialize.js
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などがわかってしまうのはあまりよろしくありません :scream:
(漏洩したところで色々な設定で制御すれば大きな問題にはならないと思いますが、ここでの説明は省きます。)

んじゃbuild前に設定ファイルを書き換えればいいじゃん :bulb:

なんとかしてbuildされるパッケージに該当の環境のファイルだけが含まれるようにすれば良いので、Hosting周りで環境を判定する時に必ず設定が記載されたjsファイルをコピーするようにしました :muscle_tone2:
(iniファイルとかにしても良かったのですが、firebase apps:sdkconfig で出力されたファイルをそのまま使えるようにしたかったのです)

要は

  1. 環境変数を判定してbuild前に設定ファイルを特定の場所にコピー
  2. アプリケーションではその場所からファイルをimportしておく (コピー先は .gitignore に追加しておく)

とすれば、他の環境の設定が入ることはありません

例えば以下のように環境別にファイルを用意し、それぞれの設定を書いておきます。

├── environments
│   ├── dev
│   │   └── firebaseConfig.js
│   └── production
│       └── firebaseConfig.js
└── src
    ├── configs
    │   └── .gitkeep
    │   └── firebaseConfig.js (git ignored)
    └── initialize.js

で、以下のコピー関数と

hosting/environments/copyConfig.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を作っておきます。

hosting/environments/copyConfigCli.js
const { copyConfig } = require('./copyConfig');
copyConfig(process.env.YAHHARO_ENV, false);

実行例:ローカルの実行やデプロイの時:Webpackでbuildする前に

hosting/webpack.config.js
// 途中色々省略
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の前に実行できるように

cloudbuild.yaml
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']

こんな感じでやったら環境別にデプロイできました!:tada::tada:

さらに開発者が増えたのでそれぞれ個人の環境を作成するよ! :muscle_tone2::muscle_tone2::muscle_tone2:

deploy-4.png

チームで開発していくと、どうしても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も個人環境でできたりします。

hosting/package.json
{
  "scripts": {
    "serve:developer": "YAHHARO_ENV=each-developer npm run build && npm run firebase serve --only hosting --project=$YAHHARO_DEVELOPER_ENV"
  }
}

デプロイコマンド

ここで、 each-developer--project は先ほどの環境変数を設定します

firestore/package.json
{
  "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

で自分の環境にデプロイできるようになりました! :tada::tada::tada:

本番環境と開発環境のデプロイコマンド同じで大丈夫? :thinking:

そんな中、問題が発生しました :scream_cat::scream_cat::scream_cat:
開発中に個人環境にSecurity Rulesなどを適用していたエンジニアが、誤って開発中のRuleを本番にデプロイしてしまいました・・・

幸いすぐにエラーが頻発したため気がつくことができ、戻しましたが、そもそも、いくつかの問題がありました。

  • deploy:production deploy:each-developer はコマンドのヒストリーなどから実行した場合に間違えやすい
  • 本番へのデプロイはローカルからの firebase deployではなくCloudBuildなどの同一環境から行うべき
  • 本番と開発環境のデプロイ権限を分けるべき

などなど

すぐにできる対応として deploy:production deploy:each-developer のコマンドを実行時に区別できるようにしました。

deploy-5.png

本番環境へのデプロイについてのみ、確認を促す

こんな感じの確認が出るようにしました。

$ npm run deploy:production
$ /bin/bash deploy.sh --project=production
CONFIRM: deploy to production: (y/N)

ポイントは 開発環境へのデプロイでは確認を出さない という点です。

確認用の関数を作る

deployFunctions.sh
########################
# 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
}

それぞれのリソースにてデプロイシェルを作成

firestore/deploy.sh
#!/bin/bash

## Deploy command
deploy(){
  npm run firebase deploy --only firestore --project=$project
}

source ../deployFunctions.sh

checkProject $@
confirmationDeploy
executeDeploy

この設定を追加して以降はデプロイのミスはなくなりました:tada::tada::tada::tada:
これでもう怖くない:muscle_tone2::muscle_tone2::muscle_tone2::muscle_tone2:


まだまだあります :hugging:

と、ここまでデプロイの話を書きましたが、まだまだ足りないことがたくさんあります。
以下はここに書ききれないことを、記載しておきます。

ビルドをローカルから行わないでCloudBuildに任せる

  • 環境の差異や、たまたまローカルのコードに混入してた場合のリスクを減らせる
  • デプロイの権限を集約できる

Hosting, Functionsのリリースが遅い

リリース時に内部のメンバーに自動的に告知したい

などなどなど!


まとめ :ok_hand_tone2:

  • Firebaseのデプロイはリソース別、環境別に設定できる
  • 運用に合わせて少しづつ変えていこう
  • 俺たちの理想のデプロイはまだ始まったばかりだ・・・

ということで、Firebase超絶良いので使っていきまっしょい!

ちなみに今回作成した画像はすべて Strap を用いて作成しました。
全部で15分くらいで作れたので情報の整理やちょっとした作図にもおすすめです :thumbsup_tone2:
トライアル是非試してみてください!(大事!)
わっしょい!

明日の Goodpatch Advent Calendar 2020 - Qiita@enpipi がいいこと書いてくれるそうです :wink:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5