【開催レポ】Security Engineering Casual Talks #1

こんにちは。インフラストラクチャー部セキュリティグループの水谷(@m_mizutani)です。2018年10月31日にクックパッドにて Security Engineering Casual Talks #1 を開催しました。

f:id:mztnex:20181108151253j:plain

sect.connpass.com

セキュリティに関する様々なトピックが議論されている昨今ですが、「実際のところサービス開発や運用にどうセキュリティを組み込んでいるのか」という話はまだなかなか表にでてこない、と感じる方もいらっしゃるのではないでしょうか。各種製品・サービスによってセキュリティを高めるための手段は増えてきましたが、それを自社のサービスやプロダクト、社内のセキュリティ運用にどう役立てているか、あるいはどのような工夫をして運用されているのかの知見は組織に閉じてしまっていることがまだまだ多いかと思います。

この会は「開発や運用に実際携わっているエンジニア」があつまって「現場におけるセキュリティに対し、エンジニアはどのようにアプローチしているのか」にフォーカスし、ビールでも飲みながらざっくばらんに話せるように、ということを目的として開催しました。

第1回は「クラウドサービスでつくるセキュリティ」と題してクックパッドを含む3社からの発表がありました。各クラウドサービスが提供しているセキュリティ機能をどのように実環境で構築・実用化し、どのように運用しているのか?という実情について話をしました。

[発表1] 自由でセキュアな環境のつくりかた by @kani_b

1件目の発表は弊社の星(@kani_b)が「自由でセキュアな環境のつくりかた」と題して、いかにセキュリティを維持しつつ開発者が自由にAWSを使える環境を作るかというテーマについて話しました。

[発表2] 踏み台環境のあれそれ by @ken5scal

2件目の発表はFolioの @ken5scal さんによる「踏み台環境のあれそれ」で、クラウド環境における踏み台を Folio でどのように構築しているかという話をしていただきました。Folioでは Teleport というリモートアクセスをサポートするツールを活用して実際の運用をされているとのことでした。

残念ながらsshから完全に開放されるわけではないとのことでしたが、WebUIから使えるコンソールを利用したりロールなどに基づいたログインの制御ができる点でメリットがあるようでした。まだAWS環境との密な連携ができなかったり、Enterprise版でしか使えない機能もあったり、構築・管理・運用での課題もなくはないので完璧というわけではないようですが、参加者の皆さんも反応をみるとかなり実用的に使える段階に来ているのではないか、ということを実感できているようでした。

[発表3] 開発現場で使えるAWS KMS by okzk

3件目の発表はCyberAgentのokzkさんによる「開発現場で使えるAWS KMS」でAWSのKMS、およびAWS System Manager の Parameter StoreやAWS Secrets Managerについてご紹介いただきました。それぞれで秘匿値を扱う際のポイントやtipsなどを共有してもらいました。

個人的に面白かったのはKMSやSecrets Managerによって秘匿値を取り出す実装をアプリに持たないようにしているということです。ではどうするかというと実際にKMSやSecretsManagerにアクセスする実装 env-injector が秘匿値を取得し、アプリとなるプロセスを起動させる際に環境変数で渡すようにしているとのことでした。これによってアプリ側では実行環境に依存せず、透過的に環境変数から秘匿値を引き出せるため実装がシンプルになって管理しやすくなるというメリットがあり、実践的で参考になるtipsだなと感じました。

次回開催に向けて

懇親会中に「次回以降で聞きたいネタをホワイトボードに書き込んでください」というお願いをしてみたところ、想像以上にいろいろなトピックが集まりました。どのトピックも「で、実際のところどうなのよ」というのがなかなか見えにくく、気になる方は少なくないのではないかなと想像しています。

  • データ暗号化ってぶっちゃけどうしてるの(対象、方法とか)
  • エンドポイントセキュリティ・FIMどうしてますか
  • DBのクエリログとってますか、監査してますか
  • 予算どう確保&上に説明していますか
  • マイクロサービスとセキュリティ
  • CI、CDの文脈におけるセキュリティ、監査
  • FaaSの権限管理
  • セキュリティ部署の立ち上げ、各サービスのセキュリティを保障する方法
  • docker imageとかLambdaの脆弱性診断
  • 開発環境のアクセス制限
  • 開発者にどれくらい権限を渡しているか@本番環境
  • どれくらいアップデート作業をちゃんとやってますか
  • 複数サービスの権限管理
  • 誰に本番のアクセス権限を渡すのか(ルール・選別・etc)
  • 固定IP以外での接続制限(クライアント証明書以外)
  • 社員のID管理
  • イケてる社内NW

