Firebase Authから内製認証基盤に無停止移行して年間1000万円以上削減した
症状検索エンジン「ユビー」 では、ローンチ当初から Firebase Auth (GCP Identity Platform) を使っていましたが、OIDCに準拠した内製の認証認可基盤に移行しました。
認証認可基盤そのものは m_mizutani と nerocrux と toshi0607(退職済) が作ってくれたため、僕は移行のみを担当しました。
結果として、強制ログアウトなし・無停止でビジネス影響を出さずに、年間1000万円以上のコスト削減に成功しました[1]。その移行プロセスについて紹介します。認証認可基盤そのものの紹介はあまりしません。
移行した理由
大量の匿名アカウント
ユビーでは、アクセスした全ユーザーに対して自動的に匿名アカウントを発行しています。これにより、ユーザーがアカウント登録しているかどうかに関わらず、同じID体系で透過的に履歴情報等を扱うことができます。アカウント登録する時は匿名アカウントのIDを引き継げるため、特に意識せずとも登録前のデータを引き継ぐことができ、ユーザー体験にもメリットがあります。
ユーザー体験や開発体験の面では優れていますが、コスト面で問題があります。Firebase Auth はMAUベースの料金システムになっていて、匿名アカウントも加算されてしまいます。ローンチ当初は問題ありませんでしたが、700万MAUを超えるまでにグロースした現在では、無視できないコストになっていました。
コストには直接関係しませんが、匿名アカウントを含む累計レコード数は1億を超えていました。
機能の不足
Firebase Auth は多くのユースケースで必要十分な機能を提供しています。しかし、ユビーではCapacitorでモバイルアプリを作っているなど、特殊な要件が多く無理なハックが必要になっていました。
さらに、より厳密なリスクベース認証をやりたかったり、SMS以外での二要素認証をやりたかったりと、Firebase Auth では満たせない新たな要件も見えてきています。
IDが重要なプロダクト
ユビーは、発症から治療まで(ペイシェントジャーニー)における意思決定を包括的に支援するプラットフォームです。
ユーザーひとりひとりを適切なタイミングで適切な医療に導くためには、症状、診断、服薬など様々な角度の情報を蓄積し、時系列を加味してパーソナライズした情報提供をする必要があります。その性質上、Ubieが提供するプロダクト全体を通したID管理は極めて重要で、今後の運用を加味しても基盤として投資する価値がありました。
移行の要件
ユビーでは、自動発行される匿名アカウントに加えて、LINE, Google, Apple, メールアドレスでアカウント登録することができます。これらのアカウントを、サービスダウンタイムなしで、ユーザーをログアウトさせずに移行することが要件でした。先述の通り、一貫したユーザー情報の蓄積を大切にしており、強制ログアウトに気づかずデータが分断されてしまうことは避けたかったからです。
ただし、メールアドレスについては、パスワード認証からOTP認証に切り替えることにしました。Firebase Authからハッシュ化パスワードをエクスポートすることはできますが、Firebase独自のハッシュアルゴリズム[2]を使っているため、扱いが面倒でした。ユーザーに明示的にパスワードを再設定してもらうことも考えましたが、負荷の割にメリットを感じられず、OTPで十分と結論づけました。もっとも、OTPでも認証の強度は変わらず低いため、パスキーの導入等を別途進めています。
移行プロセス
大きいリリースをできるだけ避けるため、小さくステップを切って進めました。ステップバイステップで紹介します。
0. 移行前の認証フロー
まずは移行前の Firebase Auth を使った認証フローを紹介します。本来はリフレッシュトークンなども登場する複雑なフローですが、その辺りは Firebase SDK がやってくれるので簡略化して記載します。
新しいユーザーが来訪すると、自動的に匿名アカウントを発行します。同時に発行したトークンを永続化し、バックエンドへのリクエストヘッダに乗せて認証しています。
バックエンドで作成しているユーザーレコードは、以下のような情報をDBに保存します。
- ID: このIDに紐づけて履歴等のユーザーデータを記録する
- FbAccountID: Firebase Authで発行したアカウントのID
- UbieAccountID: 内製認証基盤で発行したアカウントのID(現時点では空)
再来訪ユーザーは永続化されているトークンを使ってそのまま認証します。アカウント登録/ログイン時も同様に、Firebase側でログインした後にコールバックで渡されるトークンをフロントエンドで永続化します。
1. トークン検証をFBアカウントとUbieアカウントの両方に対応させる
まずは、APIコール時にバックエンドで行われるトークン検証をFBアカウントに対応したまま、Ubieアカウントにも対応させます。
フロントエンドからはFBアカウント/Ubieアカウント問わずリクエストヘッダでトークンを送ります。バックエンドはまずUbieアカウントのトークンを検証し、UbieAccountIDを使ってユーザーレコードを探します。失敗したら、FBアカウントのトークンにフォールバックして同じように認証を試みます。
これにより、ここからはフロントエンドが握っているトークンをUbieアカウントのものに切り替えるだけで、その後のAPIコールにおける認証処理もそのまま対応できます。
2. アクセス時に匿名Ubieアカウントを発行する
ユーザーが来訪したタイミングで匿名Ubieアカウントを発行するようにします。
匿名Ubieアカウント発行の対象となるユーザーは2種類に分けられます。
- 匿名FBアカウントで認証されているユーザー(=移行前に来訪したことがある)
- 匿名FBアカウントを持っていないユーザー(=移行前に来訪したことがない)
前者の場合はFbAccountIDから既存のユーザーレコードを取得することができます。このユーザーレコードに対して、新規に発行した匿名UbieアカウントのUbieAccountIDを紐づけることで、移行前の情報を引き継ぐことができます。
後者の場合は引き継ぐべきユーザーレコードがないため、UbieAccountIDに紐づくユーザーレコードを新規作成します。このレコードのFbAccountIDは空です。
引き継ぎ後はUbieアカウントのアクセストークンを用いた認証に切り替わりますから、ユーザーはセッションを切ることなくシームレスに移行することができます。
現時点での挙動整理
- この対応後にアクセスした匿名アカウント: 移行完了
- この対応後にアクセスした登録アカウント: Firebase Auth のまま
- この対応後にアクセスしていない匿名アカウント: Firebase Auth のまま
- この対応後にアクセスしていない登録アカウント: Firebase Auth のまま
3-1. アカウント登録/ログイン時にリアルタイム移行
この時点ではアカウント登録/ログイン処理そのものはFirebase Authのままです。ただし、そのタイミングでアカウント情報を認証基盤に送り、Ubieアカウントを新規作成します。
フロントエンドでは、Firebase Authが発行したトークンではなく認証基盤で発行したトークンを握るようにすることで、シームレスにセッションを移行できます。
3-2. バッチで全アカウントのデータを移行する
3-1 の対応後にアクセスしていないユーザーは移行できないため、代わりにFirebase Authから全アカウントをエクスポートし、認証基盤に流し込みます。
Firebase CLI 公式でエクスポート機能があるのですが、数百万レコードになるようなワークロードではリトライ等の面で不都合があったため、独自にエクスポートツールをつくりました。
これにより、Firebase Authと認証基盤が完全に同期されました。バッチ実行後に作成されたアカウントは 3-1 の対応でリアルタイムに同期されていきます。
3-3. アカウント登録/ログインを認証基盤で行う
3-2 で全てのアカウントが認証基盤に同期されたため、アカウント登録/ログイン処理も認証基盤で行えるようになりました。もっとも、認証基盤はOIDCに準拠しているため、フローとしてはFirebase Authの頃とほぼ変わらず向き先が変わっただけで、特殊な引き継ぎ処理が掃除されました。
3-4. ログイン済みユーザーのセッションをリアルタイム移行
新たにアカウント登録/ログインするユーザーはUbieアカウントになりますが、既にFirebase Authでログイン済みのセッションを持つユーザーにはFirebase Authのセッションが残ってしまいます。
そこで、匿名アカウントと同様に、ユーザーがアクセスしたタイミングでUbieアカウントのセッションを発行します。
3-2 でFirebase Authと認証基盤が同期されているため、単にFbAccountIDからアカウントを検索するだけで済みます。これはOIDCには準拠しないセッション発行フローになってしまいますが、移行期間だけなので許容しました。
ここまでの対応で、全アカウントのデータ移行が完了し、あとはユーザーがアクセスさえしてくれればセッションも移行できる状態になりました。
現時点での挙動整理
- この対応後にアクセスした匿名ユーザー: 移行完了
- この対応後にアクセスした登録ユーザー: 移行完了
- この対応後にアクセスしていない匿名ユーザー: Firebase Auth のまま
- この対応後にアクセスしていない登録ユーザー: データは認証基盤に同期されているが、セッションはFirebase Authのまま
4. しばらく待ってからセッション移行処理を掃除
セッションの移行はユーザーのブラウザで握っているトークンを差し替える必要があるため、ユーザーがアクセスしたタイミングでしか行えません。そのため、移行期間をしばらく設ける必要があります。2 と 3-4 の対応で移行処理が入っているので、この移行期間はユーザーのアクセスを待つだけです。
移行期間が終わったら、匿名アカウント/登録アカウントともに引き継ぎ処理を消します。ここまでに移行できなかった匿名アカウントは消失、登録アカウントは再ログインが必要になります。
これはどうしても避けられないため、リテンションレートを参考に移行期間を数ヶ月単位に設定し、できるだけビジネス影響を抑えました。
これでFirebase Auth関連ロジックは完全になくなり、移行作業が完了しました。
現時点での挙動整理
- この対応後にアクセスした匿名ユーザー: 移行完了
- この対応後にアクセスした登録ユーザー: 移行完了
- この対応後にアクセスしていない匿名ユーザー: 消失
- この対応後にアクセスしていない登録ユーザー: データは認証基盤に同期されているが、再ログインが必要
おわりに
丁寧にステップを踏むことにより、ビジネス影響ほぼなしで認証基盤に移行することができ、コストは1/3以下になり、年間1000万円以上の削減となりました。
しかし、この移行は始まりにすぎません。内製認証認可基盤の上で、リスクベース認証やマイクロサービスのアクセス制御など、やりたいことが山ほどあります。
一緒にやってくれる方を大募集中です!
-
Firebase Authにかかっていたコストと、内製認証認可基盤にかかるインフラコストの差額 ↩︎
-
scryptをカスタムしたもの https://github.com/firebase/scrypt ↩︎
Discussion