Slide 1

Slide 1 text

Singular Perturbations Inc Keisuke Nishitani ୤ 'JSFCBTF զʑ͸Ͳ͏ੜ͖Δ͔

Slide 2

Slide 2 text

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. ⚡

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

世界の悲しい経験を減らす。 VISION

Slide 5

Slide 5 text

コンピューターサイエンスがもたらす知能を 安全に関わる全ての⼈へ MISSION

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

前置き Photo by Kristina Flour on Unsplash

Slide 8

Slide 8 text

弊社のアプリケーション構成 • 最初期はモバイルだけであり、エンジニアがいなかったこともあ り全⾯的にFirebaseに依存 • Cloud Firestore以外にもFirebase Authentication、Cloud Storage for Firebaseなどを利⽤ • 昨年からプロダクト構成が整理され、モバイルの作り直しや新た にWebアプリを開発 • モバイルとWebは役割の異なるアプリケーションだがデータは同 じものを参照 • AWSは⼀部のAPIと機械学習パイプラインで利⽤

Slide 9

Slide 9 text

弊社のアプリケーション構成 Firebase Authentication Firebase Cloud Firestore Cloud Storage for Firebase Routing API

Slide 10

Slide 10 text

弊社のアプリケーション構成 その他 • 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

Slide 11

Slide 11 text

弊社のアプリケーション構成 Firebase Authentication Firebase Cloud Firestore Cloud Storage for Firebase Routing API REST API Firebase Authentication Cloud Storage for Firebase Routing API

Slide 12

Slide 12 text

今⽇のテーマ

Slide 13

Slide 13 text

FirebaseのCloud Firestoreをやめた話 ※Firebaseをdisる話ではありません

Slide 14

Slide 14 text

そもそも、なぜやめるのか

Slide 15

Slide 15 text

なぜやめるのか • ユースケース・ニーズの変化 • 将来的なビジネスロードマップ上の布⽯ • 開発効率

Slide 16

Slide 16 text

なぜやめるのか • ユースケース・ニーズの変化 • 将来的なビジネスロードマップ上の布⽯ • 開発効率

Slide 17

Slide 17 text

なぜやめるのか • ユースケース・ニーズの変化 • 将来的なビジネスロードマップ上の布⽯ • 開発効率

Slide 18

Slide 18 text

なぜやめるのか • ユースケース・ニーズの変化 • 将来的なビジネスロードマップ上の布⽯ • 開発効率

Slide 19

Slide 19 text

ユースケース・ニーズの変化 • Cloud Firestoreでは対応するのが難しいケースの発⽣ • 集約 • 当初はユースケースとして不要 • 運⽤上発⽣していた集計処理は⽉次でBigQuery (BQ) にインポートして処 理 • ユーザ操作によってその時点での集計結果を得たいという要件が発⽣ • 地理情報による検索 • 特定の地理情報でデータを柔軟にクエリする必要が発⽣ • Geographical point型があるが公式にも推奨されておらず、Geohashを 使った実装が必要

Slide 20

Slide 20 text

そうだ、Firestoreやめよう

Slide 21

Slide 21 text

では、どうするか

Slide 22

Slide 22 text

GraphQL vs REST API

Slide 23

Slide 23 text

GraphQL vs REST API • GraphQLの⼀般的な利点 • クライアントで取得したいデータを決められ、ちょっとした仕様変更なら API側の修正やリリースが不要 • REST APIのように固定的なリクエスト/レスポンスに従うわけではなく、 必要なデータのみを含むレスポンスとなり効率がいい • 型がある • フロントエンドのためのゲートウェイとして振る舞ることも可能で、いわ ゆるフェデレーション的なことも可能 • 正直なところRESTと⽐べると利点しかない • でもREST APIを採⽤した

Slide 24

Slide 24 text

なぜREST APIにしたのか

Slide 25

Slide 25 text