今後も参加していただいた皆さんにあげていただいたトピックを中心としてSecurity Engineering Casual Talksを開催していきたいと考えています。次回は年明けあたりで開催したいと考えていますので、興味のある方はぜひご参加いただければと思います!!

f:id:mztnex:20181108151736j:plain f:id:mztnex:20181108151853j:plain

Cloud Firestoreのrulesのテストを全てローカルエミュレータを使うように書き換えた話

Komerco事業部エンジニアの岸本(id:sgrksmt)です。
先日Firebase Summit2018が催され、その中でCloud Firestore(以下Firestore)とRealtime Databaseにローカルエミュレータがβ版として追加されたという発表がありました。
Komercoでは、前回投稿した記事の通り、テスト用のfirebaseプロジェクトを立てて、そこにrulesをデプロイし、オンラインテストといった形でrulesをテストしていましたが、
全てローカルエミュレータを用いたrulesのテストに書き換えました。

今回はローカルエミュレータを用いたFirestoreのrulesのテストの話をします。

使うと何が変わるか

ローカルエミュレータを使ったrulesのテストに切り替えることによって、良い点がいくつかでてきます。

  • テストを実行するためのfirebaseプロジェクトを別途用意する必要がなくなる
  • firebaseプロジェクトにrulesをデプロイしてからテストする必要がなくなる
  • オンラインテストと比べてテストの所要時間が短くなる
  • 開発環境のバッティングによってテストが壊れることがなくなる

特にテストの時間が短くなることと、開発環境のバッティングによってテストが壊れることがなくなるのは大きいです。
KomercoではPull Requestが出たり、masterにマージされたタイミングでにCIでテストを実行しているのですが、rulesの変更が伴う機能開発や改善があると、 CIで用いるfirebaseプロジェクトは1つなのでCIの走るタイミングによってはrulesが違うものに変わってしまいテストが通らない...なんてこともありました。
ローカルエミュレータを使うことでそれらの問題から解放され、デプロイ前に手元でテストができるようになります。
変更前と後を図で示すとこのようになります。

  • Before

f:id:sgrksmt:20181105134322j:plain

  • After

f:id:sgrksmt:20181105134349j:plain

また、テストの所要時間ですが変更前後でこのように変わりました。(各5回ずつ実行の平均値を取っています。)
テストケースはおよそ60ケースほどです。

Before After
約50秒 約13秒

テストケースの大きさ、複雑度によって変わりますが、1/3ほどに減らせたのはとても大きい改善となりました。

実際に使ってみる

実際にFirestoreのローカルエミュレータを使ったテストを書くところまで説明していきます。
公式のドキュメントはこちらにもあるのですが、細かく解説していきます。

※なお、この記事の投稿時点ではβの機能となっています。

事前準備

toolsの更新とemulatorのインストール

まず最初に、firebase-toolsのバージョンは6.0.0以上である必要があります。

npm i -g firebase-tools

あるいは、package.json等でバージョン管理している場合は

"devDependencies": {
  "firebase-tools": "^6.0.0",
}

のように記述し、firebase-toolsをインストールあるいはアップデートします。
その後、emulatorの準備をします。

firebase --open-sesame emulators
firebase setup:emulators:firestore

firebase --open-sesame emulators をすると、 firebase login で再ログインを求められることがあるのでその場合は再ログインします.
CIの場合は基本的に FIREBASE_TOKEN を使って各種コマンドを実行していると思うので、firebase login をし直すのは不要かと思います。

ここまで成功したら、firebase serve --only firestore を実行し、プロセスが起動してlocalhost:8000が立ち上がるのを確認します。

$ firebase serve --only firestore
✔  firestore: started on http://localhost:8080

Dev App Server is now running.

API endpoint: http://localhost:8080

.gitignoreに追加

firebase serve --only firestore を実行すると、firestore-debug.logが吐き出されるので、不要ならignoreします。

