🤔

Firebaseプロジェクトのリポジトリ管理について

2023/11/30に公開

はじめに

普段はバックエンドとフロントエンドの開発と運用を行いFirebaseのプロジェクトを複数開発してきた中で、試行錯誤とリファクタを繰り返し、現在運用している構成をその経緯を含めながら自分のメモとして残したいと思い、この記事を残します。またこれからFirebaseに触れる方や、現在悩まれている方の参考になったり、すでにご自分の最適解をお持ちの方の比較対象になれば嬉しいです。(もしより良い方法があればご指摘いただけると助かります!)

プロジェクト作成から開発を経ての課題と感じたところ

まず、プロジェクトの環境を用意するために、firebase initしてとりあえず初めてみると以下のようなディレクトリ構造をしているかと思います。

.
├── .firebaserc
├── .gitignore
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   ├── tsconfig.dev.json
│   └── tsconfig.json
├── public
│   └── index.html
├── remoteconfig.template.json
└── storage.rules

プロトタイプとして始める分にはとてもさくっと始められて良い感じです。
しかし、実際にがっつり開発をしようと思ったら特にhostingはAngularやVue, Reactなどのフレームワークを必要となるので、とりあえずpublicの部分をhostingにrenameしてそこに今回はAngularのプロジェクトを入れることにします。

この時にリポジトリを分けるか少し考えましたが、functionsはTypeScriptで書いていく予定でしたので、引き続き同じリポジトリで開発を続けることにしました。(おそらく同じような判断をした方も多いかと思います。)

その時のリポジトリ内のディレクトリ構造はこんな感じかと思います。

.
├── .firebaserc
├── .gitignore
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   ├── tsconfig.dev.json
│   └── tsconfig.json
├── hosting
│   └── {{Angular Project}}
├── remoteconfig.template.json
└── storage.rules

このような構成で一つのリポジトリで運用していました。しかしプロジェクトの開発が進むにつれて色々な問題・悩みが発生しました。

  1. functionsはnodeのバージョンに縛りがあるので、hostingをそちらに合わせるか別々にするか悩む。
    1. nvsなどのバージョン管理ツールを導入することで、幾分か緩和されますが、CI上でのテストやデプロイ時にこのnodeバージョンの操作などを主にメンテナンスコストがかかってくる。
  2. 開発が進むと、functions, hostingそれぞれが大きくなり、プロジェクト全体の可読性が下がる。
  3. rulesのunit testが必要となってくると。専用のディレクトリの中にpackage.jsonを必要とし、また前述した内容の問題が重みを増す。
  4. functionsだけの更新、hostingだけの更新ということはよくありますが、CIなどでのデプロイ処理を単純にmainブランチへのマージだけを条件にしてしまうと、更新がないのにデプロイ処理が走り、リスクを負うことになったり、余計なジョブを走らせる事になってしまうので、そのあたりも踏まえた実装を必要としてしまう。
  5. jestやlint, prettierなどを本当はリポジトリの直下でinstallしたいが、functions, hostingでnodeのバージョンが違う場合や、package.jsonが入れ子になってしまう事、eslintの内容も個別に変わる可能性などもあり、それぞれでinstallすることになることで、さらにメンテナンスコストを感じることになる。
  6. functions, hosting, (ruleのunit test)などで共通の処理が存在する場合にその関数をどこで管理するかに悩んでしまう。
    1. それぞれで同じ関数を用意する(変更があった時に全ての同じ処理も修正するかの判断が必要になってくる)
    2. リポジトリ直下に共通関数ディレクトリを置き直接import(functionsの場合、buildファイルの構造が変わる。それぞれのnodeのバージョンやtsconfigの内容次第ではエラーが出たり出なかったりという問題が発生する。)
    3. リポジトリ直下に共通関数ディレクトリを置き、package.jsonで相対パスでinstallする(パッケージ開発を必要とするディレクトリを用意し、コンパイルしてそれをインストールという流れなので、また大きなプロジェクトを作るという事になる)
    4. プライベートパッケージ
    • 上記対応が考えられるが、最終的には4という結果に落ち着く。しかし、せっかく一つのプロジェクトにしたのに、なぜこれだけ別リポジトリなのかという違和感と戦うことになりました。

以上のような課題がプロジェクトの成長と共に発生し日に日に強く感じるようになり、ストレスになっていきました。

