CloudflareだけでWebアプリを作成してみた

## はじめに

　2024年9月7日 (土)に葛西区民館で開催された[ポケモン知識王決定戦2024](https://damedemotomoto.notion.site/2024-0adb9e18b6f649009f5e0cb25aee5f64)において使用された、ボード解答Webアプリ「[Mogami](https://mogami.ogadra.workers.dev/)」を作成させていただきました。

　この記事ではなぜCloudflareを使ったのかという話を中心に、技術選定とその背景について執筆させていただきます。なお、このアプリケーションで使用した技術は下記の通りです。

- [Cloudflare](https://cloudflare.com/)
    - [Workers](https://workers.cloudflare.com/)
    - [Durable Objects](https://www.cloudflare.com/developer-platform/durable-objects/)
    - [D1](https://developers.cloudflare.com/d1/)
    - [KV](https://developers.cloudflare.com/kv/)
    - [R2](https://developers.cloudflare.com/r2/)
- [Hono](https://hono.dev/)
    - [drizzle](https://orm.drizzle.team/)
    - [Auth.js](https://authjs.dev/)
    - [vercel/satori](https://github.com/vercel/satori)
- [Remix](https://remix.run/)
    - [shadcn/ui](https://ui.shadcn.com/)
    - [Tailwind CSS](https://tailwindcss.com/)
- [k6](https://k6.io/)
- [GitHub Actions](https://github.co.jp/features/actions)

## アプリ開発の背景

　従来のクイズ大会においては、予選から決勝へと至る過程において脱落者が発生し、以降大会に対して「観戦」する以外の選択肢を失ってしまっていました。そこで、準決勝（Semi-Final Round）において、会場内参加者にもクイズに解答する権利を与え、参加者の一部としてより大会を楽しんでもらうという目的のもと、作成させていただきました。

### 準決勝（Semi-Final Round）概要

　詳しくは[ポケモン知識王決定戦2024 / ルール](https://damedemotomoto.notion.site/053abe862a3749f3a52153ae19b4c92b#:ri:)をご覧ください。Mogamiに関わる部分を中心に記述します。

- 大会自体の参加者は151名
- 全20~25問
- すでに勝ち抜いており、壇上で解答する解答者は8~10名
    - 勝ち抜き済みの参加者は「Mogami」の管轄範囲外
- 問題が口頭で読み上げられる
- 壇上解答者がボタンを押すと、ボタンを押した人に対して解答権が与えられる
    - 壇上解答者はホワイトボードにマーカーで手書きして解答する（Mogamiを使用しない）
- 壇上解答者がボタンを押した時点で、「Mogami」システム上で参加者に対して解答権が与えられる
    - 参加者は、スマートフォン等のブラウザを用いて「Mogami」にアクセスし、クイズに解答する
- 「Mogami」での正解者が3名以下だった場合、壇上解答者の正解にボーナスが与えられる

### アプリ概要

- 参加者一人一人に対して、IDが振られる
- 参加者は、大会受付時にログインURLが記されたQRコードを受取る
    - そのQRコードを用いてログインする
- 準決勝が始まったタイミングで、大会スタッフが管理画面を操作する
    - 参加者のスマホの画面が自動的に切り替わる
    - 会場内のスクリーンに映し出された表示が切り替わる
- 参加者はブラウザから解答を送信する
    - 解答が模範解答に存在するかどうかによって正誤判定がなされる
    - 表記揺れなど、模範解答にない解答をした場合は、システム上で「異議申し立て」をすることができる
- 会場内のスクリーンに、会場内正解者数が表示される
    - 会場内正解者数が3名以下の場合は、正解者名が表示される
- すべての問題が終了した段階で、参加者に結果が表示される
- SNSによる共有ボタンを押下した場合、OGP付きで結果が共有される

## 技術選定

### プラットフォームの選定

　本アプリケーションを制作するにあたり、以下の2つを中心に使用技術を検討しました。

1. リアルタイムにクライアントの制御ができるかどうか
2. データの構造を適切に表現できるかどうか

#### リアルタイムにクライアントの制御ができるかどうか
　クイズの解答受付・締切によって、クライアントのHTML Input要素の`enabled/disabled`を動的に切り替えたり、正解・不正解を任意のタイミングで表示したりするため、サーバー側からクライアント側に何かしらの通信手段でメッセージを送信できる必要がありました。現状それができるプロトコルは

- [WebSocket](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API)
- [Server-Sents Event](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events)
- [HTTP Long Poling](https://datatracker.ietf.org/doc/html/rfc6202)

　の3つかな、と思います。（ほかにあればご教示ください）なかでも、サーバー側から任意のタイミングで、任意のクライアントに、別々のJSONを送りたい…！というようなユースケースに対しては、WebSocketで実装するのが一番シンプルに実装できそうかな、という感覚がありました。（今までWebSocketを使うアプリケーションをいくつか作成してきたため。）WebSocketを（安価に）実装するとすると、以下の選択肢があります。

 - [CloudflareのDurable Objects](https://developers.cloudflare.com/durable-objects/reference/websockets/)
 - [Amazon API GatewayのWebSocket API](https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-chat-app.html)

　上記の2つはいずれも、WebSocketを安価に実装することができます。API Gatewayは無料範囲内に収まるでしょう。Cloudflare Durable Objectsはそれ自体の使用に5ドル/月かかりますが、それ以上かかることは考えにくいです。

#### データの構造を適切に表現できるかどうか

　これはズバリ、「`NoSQL`（FirestoreやDynamoDBなど）によってデータが表現できるかどうか」と言い換えられます。
　アプリケーションの規模自体そこまで大きくなく、NoSQLで表現しきれないことはないと思いました。しかしながら、集計系のクエリ発行時に無理をする可能性があると考え（個人的にNoSQLの経験が乏しいこともあり）、RDBの使用を検討しました。そこで検討したサービス・構成は、以下の5つです。

- [Cloudflare D1](https://developers.cloudflare.com/d1/)
- [Supabase](https://supabase.com/)
- [AWS EFS](https://aws.amazon.com/jp/efs/)の上にSQLiteを乗せる
- レンタルサーバーで諸々構築
- 自宅サーバーで構築して[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) or [ngrok](https://ngrok.com/)

　今回作成するアプリケーションではリアルタイム性を求めるため、サービスを跨ぐことによるネットワークレイテンシの発生を可能な限り避けたいと考えました。また、オンプレやレンタルサーバーは、D1に比べると手軽さという面で劣るため、最終手段的な位置づけとしていました。
　Cloudflare D1は、SQLiteベースのサービスのため、PostgreSQLやMySQLに比べるとどうしても機能面で劣ってしまう部分があります。しかしながらMogamiではそのような機能は使用しないため、必要十分だと考えました。（同様の理由で、EFSにSQLiteを載せることも選択肢として挙げています）

　上記を踏まえ、`Cloudflare Workes x D1`, `AWS Lambda x EFS`の組み合わせのどちらかが良いだろう、と考えました。（RDS、高すぎますね…）どちらも料金や性能に極端な差があるとは思えませんでした。そこで個人的な技術的関心でえいやと決めて **Cloudflare Wokers x D1**を使うこととしました。それに合わせる形で、KVやR2なども使用しました。

<details>
<summary>Cloudflareに技術的関心を持っていた理由</summary>

- EFSのデータメンテやマイグレーションが面倒
  - 一時的にEC2インスタンスを立ち上げたり、それ専用のLambdaを作ったりする必要がある
  - ここらへんはまだコード例が薄く、手軽に実装できそうではない
- AWS Lambda x EFS構成は以前作ったことがあったので、別の技術を試してみたかった
  - Cloudflare Workersと、親和性のあるHono, Remixなど
  - 最近周りにCloudflareユーザーが増えている（体感）ので、キャッチアップすると嬉しそう
- エッジコンピューティングに興味があった
- 個人的なドメイン管理をCloudflare Registrarで行っているため
  - 仮に独自ドメインを作るとなっても、楽に当てられそう
  - 今回は使いませんでしたが…

</details>



### バックエンドの選定

#### フレームワーク

　バックエンドフレームワークは、Cloudflare Workersで動くJavaScriptフレームワークから選定する必要がありました。Cloudflare Workersではファイルサイズ制限が存在することもあり、バックエンドフレームワークとして採用するのは現状[Hono](https://hono.dev/)一択であると思います。

#### ORM

　上記ファイルサイズ制限により、[Drizzle](https://orm.drizzle.team/)が[Prisma](https://www.prisma.io/)より適していると思いました。
　実際に触ってみた感想として、DrizzleはPrismaよりデータベースの可視化（Studio）がリッチでした。ブラウザ上でSQLやDrizzleのコードが実行できます。また本番環境のDBに対しても容易にDrizzle Studioを開くことができるため、本番環境のデータメンテが必要な際もDrizzle Studioを用いていました。D1はTCPで直にSQLに接続できるわけではないため、この機能がなければまともに使えなかったと言っても過言ではないでしょう。

#### 認証

　HonoとDrizzleそれぞれにアダプタがあり、容易に実装が可能なAuth.jsを用いました。認証の実装は「独自の実装を持ち込まない」ことが原則であるため、可能な限りフレームワークの機能を用いて実装しました。
　Auth.jsでは、管理者アカウントにおける、Discordを用いたOAuthログインを実装しました。カスタマイズしたテーブル名も使えましたし、本当に助かりました :pray: 
　また、会場内参加者の認証は`hono/jwt`を用いて実装しました。
 
#### OGP生成

　Cloudflare Workersによる画像生成は、[SVGで頑張ったり](https://qiita.com/kedama-t/items/c1878b1b5d2c02a7830a)、[Browser Rendering](https://developers.cloudflare.com/browser-rendering/)を[使ったり](https://blog.3qe.us/entry/2023/10/30/234102)、[wasmを使ったり](https://blog.keiya01.dev/entry/og-image-in-workers/)する例があるようですが、今回は[vercel/satori](https://github.com/vercel/satori)を使用することにしました。

　OGP生成機能自体、大会の前日から開発を始めたため、[検索上位にきた記事](https://zenn.dev/beenos_tech/articles/20230413_generate-and-cache-ogp-image-on-cf)をコピペしたというのが理由ですが、そのまますんなり動いてくれたので助かりました。[Aki](https://zenn.dev/aki_webii)さんありがとうございます :bow: :bow: :bow: 

<figure>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596394/d05e04cf-46d5-1690-06dd-a3c8fef3bd9b.png" alt="大会前日9時に開かれて、当日15時半にMergeされたOGP生成のPR" />
<font color="#888">
大会前日9時に開かれて、当日15時半にMergeされたOGP生成のPR
</font>
</figure>

#### フロントエンド

　まずReact / Vue.js / Svelteの3大フレームワークから選択することになりますが、個人的に1番慣れているReactを選択しました。（個人的には、JSXだいすきです）

　フレームワークとしては、極度に速度要件が求められるわけではないものとして、下記を検討しました。
 
 - [Next.js (Pages Router)](https://nextjs.org/docs/pages)
 - [TanStack Router](https://tanstack.com/router/latest)
 - [Remix](https://remix.run/)

　Mogamiでは、SSR(Server-side Rendering)によってアプリケーションを配信しています。理由は3つあります。
　1つ目は、バージョンの古いスマートフォンでレンダリングする場合などを考慮した場合、SSR or SSGでHTMLを配信してあげることが望ましいと考えたからです。
　2つ目に、アプリケーションの特性上、1ページに大量の画像を表示する、といったことはなく、表示のボトルネックとしてネットワーク帯域が問題になることは少ないからです。
　3つ目に、SSGでキャッシュするほどAPIの重いページは存在しないため、SSRで事足りるからです。
　上記の理由から、SSRしやすいフレームワークを選びました。Cloudflare上でRemixを使っている例がたくさんあったので、Remixを採用することにしました。


 #### デザインシステム

　ファイルサイズ制限があるため、コンポーネント毎に手元へ置いて使う[shadcn/ui](https://ui.shadcn.com/)がユースケースに対してフィットしていると感じました。必然的に[Tailwind CSS](https://tailwindcss.com/)を使うことになるため、[v0](https://v0.dev)との親和性に期待していました。
　しかしながら、v0は思った通りのデザインを吐き出してくれないこと（特にモバイル向けデザイン）、でプロンプトを打ち込むストレスが直でスタイリングする労力を上回っていると感じたため、ほとんど使用することはありませんでした。全くデザインが定まっていないプロトタイプ生成には向いているかもしれませんが、ある程度 or かなりデザインが定まっている場合だと、あまりユースケースに向かないかなという気がしました。（ここらへんは自分がv0を使いこなせていないだけだと思っています…）
　Tailwind自体に対する賛否両論は時々SNSを賑わせておりますが、私は否定よりなため、次作るとしたら[Yamada UI](https://yamada-ui.com/)を使ってみようかなという気持ちでいます。かといって既存のシステムを書き換えるほどではないですが…

#### 負荷試験

　Mogamiの最大負荷としては会場内の150名と見学者の30名、計180名が同時にアクセスすることを想定していました。Cloudflareがそこまでの負荷に耐えられるかどうか不安だったため、[k6](https://k6.io/)を使用した負荷試験を行いました。余裕を持って250人ほどの試験を行いましたが、同時にWebSocketでリクエストしても問題なさそうであることを確認できたため、安心しました。

#### CI/CD

　語るまでもないですが、[GitHub Actions](https://github.co.jp/features/actions)による自動化を各所で行いました。具体的には

- PRの作成
- migrationの実行
- STG / PRDに対するデプロイ
 
　を行いました。本当はテストの自動実行までやらせたかったのですが、そもそもテストコードをまともに書けず… :cry: 

## まとめ

　すべてがCloudflareの上に載ったアプリケーションで、WebSocketによる250人同接（負荷試験）、本番では100人の同接を捌くアプリケーションを開発することができました。常時起動するサーバーなどを用意せず、安価に（月額5.5ドル（税込））WebSocketとRDBを使用したアプリケーションを構築したい方にとって、Cloudflare Workersはとても有力な選択肢になるのではないかなと思います。