Share
Embed
脱Firebase. 我々はどう生きるか/Migrate from Firebase
AWS DevDay 2022での登壇資料です。
Firebaseと言ってますが実際にはFirestoreだけです。
なお、Firebaseをdisるような内容ではありません。
Keisuke69
November 09, 2022
Transcript
50 slides
-
CTO at Singular Perturbations Inc Keisuke Nishitani @Keisuke69 Programming is
a creative work. 🎨 Love Music ♫ Love Camping ⛺ Blog: https://www.keisuke69.net/ 💻 Everything will be serverless. ⚡ -
弊社のアプリケーション構成 • 最初期はモバイルだけであり、エンジニアがいなかったこともあ り全⾯的にFirebaseに依存 • Cloud Firestore以外にもFirebase Authentication、Cloud Storage for
Firebaseなどを利⽤ • 昨年からプロダクト構成が整理され、モバイルの作り直しや新た にWebアプリを開発 • モバイルとWebは役割の異なるアプリケーションだがデータは同 じものを参照 • AWSは⼀部のAPIと機械学習パイプラインで利⽤ -
弊社のアプリケーション構成 その他 • Web • AWS Amplify Consoleで ホスティング •
AWS AppSyncも利⽤ • Routing API • AWS Fargate • 機械学習パイプライン • AWS StepFunctions、AWS Lambda 、AWS Fargateあたりを活⽤ Firebase Authentication Firebase Cloud Firestore Cloud Storage for Firebase Routing API -
弊社のアプリケーション構成 Firebase Authentication Firebase Cloud Firestore Cloud Storage for Firebase
Routing API REST API Firebase Authentication Cloud Storage for Firebase Routing API -
ユースケース・ニーズの変化 • Cloud Firestoreでは対応するのが難しいケースの発⽣ • 集約 • 当初はユースケースとして不要 • 運⽤上発⽣していた集計処理は⽉次でBigQuery
(BQ) にインポートして処 理 • ユーザ操作によってその時点での集計結果を得たいという要件が発⽣ • 地理情報による検索 • 特定の地理情報でデータを柔軟にクエリする必要が発⽣ • Geographical point型があるが公式にも推奨されておらず、Geohashを 使った実装が必要 -
GraphQL vs REST API • GraphQLの⼀般的な利点 • クライアントで取得したいデータを決められ、ちょっとした仕様変更なら API側の修正やリリースが不要 •
REST APIのように固定的なリクエスト/レスポンスに従うわけではなく、 必要なデータのみを含むレスポンスとなり効率がいい • 型がある • フロントエンドのためのゲートウェイとして振る舞ることも可能で、いわ ゆるフェデレーション的なことも可能 • 正直なところRESTと⽐べると利点しかない • でもREST APIを採⽤した -
GraphQLを選ばなかった理由 • エコシステムの成熟度 • ツール、ライブラリの選択肢があまりない • 開発チームが不慣れ • 有⼒なマネージド・サービスが少ない •
エンジニアが少ないのでマネージド・サービスは必須 • 安⼼して任せられる決定的なサービスが存在しない(個⼈の主観) • AWS AppSyncはVTLが⾟い • 特にデバッグ • 異論は認める -
スタック • アプリケーション • TypeScript • Nest.js • Prisma •
インフラ • AWS Fargate • Amazon Elastic Load Balancing ( Network Load Balancer ) • Amazon Aurora for PostgreSQL • PostgreSQLなのはPostGISを使うため • 開発環境はAurora Serverless • Amazon API Gateway • 認証のため -
移⾏にあたって検討したこと、決め事 • アプリケーションそのものの部分はそんなに難しい話ではない • 検討の多くはデータ格納先となるRDBに既存のデータをどう格納していくか • つまりテーブル設計 • FirestoreはNoSQLと呼ばれるタイプのDBであり、テーブルや⾏といったものがない。 •
データは『ドキュメント』として扱われ、ドキュメントをまとめたものとして『コレク ション』という概念がある • 『ドキュメント』はJSのオブジェクトのようなものでフィールドとその値で構成され、こ のフィールドは可変 • 具体的には以下のような点について検討が必要だった • ドキュメントIDをどう扱うか • サブコレクションをどう扱うか • 配列やマップといったフィールドのタイプをどう扱うか • Firebase AuthenticationとFirestoreのセキュリティルールで実現している認可をどうするか -
ドキュメントID • ドキュメントID: Firestoreで使われるID。デフォルトでは“06XWvXOqtUmLR2BnC7fZ” のような⽂字列 • RDBでID⽣成をDBのシーケンスなどの機能に任せたい場合は数値の型になる • 加えて、別の値を設定するとなるとコレクション間のリレーション全てを書き換える必要が出てくる •
プライマリーキーとなるID列を⽂字列型で⽤意し、既存データはドキュメントIDをそのまま移⾏、新規 データは新たにキーを⽣成する • UUIDなどが考えられるが、CUIDを採⽤ 参考): ⼀意な識別⼦の⽣成でUUID/ULID/CUID/Nano IDなど検討してみた https://www.keisuke69.net/entry/2022/08/01/140656 • 型はTEXT型を採⽤ • 選択肢としてはCHAR、VARCHAR、TEXT • ID列なので結果的に固定⻑ • 公式ドキュメントにも記載の通りPostgreSQLだとTEXTとVARCHARはパフォーマンス的には同等 • 他のDBと異なりCHARが⼀番遅いのでCHARを使う利点はない • 実際のところ⽂字数を制限したところで⾒積もりが楽になるくらいだと思われることと、Prismaも対応しているため TEXTを使う • 基本的に既存のドキュメントIDをそのままIDとして格納するので外部キー制約も問題ない -
サブコレクション • サブコレクション • 特定のドキュメントにぶら下がる形で定義されたコレクション • 配列やMapでネストする場合と異なりネストするデータのサイズが増えて も親のドキュメントのサイズが変わらない • ⼩さいデータをネストする場合は問題ないがそれなりに⼤きいものを格納
すると親ドキュメントのクエリで取得するデータサイズが⼤きくなる • FirestoreのドキュメントをRDBのレコード、コレクションをテーブ ルだと⾒⽴てるとリレーションのある別テーブルと構造としては 同じ • 別テーブルとして切り出し、コレクションの移⾏先を親テーブル とした⼦テーブルとする -
配列 • サブコレクションと異なりデータのサイズ・要素数は多くないものの⼀ つのドキュメントに複数存在しているという状況が多かった • この状況でサブコレクションと同様の対応をすると⼩さすぎるテーブル が⼤量になる • 親⼦関係を持つテーブルが⼤量になるため、⼤量のJOINも発⽣ •
配列に関してはPostgreSQLは配列型があるのでこれを利⽤ • 標準SQLとしても定められている • Prismaでも普通にサポートしている • ただし、使いすぎると『正規化とは…?』という状況になるため注意 • Indexも張れる • GINインデックスで配列の各要素に張れる -
Map • MapについてはPrismaもサポートしているJSON/JSONB型を検討した が、展開して列として定義 • 理由 • 正規化が崩れる(これは配列型も同じ) • クエリにRDBごとの⽅⾔が強めで、SQLの可読性が低い
• Prismaが吸収してくれる部分もあるが、$queryRawを使うケースも多いため気になる • スキーマレスになり何が格納されているか分かりづらく、型も指定できな い • 既存データではMapのキーの個数が少なく可変なものもなかった • JSON/JSONB型ではなく列として展開して格納することで、型も指 定できる -
Mapの配列 • 配列の要素としてMap型のデータを持っているケース • 『⾏持ちテーブル』などと呼ばれる形式に変換 [ { "name": "Scott", "age":
30 }, { "name": "John", "age": 35 }, { "name": "Bill", "age": 25 } ] 列名 データ型 備考 id text 親テーブルの主キー index integer いわゆる配列の要素番号に相当 name text Mapに含まれるキー age integer Mapに含まれるキー id index name age AAA 0 Scott 30 AAA 1 John 35 AAA 2 Bill 25 -
(余談) PostGISとGEOMETRY型 • 今回の事例ではMapの配列が使われて いたのは多くが位置情報 • このケースはPostGISのGEOMETRY型の列 を⽤意するだけで解決する • 例えば右のようにLineStringのデータを格
納していた場合、 GEOMETRY(‘LineString’,4326)の列を⽤意 するだけでいい [ { "latitude": -73.993433, "longitude": 40.736274 }, { "latitude": -73.993632, "longitude": 40.736007 }, { "latitude": -73.984937, "longitude": 40.732353 }, { "latitude": -73.986374, "longitude": 40.730382 }, … ] -
認証・認可 • 認証にはFirebase Authenticationを利⽤しており、認可はFirestoreのセ キュリティルールで実装していたので移⾏に伴い⾃前で実装する必要性 • Firebase Authenticationでサインインすると取得できるID Tokenをサーバー サイドに送り、サーバーサイドでそのTokenを検証、問題なければログ
イン済ユーザ情報を元に権限チェック • 任意のJWTライブラリを⽤いて検証することが可能 • 送られてきたID Tokenをデコードするとペイロードにsubおよびuser_idというキー とその値があるのでそれを⽤いてRDBに保存したユーザ情報をクエリし権限情報 を取得 • 今回は認証は引き続きFirebase Authentication、トークン検証は API Gateaway + Lambda Authorizer、認可はNest.jsのGuardとCASLを使って 実装 -
移⾏過渡期のデータについて • すでに稼働しているシステムのため既存システムからのデータ移 ⾏が発⽣する • 既存データをどうサービスの中断を限りなく少なく移⾏していくか • ベタに移⾏スクリプトを⽤意して複数回にわけて実施 • upsertで実装することで何も考えずに何度も実⾏可能に
• 実際のデータ投⼊はAPIのテストも兼ねて全部API経由で実施 • 実際には集計関連の⼀部機能のみ先⾏してリリースしたため、 Cloud Functionsを⽤いて逐次RDBに反映した -
新システムのアーキテクチャ • シンプルなWebシステム構成をコンテナで実装 • 実⾏基盤はAWS Fargate • 別で存在していたAPIとほぼ同⼀の構成とした • Cloud
Storage for FirebaseからAmazon S3への移⾏は強いモチベーションがないため ⾒送った • 極⼒マネージド・サービスを利⽤ • サーバーレスを採⽤しなかった理由 • アプリの開発⽣産性の観点 • 既存のWebアプリケーションフレームワークとそのエコシステムを『そのまま』利⽤し たい • バックエンドがRDBである • AWS上で構築した理由 • 別のシステムがすでにAWSで稼働していた • 開発メンバーに他クラウドを知るものがおらず学習コストをかけたくなかった • 今回のスタックでは他クラウドを使いたいという強いモチベーションもなかった -
増える運⽤ • システムのモニタリング • APIの死活監視 • DBの監視(死活、容量 etc) • DBのバックアップおよびリカバリ戦略
• これまではFirestoreのエクスポートとFunctionsで簡単に実現していた • システムの運⽤ • DBマイグレーション • アプリケーション⾃体はCI/CDで⾃動デプロイされるがPrismaのマイグレーションは ⼿動で対応中 -
実際の移⾏ステップ 1. 設計⽅針に従って⼀通り論理設計 2. 各テーブルのCRUDをベタに実装 3. フロントエンド(Webとモバイル)のDBアクセス部分をAPIに切り替 えつつ、⾜りないAPIの洗い出し 4. ⾜りないAPIの追加実装(テーブル設計の⾒直し含む)
5. 認証認可の実装 6. モバイルの申請 (事前に審査には出しておき当⽇は公開のみに) 7. APIリリース、DBマイグレーション、データ移⾏1回⽬ 8. Webのリリース 9. モバイルの公開と強制アップデート(強制アップデートは独⾃の仕組 み。この時点でFirestoreを⾒るユーザがいなくなる) 10. データ移⾏2回⽬