初めに
こんにちは、株式会社 LegalOn Technologies の LegalForceキャビネ開発部でテックリードを務めている横道と申します。
私たちのプロダクト、「LegalForceキャビネ(以下キャビネ)」では Google Firebase を使用しています。
この Google Firebase を、実際のプロダクト開発と運用で使用した際に生じた課題と対応ついて、「Firebase 使用上の注意 Functions 編」「同 Firestore 編」の 2 つに分けてお送りします。 今回は Functions 編として、Firebase Functions を使ったプロダクトが、規模の増大と機能が増加していった際に、どのような課題が生じ、そしてどのような対応を行ったかを共有します。
スタートした段階では、キャビネの開発チームも小さく、サービス自体もどれくらい普及するのかわかりませんでしたので、スモールスタートを行うこととしました。さらに少ない人数で迅速に開発することを目指し、以下の要件を設定しました。
- チームの人数を抑えるために、Front-end と Back-end を同じ言語で開発したい。
- スケジューリングタスク、イベント実行、REST API のデプロイを簡単に行いたい。
- REST API サーバーの運用を簡単に行いたい。
- データベースの運用を簡単に行いたい。
これらの要件を満たすものとして、Google Firebase を選択しました。
Google Firebase は以下のコンポーネントの集合体です。
- Firebase Functions
- Google Cloud Functions を元とした、JavaScript の Serverless 実行環境
- Firebase Cloud Firestore
- NoSQL ドキュメントデータベース
- Firebase Hosting
- 静的 Web ページの Hosting
- Firebase Authentication
- ユーザー認証機能の提供
これらのコンポーネントにより Google Firebase は以下のような特徴を持っています。
- API を Functions に JavaScript で構築できる。これにより、REST API サーバーの運用が簡単になり、Front-end と Back-end を TypeScript で共通で開発できる。
- データベースとして、1 プロジェクトに 1 つ Firestore データベースを割り当てることができ、さらにそのデータベースは自動的にスケーリングされる。
- スケジューリングタスク、イベント実行の定義をコード上に書くだけで自動的にデプロイされる。
これらの特徴により、初期段階の要件を満たしていましたので、Firebase を使い、キャビネの開発が始まりました。
しばらくして、キャビネの導入が進み、ユーザー数が多くなり、プロダクトの機能も増えてきて、Back-end の API が 100 個を超えるようになってきた際に、Firebase の運用で以下のような課題が生じました。
- デプロイが規模に合わせて遅くなる
- デプロイが途中でエラーになる。規模が大きくなるとエラーになる確率が上がる
- 分割デプロイが簡単には使えない
- Functions のメモリの設定が正しく反映されない
- firebase tools のバージョンを簡単に戻せない
これらの課題の詳細を共有しますので、これから Firebase を使用する方々、現在使用している方々に役立てていただければと思います。
デプロイが規模に合わせて遅くなる
まず、大きな課題はデプロイに要する時間です。
Firebase では、デプロイする Function の数、使用しているライブラリのサイズに合わせて、デプロイ時間が長くなります。これは、サーバーへアップロードするコンテナイメージを作る際に、npm install にて、JavaScript のライブラリのインストールを毎回行い、そして、Functions の個数分、設定を変更する処理を行うためです。
私たちのプロダクトでは、前述の通り 100 個ほどの REST API があり、それぞれが Functions に対応しており、100 個ほどの Functions があります。その結果、Firebase のデプロイだけで、10 分程度の時間を要しています。 これは、プロダクトのリリースのイテレーション速度を上げるための大きな障害となっています。
プロジェクト内に yarn.lock ファイルがあると、コンテナイメージを作る際に npm の代わりに、yarn が使用され、デプロイ時間を若干短くすることが出来ますので、yarn を使うと良いでしょう。
デプロイが途中でエラーになる。規模が大きくなるとエラーになる確率が上がる
もう一つの大きな課題が、デプロイ時のエラーです。
Firebase では、デプロイを処理する GCP のサーバーの負荷をあげないために、デプロイ関連の API の実行回数にクォータリミットが設定されています。通常はこのリミットに到達することはほとんどありませんが、多くの Functions を持つ大きなプロジェクトをデプロイする際には、このクォータリミットに到達する場合があります。
firebase tools の deploy コマンドでは、Functions の upsert 処理や Functions の実行権限の変更など、多数の処理が実行されます。一番重要な処理である Functions の upsert 処理に対しては、クォータリミットのエラーが GCP のサーバーから返されても、リトライを行っているようです。しかしながら、その後の Functions の実行権限変更の処理では、クォータリミットのエラーが返された場合、そのままリトライされず、エラーとなります。
Functions が全く作られていない状態からデプロイを行った場合には、100 個ほどの Functions の場合は、確実にクォータリミットに達します。一度デプロイを行った状態であれば、権限変更は行われないので、クォータリミットに達する確率は低くなりますが、全く起きないわけではありません。
クオータリミットのエラーが発生した場合、Functions デプロイのどの時点でエラーが発生したのかが定かではありません。そのため権限変更のみを自前のツールで行い修正したとしても、正しく動作しない可能性があります。そこで、私たちのプロダクトでは、クォーターリミットによるデプロイエラーが発生した場合は、再度デプロイを行うようにしています。
追記
その後、弊社の SRE が firebase tools の不具合を見つけ、プルリクエストを出してくれました。
https://github.com/firebase/firebase-tools/pull/5577
結論として、クォーターリミットのエラーが発生した場合には、upsert 処理、実行権限変更の処理、どちらにおいてもリトライは行われていませんでした。リトライを行うコードは存在していますが、不具合によりそのコードが実行されておりませんでした。
firebase tools の開発チームも、プルリクエストにより不具合を認識し、違う形で修正するとのコメントをもらいましたので、近いうちに修正されるのではないかと思います。
分割デプロイが簡単には使えない
前述したデプロイ時のエラーを解消するために、Functions のデプロイを何回かに分ける分割デプロイも検討しました。GCP のドキュメントにも、プロジェクトに 5 つ以上の関数が含まれている場合は分割デプロイを推奨すると書かれています。(50 の間違いではありません。5 つです)
https://firebase.google.com/docs/functions/manage-functions
しかし、分割デプロイすると、デプロイ時間は分割した回数分増加してしまいました。これは、npm install が分割した回数分実行されてしまうためです。
また、firebase tools の一括デプロイでは、使われなくなった Functions は自動的にデリートしてくれる便利な機能がありますが、分割デプロイすると、当然この機能は使用することが出来ず、手作業でのデリートが必要になります。
これらの理由より、分割デプロイを選択することが出来ませんでした。
Functions のメモリの設定が正しく反映されない
firebase tools の deploy コマンドには、JavaScript Functions のメモリの設定が正しく反映されないというバグがありました。このバグは Version 10.3.0 にて修正されています。
https://github.com/firebase/firebase-tools/releases/tag/v10.3.0
このバグは、複数の Functions をデプロイした際に、コンテナイメージの内の Node.js を起動するためのコマンドラインのメモリ設定のオプション(--max-old-space-size
)が、全て最初にデプロイした Functions のメモリ設定と同じになってしまうというバグです。
どの Functions が最初にデプロイされるかは、Functions の追加、削除で変わり、予測することは困難ですので、このバグの発生も予測することは困難でした。
このバグの良くないところは、GCP の Functions のコンソールのメモリ設定からでは確認できないという点です。GCP の Functions のコンソール画面に表示されるメモリ設定は、コンテナイメージ自体のメモリの設定であり、Node.js の起動オプションのメモリ設定を確認する場所はありません。Functions 実行時にログなどで、Node.js の heap 情報を確認するしかありません。
さらに、私たちのプロダクトでは、このバグが修正された firebase tools を使うと、前述したクォータリミットに到達する確率があがり、デプロイが失敗することが多くなりました。そのため、簡単には firebase tools のバージョンを上げることが出来ませんでした。
私たちのプロダクトでは全ての Functions のメモリ設定をほぼ統一し、このバグが発生しないようにしました。
Firebase tools のバージョンを簡単に戻せない
firebase tools では、コンテナをデプロイする際のイメージストアに Container Registry を使用していましたが、Version 11.2.1 から、Artifact Registry を使用するようになりました。
この切り替えの結果、一度、Artifact Registry を使用してデプロイされた Functions は、Container Registry を使用してデプロイできなくなりました。
つまり、Version 11.2.1 でデプロイし、firebase tools になんらかの問題が発生した場合に、Version 10.x などでデプロイが出来なくなります。
再度古いバージョンでデプロイを行うには、一度 Functions を消してからデプロイを行う必要があります。一度 Functions を消してからデプロイを行うとデプロイ終了までの時間は API が使用できなくなり、大きなダウンタイムが発生することになります。
私たちのプロダクトでは、前述の理由により、firebase tools のバージョンを上げてしまうとデプロイが失敗することが多くなり、結果としてデプロイ時間がさらに増大してしまったので、バージョンを戻さざるを得ませんでした。そこで、一度メンテナンス時間を設けて、サービスを1時間ほど停止させ、 Functions の全体消去、バージョンダウン、再デプロイを行いました。
24x365 日のサービス提供を行う必要のあるサービスの場合には注意が必要でしょう。
まとめ
以上のような課題が発生し、運用での回避や、リトライを行い、解決してきました。
プロダクトの規模が大きくなり、Functions の個数が多くなってきてから発生し始める課題が多く、初期の段階ではこれらを予測することは困難でした。
Firebase は、デプロイが統合されており簡単にデプロイが可能で、Functions を使うことにより負荷が増大しても自動的にスケールし、デプロイ時のダウンタイムも0に出来る、など小さなプロダクトを高速に開発するには非常に便利なサービスだと思います。しかし、規模が大きくなるとこのような課題がありますので、この情報共有にて皆さんもプロダクトの予想規模を考え、Firebase を正しく使っていっていただければ幸いです。
エンジニア募集
LegalOn Technologies では一緒にプロダクト開発を行うエンジニアを募集しています。詳しくは、開発の求人一覧をご覧ください。
また LegalOn Technologies の開発組織や、今回紹介したプロダクト「LegalForceキャビネ」とは別のプロダクトである「LegalForce」についてまとめた「LegalOn Technologies にご興味を持っていただいた皆様へ」というページもありますので、合わせてご覧ください。