リポジトリの構成とディレクトリ構成

今現在のリポジトリ構成ではありますが、最終的に目的別にリポジトリを分ける決断をしました。

  • firebase: デプロイやエミュレータの起動便利コマンドなどの管理を行う
  • プライベートパッケージ: hosting / functions / rulesのリポジトリで共通処理や型定義などを管理する。
  • hosting: Web画面の開発用リポジトリ(ユーザー画面、管理画面など複数のhostingを管理)
  • functions: nodeで書かれたfunctionsを管理
  • rules: firestoreとstorageのrulesを管理。rulesのunit testやマイグレーションなど含む
  • terraform: 複数のプロジェクトを横断して管理するlac用のリポジトリ

https://zenn.dev/cilly/articles/a405afee95c515

リポジトリ構造のイメージ

ディレクトリ構成

╭─    ~/projects/**/
╰─ tree . -L 2
.
├── project_1
│   ├── firebase
│   ├── functions
│   ├── hostings
│   ├── rules
│   └── package
├── project_2
│   ├── firebase
│   ├── functions
│   ├── hostings
│   ├── rules
│   └── package
└── terraform
    ├── modules
    ├── project_1
    └── project_2

デメリット・メリット

デメリット(とその解決・考え方の切り替え)

  • リポジトリを複数に分けた事で運用コストがあがったのでは?
    • フルスタックなポジションの方で且つ開発初期段階であればそう感じやすい構成です。ただ、開発が進めば全てを同時に開発することの頻度は少なくなるので、運用に乗って慣れてしまえば影響範囲を意識する部分を絞れる為、逆に運用コストは下がったと感じています。
  • デプロイ時の整合性がずれてしまう危険性があるのでは?
    • これに関しては一番悩んだ部分です。むしろこれさえ解決できればもっと早い段階でリポジトリを分ける判断をしていたと思います。その解決としてfirebaseリポジトリだけでデプロイできる仕組みにしました。
      1. まず、 hosting, functions, rulesのリポジトリ毎にmain, staging, feat/*のブランチを用意する。
      2. feat/* ブランチ からstagingブランチにPRマージされたタイミングで*.*.*+stgというタグを作成(github actions)
      3. 同時にstagingブランチからmainブランチへのPRを作成(github actions)
      4. mainブランチへPRマージされたタイミングで *.*.*というタグを作成
      5. firebaseリポジトリのリリースを作成し、そのリリースコメントにhosting, functions, rulesのタグを指定する。
      6. firebaseリポジトリにリリースが作られたタイミングでコメントを確認し、自動デプロイを行う(github actions)(mainブランチに対してのリリースであれば本番に、stagingブランチへのリリースであればstagingにデプロイする)
    • 以上のようにすることで、デプロイのログを残しつつ、整合性を取れるようにしました。
    • 余談ですが、slackのコマンドを使い、むき先やデプロイ理由、それぞれのバージョンを指定をしてリリース作成をするようにし、また誰がコマンドでデプロイしたかをデータに残すようにも実装しました。
  • 使っている他のパッケージのバージョンがあがった時、特にプライベートパッケージに更新があった時のアップデートが面倒になるのでは?
    • 確かにそれはありますが、ただ、一つのリポジトリでの管理の場合はその度に対象になっていない他の機能への影響を考えなくてはいけないリスクやストレスよりはこちらの方が良いと考えましたし、renovateなどを利用することで、幾分か緩和できると考え総合的に分けた方が良いと判断しました。
  • 結局eslint, jest, prettierをそれぞれで入れてるよね?
    • はい、確かにそうですが、一つのリポジトリでhosting, functions, rulesにそれぞれで存在するのと比べると違和感がなくなり、ストレスがなくなりました。

メリット

  • リポジトリ単位でのコード量が減るのでコード全体を把握しやすい。
  • hosting, functions, rulesそれぞれで最新の状態を維持できる。
    • 同じリポジトリで管理すると、少なからず他への影響を常に考えないといけない。
  • CIの中身がシンプルになり、可読性が上がる。
    • リポジトリの用途用のみのCI処理になるため、何に対してのものなのかが分かりやすくなる。

最後に

以上がこれまでの経緯とその対策内容です。
読む方によっては色々ご意見あるかと思いますので、もしよりベターな方法があれば是非参考にしたいので、教えてください!

Discussion