# .gitignore

firestore-debug.log

firebase/testingの追加

package.jsonにfirebase/testingを追加し、インストールをします。

  "devDependencies": {
    "@firebase/testing": "0.3.0"
  }

ここまで問題なければ事前準備は終了です。

firebase/testing

firebase/testingモジュールを使うことで、firestoreの読み書きのテストを書くことができるようになります。 テストを書くのに使用するインターフェースを紐解いていきます。

projectIDについて

firebase/testingを使ってテスト用のFirebaseのAppを作成します。
その時にprojectIDを指定して作成するのですが、このIDはテストケース毎に一意である必要があります。
ドキュメントには

The Cloud Firestore emulator persists data. This might impact your results. To run tests independently, assign a different project ID for each, independent test. When you call firebase.initializeAdminApp or firebase.initializeTestApp, append a unique ID, timestamp, or random integer to the projectID. You can also resolve race conditions by waiting for promises to resolve through async or await keywords in JavaScript.

と書かれており、エミュレータ起動中は作成したデータを保持しているので、異なるテストケースで同じIDを使ってFirebase Appを立ててアクセスしてしまうと、他のテストケースで作成したデータが混じっていてテスト結果に影響を及ぼす可能性があります。
なので、projectIDを作成するときはタイムスタンプや乱数を含めて一意にすると良さそうです。
公式のサンプルでは次のようにprojectIDを作っています。

// 一部だけ抜粋
import * as firebase from '@firebase/testing';
const projectIdBase = 'firestore-emulator-example-' + Date.now();

let testNumber = 0;

function getProjectId() {
  return `${projectIdBase}-${testNumber}`;
}

class TestingBase {
  async before() {
    // Create new project ID for each test.
    testNumber++;
    await firebase.loadFirestoreRules({
      projectId: getProjectId(),
      rules: rules,
    });
  }
}

テストケース毎に、

firestore-emulator-example-[テスト開始時のタイムスタンプ]-[テスト番号]

という文字列でprojectIDを生成しています。testNumberはbefore(Each)が実行されるときにインクリメントしています。
このように一意となるprojectIDを生成すれば、テスト時に問題になることはないでしょう。

rulesファイルをロードする

エミュレータにprojectIDを指定してrulesを読み込ませます。 先のコードの例にもありましたが、このようにしてrulesの内容を文字列として渡してあげます。

import * as firebase from '@firebase/testing'

const rules = fs.readFileSync('firestore.rules', 'utf8')
await firebase.loadFirestoreRules({
  projectId: getProjectId(),
  rules: rules
})

今後はこのprojectIDと一致するFirebaseのAppを作成することで、そのAppはここでロードしたrulesを見るようになります。(後述)

ProjectID、authを指定してFirebaseのAppを作成する

テストで用いるFirebase Appを作成する場合は、initializeTestAppを使います

const db = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'user' }
}).firestore()

引数にはprojectIDと、auth情報を渡します。
注目すべき点は、認証情報を変えた複数のFirebase App(Firestore)を用意できる点です。

今までのrulesのテストの場合、認証ユーザーを切り替えながらデータを作成するときは次のように一々signInとsignOutを繰り返していました。

// userAとして認証してモデルを作成
const userA = await auth.signInAnnonymously()
firestore.collection('post').doc().set({ title: 'test1', body: 'xxxx', author: userA.user.uid })
await auth.signOut()

// userBとして認証してモデルを作成
const userB = await auth.signInAnnonymously()
firestore.collection('post').doc().set({ title: 'test2', body: 'xxxx', author: userB.user.uid })
await auth.signOut()

firebase/testingを使う場合はユーザーごとにfirestoreを分けることができ、

const userADB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'userA' }
}).firestore()
const userBDB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'userB' }
}).firestore()

// 認証したuserAで書き込みをする
userADB.collection('post').doc().set({ title: 'test1', body: 'xxxx', auhtor: 'userA' })
// 認証したuserBで書き込みをする
userBDB.collection('post').doc().set({ title: 'test2', body: 'xxxx', author: 'userB' })