GraphQLを選ばなかった理由 • エコシステムの成熟度 • ツール、ライブラリの選択肢があまりない • 開発チームが不慣れ • 有⼒なマネージド・サービスが少ない • エンジニアが少ないのでマネージド・サービスは必須 • 安⼼して任せられる決定的なサービスが存在しない(個⼈の主観) • AWS AppSyncはVTLが⾟い • 特にデバッグ • 異論は認める

Slide 26

Slide 26 text

というわけでCloud Firestoreをやめて、⾃前のREST APIに移⾏する

Slide 27

Slide 27 text

スタック • アプリケーション • TypeScript • Nest.js • Prisma • インフラ • AWS Fargate • Amazon Elastic Load Balancing ( Network Load Balancer ) • Amazon Aurora for PostgreSQL • PostgreSQLなのはPostGISを使うため • 開発環境はAurora Serverless • Amazon API Gateway • 認証のため

Slide 28

Slide 28 text

(余談) PrismaがPostGISに未対応 • PrismaはPostGISのGEOMETRY型に対応していない • PostGISが持つ地理情報を扱うための関数群をPrismaそのものでは 扱えない • $queryRawならびに$executeRawでSQLを発⾏するしかないので ORMを使う嬉しさは半減する • このあたりは本題からはずれるので別の機会に

Slide 29

Slide 29 text

移⾏の仕⽅を考える (アプリケーション編)

Slide 30

Slide 30 text

移⾏にあたって検討したこと、決め事 • アプリケーションそのものの部分はそんなに難しい話ではない • 検討の多くはデータ格納先となるRDBに既存のデータをどう格納していくか • つまりテーブル設計 • FirestoreはNoSQLと呼ばれるタイプのDBであり、テーブルや⾏といったものがない。 • データは『ドキュメント』として扱われ、ドキュメントをまとめたものとして『コレク ション』という概念がある • 『ドキュメント』はJSのオブジェクトのようなものでフィールドとその値で構成され、こ のフィールドは可変 • 具体的には以下のような点について検討が必要だった • ドキュメントIDをどう扱うか • サブコレクションをどう扱うか • 配列やマップといったフィールドのタイプをどう扱うか • Firebase AuthenticationとFirestoreのセキュリティルールで実現している認可をどうするか

Slide 31

Slide 31 text

ドキュメント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として格納するので外部キー制約も問題ない

Slide 32

Slide 32 text

サブコレクション • サブコレクション • 特定のドキュメントにぶら下がる形で定義されたコレクション • 配列やMapでネストする場合と異なりネストするデータのサイズが増えて も親のドキュメントのサイズが変わらない • ⼩さいデータをネストする場合は問題ないがそれなりに⼤きいものを格納 すると親ドキュメントのクエリで取得するデータサイズが⼤きくなる • FirestoreのドキュメントをRDBのレコード、コレクションをテーブ ルだと⾒⽴てるとリレーションのある別テーブルと構造としては 同じ • 別テーブルとして切り出し、コレクションの移⾏先を親テーブル とした⼦テーブルとする

Slide 33

Slide 33 text

配列 • サブコレクションと異なりデータのサイズ・要素数は多くないものの⼀ つのドキュメントに複数存在しているという状況が多かった • この状況でサブコレクションと同様の対応をすると⼩さすぎるテーブル が⼤量になる • 親⼦関係を持つテーブルが⼤量になるため、⼤量のJOINも発⽣ • 配列に関してはPostgreSQLは配列型があるのでこれを利⽤ • 標準SQLとしても定められている • Prismaでも普通にサポートしている • ただし、使いすぎると『正規化とは…?』という状況になるため注意 • Indexも張れる • GINインデックスで配列の各要素に張れる

Slide 34

Slide 34 text

Map • MapについてはPrismaもサポートしているJSON/JSONB型を検討した が、展開して列として定義 • 理由 • 正規化が崩れる(これは配列型も同じ) • クエリにRDBごとの⽅⾔が強めで、SQLの可読性が低い • Prismaが吸収してくれる部分もあるが、$queryRawを使うケースも多いため気になる • スキーマレスになり何が格納されているか分かりづらく、型も指定できな い • 既存データではMapのキーの個数が少なく可変なものもなかった • JSON/JSONB型ではなく列として展開して格納することで、型も指 定できる

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

