takuwan's blog

感じたことを書きますよ

Firestoreのオペレーションで困っていること3選

Cloud Firestoreを採用した開発をはじめて1年半ほど経ちました。GoogleさんはFirestoreにちゃんと投資してくれていて、年々進化していますね。Firestoreを使った開発で課題に感じていたことは、この前10月のFirestore Summitで大方片付いた印象です。ドキュメントも読みやすくて充実しているので、ただ開発するだけならあまり困ることもないのではないかと思います。

一方で、オペレーションに関する機能はまだ弱いので、工夫とかノウハウが必要な部分だと感じています。そこで3つ、実際に辛みを感じて工夫しているものを挙げてみようと思います。これらは、私も満足に解決できているものではありません。また、これらは人に依っては、細かい事柄だと感じるかもしれません。実際、全くのゼロイチの新規事業開発においては、事業や組織の性質に依っては、あまり考慮しなくても済む内容かもしれません。しかし、私が開発しているプロダクトは、ある程度腰を据えて開発をすることが既定路線になっていて、細かいオペレーション(とはいえ、一般的なAPI開発とかでは普通のオペレーション)にもリソースを割きたくなっているという背景があります。

Firestoreを既に利用している方で、「こうしたら良いよ」というアドバイスがある方は、ぜひtwitterとかで教えてもらえると嬉しいです。Firestoreの採用を検討している方へは、判断材料の手助けになっていたら嬉しいです。

 

 

1. マイグレーションが辛い

みなさん、マイグレーションどのように行っているのでしょうか。私は極力、マイグレーションは行わないように避けていますが、どうしてもマイグレーションを行わないといけないときがたまにあります。

Firestoreにおけるマイグレーションの辛さの原因は2つあります。1つ目は、Firestore自体がスキーマレスでマイグレーションという概念は持ち合わせていないことです。RDBのようにchange tableを気軽にできないのは、もう構造上仕方ないです。もう1つは、FirestoreはiOS・Androidのクライアントアプリが直接クエリを発行するため、アプリのバージョン(アップデート)を考慮する必要があることです。OSでアプリのバージョンを自動的にアップデートする機能を切っているユーザーは一定数いて、そうしたユーザーは古いバージョンのアプリをいつまでも使う傾向にあります。従来のWeb APIであれば、DBはマイグレーションしてもAPIのインターフェースは変えなかったり、エンドポイントのバージョンをincrementして複数バージョンを同時に走らせるといった対応を取ればよかったですが、Firestoreに乗っかったiOS・Androidアプリを開発していると、そうした選択肢を取ることはできません。

実現したいことに応じて色んなマイグレーション戦術は考えられると思いますが、基本的には下記のフローに則ってマイグレーションを行っています。

  1. 前提として、既存のフィールド(カラム)の変更・削除は行わず、新しいフィールドを生やす対応を採る
  2. 対象ドキュメントのアップデートをトリガーとしてCloud Functionsを走らせて、マイグレーション処理を走らせる(トリガーが無限ループしないように注意)
  3. マイグレーション対象のドキュメントを全て読み込み、各ドキュメントのtimestampを更新する(2の処理をトリガーする)
  4. Firestoreのセキュリティールールで、新旧フィールドが両方通るように更新する
  5. クライアントアプリを書き換え、新しいフィールドを参照する
  6. アプリの強制アップデートのタイミングで、2で設けたCloud Functionsの関数を削除、また4で書き換えたセキュリティールールを修正して新しいフィールドのみ許可する
  7. 問題なければ、旧フィールドを削除

小さいコレクション・ドキュメントのマイグレーションであれば、これで大方問題はありません。強制アップデート(6)のタイミングまで、マイグレーションの管理をしないといけないのがちょっと面倒なくらいです。しかし、コレクション内のドキュメント数が多いと辛みが出てきます。