このように書くことができます。
signInしたりsignOutしたりがなくなるので見通しがよくなりますし、「userAで作成したドキュメントのpathを基に、userBでは書き込みができない」、といったテストを以前より簡単に書くことができるようになります。
また、認証が通っていないユーザーとして振る舞いたい場合は、auth パラメータにnullを渡します。

const unAuthedDB = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: null
}).firestore()

これで認証の通っていないユーザーに対するテストもかけるようになります。

admin権限でFirebaseのAppを作成する

テストを書くときに、事前にFirestoreにテストデータを入れたい場合があります。このときに、rulesの影響を受けずにデータを作成したり、作成したデータを読み出したい場合には、admin権限でFirebaseのAppを作って利用します。

const adminDB = firebase.initializeAdminApp({
  projectId: this.getProjectID()
}).firestore()

後始末

各テストケース実行後は、そのテストケース内で作成したFirebase Appを削除する必要があります。

await Promise.all(firebase.apps().map(app => app.delete()))

これらの処理をclassにまとめてモジュール化

rulesファイルのロードから、各テスト実行ごとに一意のProjectIDを作成してAppを作ってfirestoreを利用する処理を、classとしてまとめてimportできるようにすると便利になります。
KomercoではFirestoreTestProviderというクラスを作り、各テストファイルでimportしています。

import * as firebase from '@firebase/testing'
import * as fs from 'fs'

export default class FirestoreTestProvider {
  private testNumber: number = 0
  private projectName: string
  private rules: string

  constructor(projectName: string, rulesFilePath: string = 'firestore.rules') {
    this.projectName = projectName + '-' + Date.now()
    this.rules = fs.readFileSync(rulesFilePath, 'utf8')
  }

  increment() {
    this.testNumber++
  }

  private getProjectID() {
    return `${this.projectName}-${this.testNumber}`
  }

  async loadRules() {
    return firebase.loadFirestoreRules({
      projectId: this.getProjectID(),
      rules: this.rules
    })
  }

  getFirestoreWithAuth(auth?: { [key in 'uid' | 'email']?: string }) {
    return firebase.initializeTestApp({
      projectId: this.getProjectID(),
      auth: auth
    }).firestore()
  }

  getAdminFirestore() {
    return firebase.initializeAdminApp({ projectId: this.getProjectID() }).firestore()
  }

  async cleanup() {
    return Promise.all(firebase.apps().map(app => app.delete()))
  }
}

エミュレートしているfirestoreへの書き込みの成功失敗を評価する

書き込みが成功する/失敗するのを評価する場合は、assertSucceedsassertFailsを使います。

const db = firebase.initializeTestApp({
  projectId: getProjectId(),
  auth: { uid: 'user' }
}).firestore()
await firebase.assertSucceeds(db.collection('users').doc('user').get())

ちなみに、assertSucceedsassertFailsの実装はこちらで確認ができます。
Succeedsは単純にpromiseを返し、failsの場合はPromiseが成功したらエラーを返し、失敗したらエラーを値として返すようにしています。(成功と失敗を反転させている)
これは使っても使わなくても問題ありません。(jestの場合はexpectで評価することもできますし。)

テストを書く

先程のFirestoreTestProviderと、テストフレームワークであるjestを使ってテストを実際に書いてみます。
rulesとテストケースはCloud Firestore emulator quickstartと同じもので組んでみました。

import * as ftest from '@firebase/testing'
import FirestoreTestProvider from './firestoreTestProvider'

const testName = 'firestore-emulator-example'
const provider = new FirestoreTestProvider(testName)

function getUsersRef(db: firebase.firestore.Firestore) {
  return db.collection('users')
}

describe(testName, () => {
  beforeEach(async () => {
    provider.increment()
    await provider.loadRules()
  })

  afterEach(async () => {
    await provider.cleanup()
  })

  describe('users collection test', () => {
    test('require users to log in before creating a profile', async () => {
      const db = provider.getFirestoreWithAuth(null)
      const profile = getUsersRef(db).doc('alice')
      await ftest.assertFails(profile.set({ birthday: 'January 1' }))
    })

    test('should let anyone create their own profile', async () => {
      const db = provider.getFirestoreWithAuth({ uid: 'alice' })
      const profile = getUsersRef(db).doc('alice')
      await ftest.assertSucceeds(profile.set({ birthday: 'January 1' }))
    })

    test('should let anyone read any profile', async () => {
      const db = provider.getFirestoreWithAuth(null)
      const profile = getUsersRef(db).doc('alice')
      await ftest.assertSucceeds(profile.get())
    })
  })
})