(余談) 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 }, … ]

Slide 37

Slide 37 text

認証・認可 • 認証には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を使って 実装

Slide 38

Slide 38 text

CASLによる認可についてはこちらに書いてます https://www.keisuke69.net/entry/2022/10/25/100315

Slide 39

Slide 39 text

ここまでの話を踏まえて • 実際のテーブル設計は半ば機械的に実施 • テーブルの列はドキュメントのフィールドに対応させる • 列名については既存のものをそのまま使うが慣例に従ってキャメルケース からスネークケースへと置き換え • テーブルができたらあとは必死に各テーブルへのCRUD APIを作成

Slide 40

Slide 40 text

移⾏の仕⽅を考える (データ編)

Slide 41

Slide 41 text

移⾏過渡期のデータについて • すでに稼働しているシステムのため既存システムからのデータ移 ⾏が発⽣する • 既存データをどうサービスの中断を限りなく少なく移⾏していくか • ベタに移⾏スクリプトを⽤意して複数回にわけて実施 • upsertで実装することで何も考えずに何度も実⾏可能に • 実際のデータ投⼊はAPIのテストも兼ねて全部API経由で実施 • 実際には集計関連の⼀部機能のみ先⾏してリリースしたため、 Cloud Functionsを⽤いて逐次RDBに反映した

Slide 42

Slide 42 text

移⾏の仕⽅を考える (インフラ編)

Slide 43

Slide 43 text

新システムのアーキテクチャ • シンプルなWebシステム構成をコンテナで実装 • 実⾏基盤はAWS Fargate • 別で存在していたAPIとほぼ同⼀の構成とした • Cloud Storage for FirebaseからAmazon S3への移⾏は強いモチベーションがないため ⾒送った • 極⼒マネージド・サービスを利⽤ • サーバーレスを採⽤しなかった理由 • アプリの開発⽣産性の観点 • 既存のWebアプリケーションフレームワークとそのエコシステムを『そのまま』利⽤し たい • バックエンドがRDBである • AWS上で構築した理由 • 別のシステムがすでにAWSで稼働していた • 開発メンバーに他クラウドを知るものがおらず学習コストをかけたくなかった • 今回のスタックでは他クラウドを使いたいという強いモチベーションもなかった

Slide 44

Slide 44 text

増える運⽤ • システムのモニタリング • APIの死活監視 • DBの監視(死活、容量 etc) • DBのバックアップおよびリカバリ戦略 • これまではFirestoreのエクスポートとFunctionsで簡単に実現していた • システムの運⽤ • DBマイグレーション • アプリケーション⾃体はCI/CDで⾃動デプロイされるがPrismaのマイグレーションは ⼿動で対応中

Slide 45

Slide 45 text

実際の移⾏ステップ 1. 設計⽅針に従って⼀通り論理設計 2. 各テーブルのCRUDをベタに実装 3. フロントエンド(Webとモバイル)のDBアクセス部分をAPIに切り替 えつつ、⾜りないAPIの洗い出し 4. ⾜りないAPIの追加実装(テーブル設計の⾒直し含む) 5. 認証認可の実装 6. モバイルの申請 (事前に審査には出しておき当⽇は公開のみに) 7. APIリリース、DBマイグレーション、データ移⾏1回⽬ 8. Webのリリース 9. モバイルの公開と強制アップデート(強制アップデートは独⾃の仕組 み。この時点でFirestoreを⾒るユーザがいなくなる) 10. データ移⾏2回⽬

Slide 46

Slide 46 text

というわけで10⽉31⽇に無事に完了しました

Slide 47

Slide 47 text

というわけで10⽉31⽇に無事に完了しました ※実際にはその後ちょっとした不具合修正をしたので11⽉2⽇

Slide 48

Slide 48 text

SQL最⾼

Slide 49

Slide 49 text

Photo by Daniel Andrade on Unsplash

Slide 50

Slide 50 text

気になることとかあったらいつでも @Keisuke69までDMください Photo by Daniel Andrade on Unsplash