というのも、コレクション内のドキュメント数が多くなると、コレクションを一気に読み込むことができなくなり、(3)の処理に時間がかかったり、変なスクリプトを書く必要がでてきます。うまいことクエリカーソルを使えれば良いですが、得てしてそんな都合の良いフィールドがなかったりするので、timestampを使って日・時間ごとにpagingする処理を書いていったりする必要がでてきます。(もしtimestampをフィールドに生やしていない場合は、どうやってpagingすれば良いかを考える必要があります。)各ドキュメントのサイズにも依るでしょうが、コレクション配下のドキュメント数が10万を超えるなら、メモリ的にもきついでしょうし、まずクエリがタイムアウトします。ドキュメント数に応じた時間をかけて、地道にtimestampを更新していくしかありません。

あと、マイグレーションの結果ドキュメントのフィールド数が大幅に増える場合は、セキュリティルールの制限も気をつける必要があります。私は、マイグレーションでは引っかかったことはないのですが、普通に開発をしていて「リクエストあたりの式評価の最大数」が1000であることに引っかかったことはあります(mapやarrayを多用していたため)。まあ、これに引っかかるようでは、ドキュメントの設計が間違っている可能性のほうが高くはありますが、マイグレーションのときには気にはしています。

良いマイグレーションの方法があったら、ぜひ教えて頂きたいです。

 

2. DDoS対策を満足にできない

クライアントサイド側から直接Firestoreに対するDDoS攻撃があった場合、それを防ぐ有効な手段は乏しいです。Firebase AuthenficationとFirestoreのセキュリティルールを利用して、できる限りの予防はしていますが、不十分なのでもっと良い方法はないかと模索しています。

過去に2回、FirebaseのサポートチームにDDoSに関する問い合わせをしているのですが、FirestoreにはL7のDDoS攻撃を緩和する内部的な仕組みがまだないようなのです。一方で、Firebase Authentificationの認証さえクリアすればDDoS攻撃は可能だと考えられます。もしDDoS攻撃を受けてしまった場合は、Firebaseサポートチームに助けを求める他ないように思えます。

Firestoreは、Webブラウザやスマホアプリなどのクライアントから直接クエリを叩くことができます。Cloud Functionsを介してFirestoreのクエリを発行することでクライアントからの直接的な操作を控えることも可能ではありますが(実際、セキュリティ面を懸念してそうしているプロダクトも少なからずあるようですが)、レイテンシーは長くなりがちですし、Firestoreを活かしたオフライン対応のアプリではクライアントからの直接的な操作が必須となってきます。

クライアントから直接Firestoreを操作するということは、セキュリティの仕組みはFirebase側が用意したものに則るしかありません。つまり、セキュリティルールで実現可能な範囲で、セキュリティ対策を行うしかないということになります。

しかし、このセキュリティールールでのアクセス制御は、Firebase Authentificationの認証情報を元に行います。Facebook認証、メールアドレス認証、匿名認証などの情報をもって、どのコレクション・ドキュメントにリクエストすることができるのかを判定・認可します。

これはDDoSの文脈において何を意味するかと言うと、一度Firebase Authentificationで認証さえしてしまえば、Firestoreにいくらでもクエリを投げられるということです。特に、プロダクトが匿名認証を採用している場合は、相対的に、悪意ある攻撃の被害にあいやすい状態にあると言えそうです。

DDoS攻撃を受けた場合、書き込み制約(全コレクションの書き込み速度は1秒あたり500までとか)に引っかかったり、意図的にホットパーティション問題を再現されたりして、ユーザー体験が大きく損なわれる可能性があります。もちろん、セキュリティールールがしっかり設定出来ていてサーバーレスで設計されている場合、DDoS攻撃の規模によっては問題なくリクエストを捌けてしまうかもしれません。しかし、大量のリクエストを処理するためにかかったお金の問題は事業に直結するものですし、Firebaseサポートチームのサポート内容に期待して良いのかを判断できる材料を、今のところ私は持っていません。