これで、 firebase serve --only firestore を実行してエミュレータを起動した上で、jestを実行するとテストが動きます。
サンプルはこちらにあります。

最後に

ローカルエミュレータは投稿時点ではβ版ということですが、とても強力かつ手軽に扱えるのでこれからテスト書こうかなとか、以前僕が書いた記事のようにテスト書いてる場合は是非検討してみてはいかがでしょうか。

ローカルエミュレータ以外にも様々な発表があったので、キャッチアップしていきながら導入や実践できそうなものがあればどんどんKomercoで試していこうと思います。

参考

KomercoアプリでFirebaseからの画像取得を速くした話

こんにちは。Komerco事業部エンジニアの高橋(id:yosuke403)です。

Komercoは、「料理が楽しくなるマルシェアプリ」をコンセプトに、料理が楽しくなる器やカトラリー、リネン雑貨等を出品/購入できるサービスです。現在はiOS版のアプリケーションを提供しています。

komer.co

Komerco - コメルコ - by クックパッド

Komerco - コメルコ - by クックパッド

  • Cookpad Inc.
  • ショッピング
  • 無料

先日、Komercoアプリの画像表示の速度を改善したので、それについて書こうと思います。

背景と成果

Komercoで商品を選ぶユーザにとって、商品画像は当然重要なものです。 しかし以前は、アプリを起動してみると画像の表示が遅く、商品一覧をスクロールするとしばらく経ってから画像が表示される状況でした。

こちらは改善前のバージョンで、会社のWiFiに接続し、初回起動(キャッシュなし)から新着商品一覧を表示したときの様子です。

f:id:yosuke403:20181101175645g:plain

セルが表示されても画像が表示されるまで少し時間がかかり、特に素早くスクロールされると画面上のセルが全てプレースホルダーになってしまいます。

次に改善後のバージョンで同環境で操作した様子です。

f:id:yosuke403:20181101175724g:plain

プレースホルダーはほとんど表示されなくなりました!

原因

商品画像に関するシステム構成

KomercoはFirebaseを利用したサービスで、データベースとしてCloud Firestoreを、画像データの置き場としてCloud Storageを使用しています。

f:id:yosuke403:20181101175944p:plain

Firestoreはデータをドキュメントという単位で扱い、ドキュメントには数値や文字列といった値を格納することができます。また、ドキュメントはコレクションというグループでまとめられています。

Komercoの場合、「Product」コレクションには商品名や画像のIDを持つドキュメントが格納され、「Image」コレクションにはCloud Storageのリファレンスを持つドキュメントが格納されています。画像のIDはImageドキュメントのIDに対応しており、これによりアプリはProductドキュメントから、対応するImageドキュメントを取得できます。

画像取得のフローに原因

商品一覧画面が表示されてから商品画像を取得するまでのフローは次のようになっています。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageIDからImageドキュメントをFirestoreから取得
  • ③ ImageドキュメントのstorageRefから画像のURLをCloud Storageから取得
  • ④ Cloud Storageから画像データの取得

f:id:yosuke403:20181101180034p:plain

ここから分かるように画像を取得するまでにFirebaseと何度か通信する必要があります。 しかも②〜④についてはセルの表示ごとに発生するため、素早く商品一覧をスクロールした際などは、リクエストが大量に発生して処理に時間がかかっていました。 加えて、個々のリクエストについてもレスポンスが遅く、こちらもリクエストが詰まる要因になっていました。

既存の対応策

この課題は以前から認識しており、対策としてキャッシュを利用したリクエスト数の低減をすでに行っています。 一度取得したImageドキュメントや画像データをローカルにキャッシュして、何度も通信が発生しないようにしています。

speakerdeck.com

しかし初回起動時や久しぶりに起動したときなど、キャッシュがない状態のときはやはり画像取得は遅い状態でした。

そこで今回は、キャッシュに無い画像データを取得する際に必要な通信回数を減らすことと、各通信自体を速くすることを考えました。

改善内容

改善後の画像取得フロー

