ブラウザで動くサービスを作るときの技術選定
はじめに
私の仕事は、新規サービスをまるっといい感じに開発するのを委託されることがほとんどです。最近はネイティブアプリを作ることよりもブラウザで動くWebサービスを開発することが多いのですが、案件の規模感や要求によって技術選定を少し変えるようにしています。「こういうときはこう」みたいに一概には言えないのですが、普段使う構成を紹介します。誰かの参考になれば幸いです。
2022/02/10 現在での内容です。
前提
開発を委託される場合の運用費をどうするの問題があります。クライアントにクレカ登録をしてもらうか、こちらで支払って毎月請求するかになります。僕は毎月やるのがめんどくさいのでできるだけ前者に倒している関係上、あまりいろいろなSaaSを組み合わせて作ることをなるべく避けています。
規模感によらず使っているもの
私の場合、以下が使えるとめちゃくちゃ効率よく開発できます。
- GCP
- 好きだから使ってます。AWSじゃないとダメっていうときがたまにあるんですがそれ以外ではもうGCPになるべく寄せます。AWSよりドキュメントが読みやすくていいです。
- Stripe
- 決済入れるときはこれを使わせてもらえないと工数が増えます。使えないときは請求金額が大幅に増えます。
- SendGrid
- GCPにはメール送信を楽にできるサービスがないので仕方なく導入します。クレカ登録してもらうのがちょっとめんどう。
- 他にいいメール送信サービスがあれば知りたいです。
- TypeScript
- Webフロントエンドを作る以上、現状だと言語はTypeScriptになるのでバックエンドもTypeScriptに統一すると効率が良いです。TypeScriptが好きなのもありますが。
- Next.js
- 概ねSSRするんですがSGなども選択できるのとルーティングが楽なので現状はこれが最適かなと思っています。
- Terraform
- メジャーなクラウドプラットフォーム内に閉じているとインフラ構築はTerraformで管理できてしまいます。コマンド一発で構築できるので、いろいろな案件をやっている私にはありがたいツールです。
- GitHub Actions
- CI/CDに使います。基本的にmonorepoで組むので特定のディレクトリ以下が変更されたときはこのワークフローを動かす、みたいなのがかんたんに設定できるので重宝しています
パターン1
- Writeの複雑な権限管理がない
- 複雑なReadがあんまりない
- サービス運用費をできるだけ抑えたい
こんな感じであんまり複雑にならなさそうだな〜というときは以下の構成にします。ブラウザからFirestoreを直接触りつつ、必要に応じてtriggered Functionsを活用したり、Cloud RunでSSRします、必要に応じてStripeやSendGridを導入します。
細かい部分を文章にするのはかなり大変なので箇条書きで以下にまとめました。気になる人は御覧ください。
- FirestoreはRuleが複雑にならない場合は私の中で真っ先に上がる選択肢になります
- Firestoreからページネーション込みでデータ取得できるHooksとか作っておけば別案件でも使い回せてめっちゃ効率いいです
- ブラウザから直接データを読み書きできるのでAPIの開発がいらず高速に開発できます
- 初期コストが安い、大抵の場合お金がかかりません
- 初回リクエストを除けば高速にデータを取得でき、リアルタイムに変更を知れるので強力です
- Writeの要件が複雑だとRuleでの表現がしんどくなるのでFunctionsやNext.jsの /api を活用して逃げます
- Next.jsとコードが分離して開発効率が悪くなるので、個人的にはFunctionsはあまり使いたくないです
- Cloud RunでSSRしてるならNext.jsの /api を使ってあげるほうがコールドスタートもないしコードもフロントと共有できるので作るのが楽です
- しかしFirestoreなどのTriggerは現状だとFunctionsでしか受け取れないという制約が苦しいです
- いつ何時OGPを動的にしたいみたいなことになるのかわからないのでとりあえずSSRしとくかみたいなときがわりとあります
- ネイティブアプリではなくブラウザサービスにしたい場合は、SNSでのシェア目的でブラウザサービスにしましょうとなることが多いです
- SSRするとCDN(FirebaseHosting)のキャッシュが活用でき、ページの初期表示速度を上げることもできるのはメリットです
- 現状FirebaseFunctionsでSSRするのはパフォーマンス的な観点でイマイチだと思っているので使いません
- CloudRunにもコールドスタートがありますがmin instanceを設定でき、その料金もスペック次第だけど最小1000円以下になるので気になりません
- Functionsでもmin instanceは設定できますが1リクエスト1Functionでの処理になるのでコールドスタートの頻度が高いのではと想像しています(使ったことがないです)
- メディアみたいなサービスだとNext.jsでSGすればよいのでCloud Runは不要になります
- バッチ処理が必要になってくるとFunctionsの制限時間9分以内に終わりそうであればSchedulerからFunctionsを呼び出し処理します
- 終わらなさそうであれば CloudTasks を使います
- 処理対象を細かいTaskに分割してCloudTasksにわたすFunctionと、Taskを処理するFunctionに分けると、余程データ量が多くない限りは処理できます
- CloudTasksマジで便利で大好きなサービスです
Next.js x Hosting x Cloud Runのテンプレートも一応あるので見てみてください(最近あんまりメンテできてないですごめんなさい)。
最近、僕はあまりこのパターンの構成にしませんが、フロントエンド出身の人がサービスを作るときにはこの構成が開発しやすいのではないでしょうか(僕はiOSエンジニアからキャリアを始めたようなものだったのでこの構成が最初はとてもよかったです)。
パターン2
パターン1のようにブラウザからFirestoreのデータを読み取る場合、クライアントサイドジョインが発生してUIの制御が大変になることがあります(読み取るデータをFunctionsを活用してうまく用意すれば問題にはなりにくいですが、用意する処理を作るのが開発工数的に大変になりがちです)。またFunctionsとNext.jsでコードが分かれてイヤになってくるとすべてをNext.jsに寄せてしまいたくなります。
- Writeがちょっと複雑、Readも複雑になるかもしれない
- (FirestoreのRuleを書きたくない)
- コードを一箇所にまとめて楽したい
- 運用費は抑えめだと嬉しい
こういう感じのときは以下の構成にします。図にするのが難しいのですが、Next.jsの /api/graphql
にGraphQLのApolloServerのエンドポイントを用意して、ブラウザから叩いてもらうようになっています。
- GraphQLはもちろん素晴らしいんですが、何よりもApolloとGraphQL Code Generatorが最高すぎます
- schemaをGraphQLのDSLで書くと、それをもとにバックエンドの型を自動生成でき、さらにフロントエンドでの型はもちろん、query/mutationを呼ぶhooksまで自動生成してくれます
- これを使い始めると通信コードを自分で書かなくなり、しかも型に守られた効率的な開発ができ、本当に手放せないツールになっています
- Apollo Clientのキャッシュの仕組みを理解すると、サクサク動くブラウザサービスをかんたんに作れてしまうのが恐ろしいです
- DataStoreはnative/datastore modeの正直どちらでもいいかなと思っています
- native modeはFirebase Firestoreのことで、クライアントからの読み書きをRuleで制御します
- datastore modeはバックエンドのアプリケーションコードで読み書きの制御を行います
- その他の違いはドキュメントを読んでもらうとして、native modeはFirebase SDKでコードを書けるので結構書きやすいです。datastore modeはgRPCからコード生成しましたみたいなSDKを使う必要があり少し開発しにくいです。
- しかしdatastore modeはnative modeでの書き込みレート制限などが結構緩和されているので、writeヘビーなサービスでは大活躍すると思います
- Functionsを使わないようにすると、非常に便利なFirestoreのTriggerが使えなくなります
- DataStoreへのデータ書き込みの際に同時にCloudTasksやCloud Pub/SubへタスクをPushして、HTTP経由でNext.jsの /api/tasks などにTaskを受け取って処理するようにします
- バッチ処理はパターン1と同様にSchedulerを活用して、Next.jsの /api/scheduler などを叩いて処理します
- Cloud Runは1リクエストに対して現状最大1時間処理ができますが、Schedulerが30分が最大なので30分以内に処理を終わらせればよいです
- 30分以内じゃ終わらないようなものであれば、これまたパターン1と同じようにTaskを作るエンドポイントとTaskを処理するエンドポイントを用意すればなんとかなります
ちょっとバージョンが古い可能性もありますが Next.js x Apollo のテンプレートも用意しているのでよければどうぞ。
ある程度の規模まではこれで十分戦える気がしています。
パターン3
- コードベースが巨大になることがわかっている
- バックエンド専業・フロント専業みたいなチーム構成になりそう
- バックエンドフロントエンドそれぞれでCloud Runのリソースを最適化したい
- CDNの設定をもう少し細かくしたい
- 外部からの攻撃(DDoSとか)に対処したい
みたいなときは以下の構成にします。パターン2はApollo Serverを同居させていましたが、サーバーごとわけてNext.jsはレンダリングに専念してもらいます。FirebaseHostingを剥がして、よりGCPに寄せた構成にします。
- 基本的にパターン2で書いている内容と同じことが言えます
- Cloud SQLはなんだかんだ運用時に定期的にメンテナンスが入るのが嫌なのでスキーマちゃんとしたいときはSpannerにします
- しかしSpannerはTypeScript周りのエコシステムがほぼない状態でちょっと大変です
- 私は自分で定義したテーブルの型を渡せば補完が効くSpannerのWrapper classみたいなのを使って対応しています(結構よく作れている気がします)
- もし要望があればすこし整えて公開します
- Identity Aware Proxyを使って、開発環境には特定の人しかアクセスさせたくないみたいなことがわりと簡単に対応できます
- メンバーの技術スタックによってはバックエンドはGoで作るなどの選択も見越した構成です
- 私はGo書いたことないのでTypeScriptでこの構成にします
- Cloud Armorを入れることでDDoSなどの対策ができます
- サービスがもっと大きくなることがわかっているのであれば、Apollo Serverを置いているところをBFFとして振る舞うようにして、さらに後ろにCloud Runをいくつか置いてマイクロサービス構成にすることもできます(Souzohさんの構成はそうなっていますね、この記事は大好きで穴が空くほど読みました)
- これらをTerraformで組んでしまっているので大体はコマンド一発で構築できます
パターン2と3の間もあると思っていて、FirebaseHostingをそのままLoad balancerに変えてしまえばIAPが使えたりとメリットが増えます。FirebaseHostingは実態はFastlyなのですが、こちらからは何もいじれないに等しいのでカスタマイズしたければ自分でFastlyを導入するか、私みたいに契約が大変そうだからと言う理由でGCPに乗っかった構成にするかになります。
おわりに
今回紹介したやつが最高の完成形だとは全く思っていなくて、現状私が考えつき、私の手で運用できる範囲がこれです。ここはもっとこうしたほうがいいんじゃないかとか、こういうときはどうするんだ?などがあればぜひコメントいただければ嬉しいです。
現状Apollo x GraphQL Code Generatorから完全に抜け出せなくなっており、パターン2,3ばかり使っています。みんなおいでよこの沼へ〜
おまけ
検索機能
全文検索や複雑な検索機能を入れたいときありますよね。そのときの僕の選択肢は2つあります。
- Algolia
- レコード数がそんなに多くならない場合はこちらを使います
- 従量課金のため料金が安く収まりやすいです
- ブラウザから叩けるようになっているのでパフォーマンスがめちゃくちゃ良く、ドキュメントもわかりやすく、なによりフルマネージドで良いです
- レコード数が膨大になることがはじめからわかっているサービスだと爆発的な料金になってしまうので厳しくなります
- Elasticsearch
- レコード数が多いときはこちらを使います
- 設定が難しいので結構導入の難易度が高いですが、僕はなんとか勉強して導入しているサービスがあります。むずかしいです。
Discussion