はじめに
READYFORでプロダクトエンジニアのテックリードをしている橋本です。
本記事は READYFOR Advent Calendar 2024 の 20 日目の記事です。
READYFORでは現在、メルマガの配信に外部サービスを利用しています。しかし、外部サービスを利用する都合上、運用上の不便さ、社内に溜めているデータを利用した配信の最適化がしにくいなどいくつかの課題を抱えています。今回、これらの課題を解決するために配信システムの内製化をすることにしました。本記事では、採用を決めた技術スタックとその選定理由についてまとめていきます。
ざっくり要件
内製するアプリケーションの細かい要件は省略しますが、大まかには以下の要件になります。
- ユーザーは社内の特定のチームに限定し、それ以外のメンバーはアクセスできないようにする
- メールの配信内容は一定のカスタマイズが可能であり、配信内容や配信先の選択が可能なフォームから作成・調整できる
- 配信予約を行い、任意の時間に自動で配信できるようにする
選定した技術
アーキテクチャ
サーバーレスをベースにして、以下の構成を採用しました
アプリケーション関連
バックエンド側
RailsをAPIモードにします。配信JobはActiveJobで動かし、アダプターとして cloudtasker を使います。
フロントエンド側
ReactでSPAアプリケーションを作ります。プロジェクトは、Viteで作成します。
UIコンポーネントには、社内で作成しているコンポーネントライブラリを使用します。
選定(意思決定の)理由
SPAとAPIアプリケーションの分離
READYFORのプロダクトが、全体としてスキーマ駆動開発(OpenAPIを活用した設計手法)をベースにしているというのが主な理由です。RailsのMPAでも開発は可能ですが、分離する方針にすることにより以下の恩恵を受けることができます。
- 定義したOpenAPIスキーマからFEのAPIクライアント、BEのシリアライザを自動生成する仕組みは整っているので使いまわすことができる
- Reactコンポーネントのライブラリも整備済みのため、Reactを採用することで、デザイン・実装の一貫性を保つことができる。また合わせて実装コストも大幅に削減できる
Next.jsを採用しない理由
最近のトレンドとしてNext.jsが最初に挙がることが多いですが、今回採用しなかった理由は単純で、要件に対してNext.jsが過剰であるためです。Reactに加え、軽量なルーティングライブラリと社内のUIコンポーネントライブラリを組み合わせる構成で十分事足ります。また、バージョンアップデートなどの運用面でも、コストを軽く保つことができます。
APIアプリケーションにRailsを採用した理由
近年のトレンドや傾向からは、Ruby、Python、TypeScript、PHP、Go、Rust、Javaなど、さまざまな候補が挙げられます。現状動いているREADYFORのプロダクトは、Ruby(Rails) + TypeScript(React) で動いています。そのため、メンバーが書ける言語を前提とすると TypeScript or Ruby の二択になります。(なお、個人的にはRustを選ぶという選択肢にも惹かれましたが、今回はプロダクト要件に従って評価しました。)
ここで、TypeScriptをAPIアプリケーションの実装に採用することを考えてみます。(型をつけながら開発するのは体験が良いですしね)
まず、READYFORのBEでTypeScript(Node.js)環境を採用しているところはありません。そのため、既存の資産を活用して技術選定をすることができないので、ゼロベースで検討する必要があります。
その上で、以下のいくつかの点を考える必要があります。
- APIアプリケーションを立ち上げるのに何を使うか
- Expressを使う?NestJSを使う?Honoを使う? - DBへの問い合わせに何を使うか
- ORMを使わずに直接SQLを書く?Prismを使う?TypeORMを使う? - bundlerに何を使うか
- webpackを使う?rollupを使う?esbuildを使う?最近だとrspackやbun、swc, tsupなども選択としてあり得そうですね
- アプリケーションのアーキテクチャはどうするか
- クリーンアーキテクチャなどの話を詰めると終わりがない
- 配信Jobはどう動かすか
などなど、検討すべき点はかなり多く限られた時間の中で検証・意思決定を行うには時間が足りません。
一方Railsを採用した場合、検討すべき点は大幅に減ります。(最初のAPIを立ち上げるまでのセットアップ時間も大幅に削減できます)
もう一つの観点としてメンバーのスキルセットがあります。今いるメンバーでBEのコードを書く場合、TypeScriptで書くよりRuby(Rails)で書く方が、開発・レビュー体制ともに厚くすることができます。
これらの点を考慮した上で、最終的にRuby(Rails)を採用することにしました。
Identity Platformを採用した理由
これはアプリケーション側で認証処理を実装するコストを削減することが目的です。Identity Platformは認証の仕組み・ユーザー管理を扱ってくれるので、アプリケーション側ではidTokenの検証のみを行えば十分になります。社員のアクセス制御については、ブロッキング関数を活用することで実現する方針です。
cloudtaskerを採用した理由
最近のRailsアプリケーションでは、初期でsolid_queueが含まれて生成されます。他の選択肢としてはSidekiqやDelayedJobなどが挙げられます。
これらの選択肢として共通することは、queueを監視するためにワーカーを常時動かしておく必要がある点です。メルマガ配信はその性質上、1日に何回も実行されるものではありません。その実行のためにワーカーを起動し続ける方針をとることはコストのやワーカーの管理の観点から見ても良いとはいえません。
一方、cloudtaskerはqueueとしてCloud Tasksを利用します。キューの監視はCloud Tasksに完全に移譲できるため、運用コストは発生しません。また無料枠も大きく(100万回まで)、採用することによるデメリットが少ないです。ローカルでの開発についても、cloudtaskerはRedisをキューとして動かす開発モードをサポートしているため、開発環境での動作確認が容易です。
まとめ
トレンドだから、イケているからだけでなく運用の継続を見据えた技術選定をしましょう
(とはいえ、イケている選定もとても大事)