過去に二度、Firebaseのサポートチームにこうした見解を相談していますが、否定はされないうえ、「攻撃を緩和する手段はいまのところない。予算アラートを設定してくれ。DDoS攻撃と思われる被害にあったらすぐ連絡してくれ。」という回答をもらっており、的外れではないのだろうと認識しています。過去に開発をしていたウェブサービスで、DDoS攻撃を何度か受けた経験があることもあり、率直に、ちょっと怖いなーという印象です。

対策としてはまず、writeに属するcreate, update, deleteのリクエストであれば、リクエストごとにタイムスタンプを取得できるので、下記のようにリクエストに制限を加えることができます。これでupdateやdeleteに関しては、大量のリクエストは送れなくなります。また、ブラックリストのコレクションを作成し、ブラックリストに載っているユーザーはリクエストを許可しないといった処理も書くことができます。

Firestore security rule

readクエリでの対策方法は、リクエストごとにドキュメントを都度作成して...といった富豪的なアプローチしか思いついていません。よって対策できていません。またcreateクエリの対策も、上記ではhasTimestamp()で制約はかけていますが、本質的には気休めでしかありません。イベントソーシングで設計しているEntityとかもあり、本当は対策したいのですが、富豪的なアプローチ以外は思いついていません。

 

次に、Firebase Authentificationでは同一IPで作成できるアカウント数を制御することができます。下記のスクショはデフォルト値の100になっていますが、プロダクトに合わせた数字を設定する必要があります。こちらは、例えばなにかの中・大規模イベント会場でアプリが喧伝されたりする可能性があるならば、閾値が100だと足りないという判断もできると思います。どちらかというと、あまり大きな数字にしすぎないという意識でいれば良いと思います。

f:id:takuwan0405:20200207180113p:plain

 

最後に、DDoS攻撃を認知するために、予算の閾値を設定します。閾値を超えた場合にはアラートを飛ばすことができます。ただし、下記スクショにあるように、予算アラートの機能はあくまでアラートを飛ばすだけで、Rate Limiting(APIの利用制限)の機能ではありません。問題にはやく気づくことができるようにするための機能です。

f:id:takuwan0405:20200208012224p:plain

 

本来であればユーザー単位でのRate Limitingの仕組みだったり、IP単位のアクセス排除の機能であったり、WAFのオプションなどが付いていてほしいところではあります。AWSのドキュメントを見る限りでは、AWSのLambdaやDynamoDBにはWAFを付けれたりするそうなので、隣の芝が青く見える次第です。

ちなみに、Cloud Storage for Firebaseでも、本質的には同様のことが生じるかと思います。また、Cloud FunctionsでのDDoS対策は、Expressのミドルウェアで行うのがベストプラクティスなようです。

 

3. 監視項目で足りないものがある

Firestoreの監視は、以前は何もできませんでしたが、徐々に監視項目が増えてきました。現在は読み込み数・書き込み数・スナップショットのリスナー数・アクティブな接続数・セキュリティールールの評価数と評価結果を監視することができます。しかし、気になる異常系の捕捉やレイテンシーの計測は、まだ不十分なのかなと思います。クライアントサイドでデータを収集するする必要があります。

Firestoreのリソース状況はStackdriverで計測・監視することができます。StackdriverはFirebaseのラインナップではないため、GCPのコンソールからアクセスする必要があります。リソースの利用状況のグラフを見るだけなら、Firebaseのコンソールからでも可能です。

f:id:takuwan0405:20200208014014p:plain

Stackdriverで監視できる項目は先に述べた通りです。中でもセキュリティールールの評価結果に関しては、アラートを設定して重宝していて、基本的にDenyやErrorは極力ゼロになるように意識しています。最近の事例だと、アプリの起動直後にFirestoreのクエリを叩くと、2~3%の確率で”Missing or insufficient permissions”を返すケースがあったのですが、アラートですぐ気づけてすぐ対応できたというのがあったりします。(ルールの監視は昨年末?追加されたためか、いま見たら日本語のドキュメントだと記載がなかったのですが、英語だとちゃんと書かれてます)。