ProductドキュメントにimageURLという画像のURLを格納するフィールドを追加しました。 これによりフローは次のようになりました。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageURLから画像データを取得

画像取得に必要な通信は②のみになりました。また、②の通信速度もかなり改善しています。

f:id:yosuke403:20181101180132p:plain

以下では行ったことについて詳しく説明します。

Productドキュメントに画像のURLが付与されるようにする

元々画像データを取得する際は、Imageドキュメントを介することで、Cloud Storage上の画像データの在り処を他ドキュメントと共有したり、Imageドキュメントから得られる画像リサイズの完了イベントを取得したりすることができました。しかし今回はパフォーマンスを優先し、Imageドキュメントを介さず、Productドキュメントに付与された画像のURLを使うようにしました。これにより、改善前の画像取得フローの②と③の通信を省略することができました。

ここで言う画像のURLは、以前のフローの③で得られるダウンロードURLとは違うのですが、理由は次の節で説明します。

画像のURLの付与はCloud Functionsを利用しました。 Cloud FunctionsではFirestoreのドキュメントの作成・更新をトリガーに処理を実行できます。 これを利用して、Productドキュメントに更新がかかったときにimageIDをチェックし、変化があれば画像のURLを取得してProductドキュメントに付与します。

f:id:yosuke403:20181101180154p:plain

Cloud Functions側の実装は概ね次のようになっています。 これはProductドキュメントの新規追加時の例ですが、更新の際もimageIDが変更されたかをチェックしている以外は同じような実装になります。

export const productCreated = functions.firestore.document('/Product/{productID}').onCreate(async (snapshot, context) => {
  const db = firebase.firestore()
  const imageDoc = await db.collection('Image').doc(product.data.imageID).get()
  return product.ref.update({
    imageURL: makeImageURL(imageDoc.ref.id, imageDoc.data))
  })
})

Cloud Functionsを利用するメリットは、Productドキュメントを更新するクライアントのアップデートが不要であることです。 Komercoでは購入用のKomercoアプリとは別に出品用のアプリがあるのですが、出品用アプリはimageURLの存在を気にする必要はありません。 そのため、このアプリの強制アップデートはせずに今回の対応が可能でした。

画像データを取得する時間を短縮する

全体的にレスポンスが遅い原因はリージョンの問題でした。 画像データについては、デフォルトの「Multi-Regional 米国」ではなく「Regional アジア太平洋(東京)」に画像データを移すだけで、かなり改善しました。

さらにFirebaseのCloud Storageではなく、その背後にあるGCPのCloud Storageを直接見に行くようにすると更に速くなりました。 これは、Firebaseではリクエストの検証をし、アクセス権の有無を確認しているためと思われます。 今回の商品画像は公開しても問題なかったので、全ユーザに閲覧権限をつけて公開状態にしています。 GCPのCloud StorageのURLはFirebaseから取得できないので、Imageドキュメントの内容を基にCloud Functions自身がURLを生成してProductドキュメントに付与します。

ちなみに、Firestoreも「Multi-Regional 米国」のためにレスポンスが遅いのですが、現状では東京リージョンは利用できません。 リリースは予定されている模様なので、もう少し待ちたいと思います。

改善結果の数値

改善前のユーザのデータは無いのですが、手元のビルドで計測してみたところ、セルの表示が始まってから画像が表示されるまでに平均1.5秒ほどかかっていました。 これは会社のWiFi環境で計測したので、実際ユーザの手元ではもっとかかっていると思います。

改善後については、画像データ取得自体の時間は中央値で66ミリ秒程度、95パーセンタイル値で841ミリ秒でした。 画面上での表示も概ねこれに近い時間で行われていると思います。

f:id:yosuke403:20181101180220p:plain

最後に

Komercoは6月にリリースしたばかりのサービスで、改善の余地はたくさんある状態です。 作りたい機能もまだまだありますが、こういったサービスのパフォーマンスの改善も怠らないようにしたいと思います。

また、先日Firebase Summit 2018が催され、様々なニュースが発表されましたね。

firebase.google.com

個人的にはFirestoreのlocal emulatorと、Management APIが気になっています。 進化するFirebaseの機能を積極的に導入して、よりスピーディーにサービスを成長させていきたいと思います!