Firebaseでサービスを運用して2021

本投稿は TECOTEC Advent Calendar 2021 の3日目の記事です。

次世代デジタル基盤開発事業部の飯田です。

昨年に引き続きFirebaseでサービスを運用してきた中でのあれこれをご紹介したいと思います。

Cloud Functions のリソース問題

Cloud Functions を使って作成したAPIの負荷テストを行っていた際にタイムアウトで終了するものが不定数出てくる現象に遭遇しました。
最初はAPI内の処理で時間がかかってタイムアウトしているのかと調べていたのですが、実行後のログを見ていたところファンクションが起動してから処理が始まるまで進まずにタイムアウトしていることが読み取れました。

通常処理時のログ(起動直後に出しているログが出力されている)
通常処理時のログ(起動直後に出しているログが出力されている)

タイムアウトしていたときのログ(起動直後に出しているログが出ていない)
タイムアウトしていたときのログ(起動直後に出しているログが出ていない)

サポートへ問い合わせたところ「Cloud Functions はリージョン毎にリソースに限界がある」との回答をいただきました。
また「リソースは共用であるためその時の状況によって限界となる数が変わる」とのことで、タイムアウトとなる数が不定数だったのはこのことが要因となっていたようです。
対応策としてサポートの担当者から以下が提示されました。

  • ファンクションを複数のリージョンへデプロイしてロードバランサーで分散させる
  • GAEへファンクションを移行して「同時リクエスト数」を調整することで1インスタンスで処理できる数を増やす

今回はGAEへ移行することで対応することにしました。
クラウドサービスだと把握できないリソースに関する問題が突然でてくる事があるという一例でした。

Firebase SDK を v9 系に更新した

長いことサービスを運用しているとライブラリをバージョンアップするタイミングがなかなか取れないですよね。
FirebaseのSDKは結構頻繁にバージョンが更新されるのでその都度更新すると更新頻度が多くなってサービスに影響が出たりするのでなかなかできません。
そう考えて更新をせずに1年ほど経過したところ「 v9 系から使い方が変わるよ!」というリリースノートが・・・

f:id:teco_iida:20211130094503p:plain

アップグレードガイドを読むとv9系からはモジュラー形式の呼び出し方法に変わったらしく、その方法に変えるとアプリの容量が削減できるとのことでした。

firebase.google.com

ただv8系以前から互換性のある利用方法も用意してあり、その方法だと容量は削減できないが移行は簡単でした。
こちらはインポート時に「compat」をつけるだけで良いので簡単ですね。
使い方が変わるというのにちょっとびびってましたが、それほど時間がかからずに対応できたのでほっとしました。
(本当は容量を削減するためにモジュラー形式の呼び出しに変える必要があるのですが、それはまた別の機会にでも・・・)

Firestore rules のテストライブラリを新しくした対応のこと

上記でSDKのバージョンアップを行っているときに「@firebase/testing」が廃止されて「@firebase/rules-unit-testing」へと移行することになってたのに気付いてしまいました・・・
(Firestoreのルールのテストで使用していました)
こちらも変わってから1年ほどたっていましたね・・・
まぁ、ついでなのでこちらも移行しようかと作業を進めてたのですがSDKと比べて変更箇所が多かったので少し手間取りました。
こちらもSDK同様モジュラー形式に変わっているようでインポート時の記述が変更されていました。
またそれに伴い内包されていた「firebase」や「firebase-admin」などのライブラリを別途インポートする必要がありました。

コード上で修正したのをいくつか下記で抜粋して説明します!

  • 初期化方法が変わった
    「loadFirestoreRules」だったが「initializeTestEnvironment」に変わりエミュレータの接続先の指定も必要になりました。
// before
async loadRules() {
  return firebaseTest.loadFirestoreRules({
    projectId: this.getProjectID(),
    rules: this.rules,
  })
}
// after
async loadRules() {
  this.testEnv = await initializeTestEnvironment({
    projectId: this.getProjectID(),
    firestore: {
      rules: this.rules,
      host: '127.0.0.1',
      port: 8080,
    }
  })
}
  • 認証情報付きのFirestore接続オブジェクトの取得方法が変わった
    以前は初期化時にuidを指定していたのが、初期化後に取得できるオブジェクトの専用メソッドで取得するようになりました。
// before
getFirestoreWithAuth(auth) {
  return firebaseTest
    .initializeTestApp({
      projectId: this.getProjectID(),
      auth: auth,
    })
    .firestore()
}
// after
getFirestoreWithAuth(uid) {
  if (uid == null) {
    return this.testEnv.unauthenticatedContext().firestore()
  } else {
    return this.testEnv.authenticatedContext(uid).firestore()
  }
}
  • モックデータの読み込み方法が変わった
    以前は管理者権限を持ったFirestore接続オブジェクトを使用して読み込んでましたが、ルールを無視する権限を持ったFirestore接続オブジェクトが取得できるメソッドを使う形になりました。
// before
getAdminFirestore() {
  return firebaseTest.initializeAdminApp({ projectId: this.getProjectID() }).firestore()
}

// テスト側で上記のメソッドで取得したFirestore接続オブジェクトを使ってモックデータを読み込み
  beforeEach(async () => {
    await createMockAdminData(provider.getAdminFirestore())
  })
// after
// 共通クラスでモックデータを読み込むメソッドを定義
async loadMockAdminData() {
  await this.testEnv.withSecurityRulesDisabled(async context => {
    await createMockAdminData(context.firestore())
  })
}

// テスト側でモックデータを読み込むメソッドを定義してコールバックとして渡す
async loadMockData(callback) {
  await this.testEnv.withSecurityRulesDisabled(async context => {
    await callback(context.firestore())
  })
}
  • テスト終了時のクリーンアップ処理がわかりやすくなった
    専用のメソッドが用意されたので呼び出すだけになりました。
// before
async cleanup() {
  return Promise.all(firebaseTest.apps().map((app) => app.delete()))
}
// after
async cleanup() {
  await this.testEnv.clearFirestore()
}
最後に

前回の記事から1年たち、Firebase関連のSDKやサービスなどは更新が早くキャッチアップが遅れがちですが、その分どんどん便利になってきているのが感じられます。
新しい機能もどんどん追加されているのでそのうち試してみたいですね!

PR

tecotec.co.jp