しかし、Firestoreで監視したい項目は他にもいくつかあって、特にシステムエラーなどの異常系と、レイテンシーは最低限監視したいと考えています。前者に関しては、FirestoreのSLAは東京リージョンだと99.99%と指定されているので、年間あたり1時間弱はダウンタイムが許容されていますし、実際たまに障害は起きているためです(ドキュメント)。また、Firestoreは急な負荷がかかるとエラーを返したりと(ドキュメント)、Firestoreも例に漏れなくシステムエラーが発生しうる設計であるためです。後者のレイテンシーの監視に関しては、大量のドキュメントを読み込んでレスポンスが遅くなっているreadクエリの発見や、ホットパーティション問題の発見に繋げたいためです。無駄にドキュメントが重複しないようにドキュメントIDを開発者側で指定したりする設計もあるので、そういったホットスポットがちょっとでも発生しうるコレクションは特にレイテンシが気になるところです。

前者のシステムエラーの監視については、クエリを叩くクライアントサイドでエラーをある程度捕捉することは可能です。SDKにはエラーコードが17つ定義されているので、エラーコードが具体的にどのような事象を指しているのかよくわからないものもありますが、Crashlyticsにエラーログを送っています。Stackdriverでのセキュリティールールの評価結果の監視に関しても、Stackdriver上ではErrorやDenyが発生した件数しか確認できないので、結局はCrashlyticsで具体的なエラーの内容を分析するような運用になっています。

f:id:takuwan0405:20200211041500p:plain

 

後者のレイテンシに関しては、Firebase Performance Monitoringで気になるクエリをトレースしています(ドキュメント)。特に、先に述べたような、ドキュメントIDを自動で設定せず開発者で設定しているようなコレクションのクエリに関しては、必ずトレースを仕込むようにしています(ホットパーティションに関して問題を検知したことはまだないです)。

また、大量のドキュメントを読み込むケースが生じうるクエリに関しても、トレースを仕込んでいます。DBの監視からは少々話が逸れるかもしれませんが、Firestoreのオフラインキャッシュへのクエリは必ずしも高速ではないため、という理由もあります。まあ実際のところ、一見Firestoreのクエリが遅いようにみえても、JSONのデコード(O/R Mapper)に時間を取られてるケースのほうが多かったりはします。

もっとも、こうしたFirestoreの監視は、極力アプリケーションレイヤーで担うべきではという考えがあったとしても、わからなくもないです。一般的なWeb APIとDBの監視を考えても、経験上なんだかんだWebアプリケーション側でエラーを捕捉していることは多いように思います。また、FirestoreではCPUやストレージ容量といったマシンリソースを気にする必要はほとんどないため、実際にDB側で監視しないといけないような項目は、相対的には少なかったりもします。ただ、Firestoreは他に明らかに足りない機能があってそちらに開発リソースを割かれていただけだとは思うので(最近だとEmulator Suiteとかインパクト大きいです)、今後充実してくるものだろうとは思います。

 

 

おわりに

以上、Firestoreのオペレーション周りで困っていることを3つ挙げてみました。Firestoreは着実に良くなっているので、いま私が書いたような内容も、2年後には解決していることでしょう。

Firestoreを利用した開発は、基本的には素晴らしいものです。1年半前だと、クライアントサイドに集中したい、さらにオフライン対応もしたいとなると、Firestore以外に良い選択肢はほぼない状態だったかと思います。そういった意味でも、Firestoreの存在に感謝しています。

しかし、現在であればFirestore以外にも色んな選択肢があります。DynamoDBやLambdaの資産を活かせるAmplifyは機能面ではFirestore(Firebase)に勝るとも劣らないですし、hasuraのgraphql-engineのようなものもあります。Replicacheも興味深いなと注目しています。

良くも悪くもFirebaseはマーケティング(見せ方)の部分で成功している一面があるとは思うので、流行りに惑わされず、自分のプロダクトに合ったものを採用していきたいものだなと考える次第です。特に、今回書いたようなオペレーションでの悩みは、AWSだと問題ないものもあると思います。

何かアドバイスなどあれば、twitterでお願いします。