マッチングアプリ開発でFlutter x Go x AWSの組み合わせは失敗だった
Flutter × Go × AWSで友人とマッチングアプリを開発したけれど、「次はこの構成ではやらないな」と感じたポイントがありました。
今回は、失敗したポイントと、もし1から作り直すならどんな技術選定にするかを共有します。
ちなみに開発したマッチングアプリはApple審査の却下により日の目を浴びることはありませんでした。アプリ審査で地獄を見た記事はこちら
作ったアプリのデモ動画
インフラアーキテクチャ
使用技術と選定理由
アプリ Flutter
まず人手が少ないのでマルチプラットフォームを同時開発できる要件を満たしていて欲しかった。アプリ開発経験者がいなかったため、事例が豊富そうなものが良いと思っていたが、日本国内の個人アプリ開発者はFlutterの割合が高そうだったから。
結果、Flutterは良かったと思う。1から開発するとしても選定する。
バックエンド Go
友人も私もGoをメイン言語としていたため。
DB操作 sqlc
DB操作にはsqlcでGoコードを生成していた。
項目 | 内容 |
---|---|
ツール名 | sqlc |
用途 | SQLクエリから型安全なGoコードを自動生成する |
対象言語 | Go(他にもRustやPythonなどのサポートが進行中) |
入力 | SQLファイル(.sql )とDBスキーマ情報(.sql または.yaml など) |
出力 | Goのコード(DBアクセス関数や構造体) |
バックエンドは私がメインで開発していたがアプリ開発前にLeetCodeでSQL問題を勉強がてら解いていたらSQLを書くのが楽しくなり、SQLを書いてそこからGoコードを生成できるsqlcにした。私としては大方使い心地良かった。
sqlcの気になった点
1対多のレコードをJOINして取得する時に生成される構造体が扱いにくいところがあった。
具体例で言うと、ユーザが会える都道府県を複数設定できるような1対多のテーブル設計を想定した場合、
-- usersテーブル(1)
CREATE TABLE users (
id UUID NOT NULL PRIMARY KEY,
nickname VARCHAR(255) NOT NULL
);
-- meeting_prefecturesテーブル(多)
CREATE TABLE meeting_prefectures (
user_id UUID NOT NULL,
prefecture INT NOT NULL,
PRIMARY KEY (user_id, prefecture),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
ユーザIDを指定してユーザ情報と会える都道府県を取得したい場合以下のようなクエリを書くことになるが、
-- name: GetProfile :many
SELECT
users.id,
users.nickname,
meeting_prefectures.prefecture AS meeting_prefecture
FROM users
LEFT JOIN meeting_prefectures ON users.id = meeting_prefectures.user_id
WHERE
users.id = $1;
理想的には、配列として構造体に格納されていて欲しいが、実際に生成される構造体はSQLで帰ってくる行をそのまま構造体にした感じなので、クライアントに返す時はここら辺を適切な構造体に詰め直す必要がある。
# 理想
type GetProfileRow struct {
ID pgtype.UUID
Nickname string
MeetingPrefecture *[]int32
}
# 実際に生成される構造体
type GetProfileRow struct {
ID pgtype.UUID
Nickname string
MeetingPrefecture *int32
}
このようにJOINしても、sqlcは1対多の結果をマージしない
DBマイグレーションツール dbmate
dbmateを使用した。CLIとしての利用でmigrationファイルがある場所を指定してup/downコマンドを使用してマイグレーションが行える。開発時には特に不満なかった。
API Connect
メッセージ機能をサーバーサイドストリーミングで実装したかったため、gRPCを採用した。その後、ブラウザから利用できる管理画面を作成する際にgrpc-webなどのプロキシサーバが無くても通信できるgRPC互換のConnectに切り替えた。gRPCの自動生成の開発体験はとても良かった。
インフラ AWS
友人も私もAWSに知見があったため選定した。インフラはTerraformで管理していた。
サービス名 | 用途 | 選定理由 |
---|---|---|
Fargate | Go製APIサーバのホスティング(Dockerコンテナを実行) | gRPCストリーミング対応 |
ALB (Load Balancer) | APIリクエストのルーティング | Fargateとの連携が容易でHTTPS対応も可能。gRPC対応 |
S3 Bucket | アプリ画像保存用 | S3一択 |
DynamoDB | メッセージ情報の保存 | メッセージの容量スケールに安価に対応できると考えたから。The DynamoDB Bookを読んでDynamoDBのポテンシャルに惹かれたから |
Aurora PostgreSQL | マッチ情報/ユーザ情報などん保存 | DynamoDBだけで完結されられるか考えたが検索がRDBでないと厳しかった。料金が高かったため、途中から開発時はNeonを使っていた。 |
ECR (Elastic Container Registry) | Fargateで実行するDockerイメージの保管 | AWS内で完結する安全なDockerレジストリ。Fargateとの統合が容易。 |
CloudWatch Logs | Fargateからのログ収集・アラート設定 | CloudWatch一択 |
Amazon Pinpoint | プッシュ通知の配信(APNs/FCM経由) | pinpoint一択 |
Cognito | Flutterアプリのユーザー認証・認可 | Cognito一択 |
Amplify | FlutterアプリからCognito, Pinpointに連携する用 | 使ったほうが認証・プッシュ通知周りの開発を省けたから |
Flutter開発でAmplifyはもう使いたくない
AWS使うからFlutterアプリのユーザ管理やプッシュ通知部分はAmplifyを用いることにしたがこれが振り返ると失敗だった。Amplifyはまだ発展途上という感じでドキュメントが充実していなかったし、Amplifyならではの問題にいくつか遭遇した。また、既知の改善点がなかなか改善されることはなくコミュニティが活発でないように思えた。
Amplifyの微妙だった点
- Amplifyチームが提供しているFlutterのAuthenticator Widget(ユーザ登録・ログインに使えるWidget)が微妙
- ログインエラーメッセージを完全に日本語化できない。
- Authenticator Widgetのユーザ登録画面にはOAuthで登録するボタンを表示できない
- プッシュ通知周りの制御が不完全でプッシュ通知を開いた時に特定の画面を開くといったことがうまくできなかった。原因がいまだにしっかり掴めていない。
- AWS Pinpointの通知デバイス先にMaxで15個までしか登録できず新規登録した時に古いのを自動で削除などができずにエラーになる。開発時にはシミュレーターが違うと通知デバイス先が増えていくのでAWS CLIで登録先を消す作業が定期的に起きて面倒だった。
AWSの不満だった点
Amplify x Flutterの組み合わせは次で使うことはないと思うが、AWSについてもコスト面で不満が多かった。
- ALBを停止できないため、費用を抑えるためには削除する必要がある
2. そして削除をして再度立ち上げるとホスト名が変わってしまうため、固定にするために独自ドメインの取得とRoute53の設定が必要 → Route53のレコード保持に月0.5ドルかかる - Fargateは使っていない時に自動でインスタンス数を0にスケールインできないため、コストを発生させないためにはテストの度にインスタンス数をいじる必要がある
- AuroraDBが高額で手軽に立ち上げてテストできない
1から作り直すならFlutter x Go x Firebase x GCP
長々と書いてきたが1から作り直すならFlutter x Go x Firebase x GCPを採用する。つまり、FlutterとGoは据え置きでAmplifyとAWSを使っていたところをFirbaseとGCPに置き換える。実際にFirebaseとGCPへの載せ替えも行ってみたが、コストメリットがあり、開発体験も上がった。実際にリリースまで行けなかったこともあり、運用した場合にはまた新たな課題があるかもしれないが、少なくとも開発時点では圧倒的にこの構成が良かった。具体的に良かった点をまとめてみる。
新アーキテクチャ図
Firebase x GCPを用いたアーキテクチャ図
Amplify → Firebaseのメリット
Firebaseにしたら、Amplifyで起こっていた問題が全て解消された。ドキュメントの充実度も圧倒的にFirebaseだし、事例も豊富だった。ログイン・サインアップ画面の両方にOAuthの登録・ログインボタンを付与するのも非常に簡単にできたし、プッシュ通知を受信した時やタップした時の制御も簡単に行えた。Amplifyでどれだけ時間を無駄にしたことか...
AWS ALB → GCP API Gatewayのメリット
AWSのAPI GatewayはgRPC対応していなかったためgRPC対応しているALBを採用した。しかし、ALBは停止できず、費用を発生させないためには削除するしかなかった。結果、テストの度にALBを新規作成する必要があった。新規作成する度にアプリの接続ホスト名が変化するため、固定させるために独自ドメインを用意してCNAMEでALBに向けることで、アプリの接続先を固定する必要があった。
一方GCP API Gatewayではこれらのデメリットが発生しなかった
- gRPC対応
- API Gateway自体がサーバレスで、使っていないときに課金が発生しない
- API Gatewayを都度再作成しなくて良いためGCPが自動で割り当てるホスト名をアプリのリクエスト先としておけばOK(独自ドメインの取得の必要なし)
AWS Fargate → GCP Cloud Runのメリット
Fargateでは自動でインスタンス数を0にできなかったが、Cloud Runでは使われていない時間が一定時間でインスタンス数を0にスケールインさせて費用を発生させないようにすることができた。それにより、いちいちインスタンス数をいじる手間が省かれた。
AWS DynamoDB → Firebase Firestoreのメリット
Firebase FirestoreにはFirebaseのSDKを使ってFlutterアプリから直接書き込み・読み込みができる。また、リアルタイムに自分がlistenしている変更を検知・反映できるため、Goのアプリケーションサーバを介したメッセージ機能のストリーミング通信の必要がなくなった。
Firestoreのread/write権限を厳密に定義しなければいけない手間はあるが、ストリーミング通信を適切に実装・テストする手間に比べたら圧倒的に楽。
また、これまではストリーミングのためにはユーザの接続情報をサーバに持たせたステートフルサーバーになっていた。そのため、今の構成のままだと、サーバーのスケールアウトができないような設計になっていた。だが、ストリーミングがなくなったことによりステートレスサーバーにすることができ、負荷に合わせた柔軟なスケールアウトが可能になった。
※ ストリーミングがなくなったので、Cloud Runではなく、Cloud Functionsを使ってもいいかもしれない。AWS LambdaはgRPC非対応だが、Cloud FunctionsはgRPC対応してる。
まとめ
- Flutterでアプリ開発するならAmplifyよりFirebaseを使おう
- Firebase使うならAWSよりGCPを使おう
- gRPC使うならGCPの方が対応が進んでいる
Discussion