uiu です。ハローには創業時に入社し、エンジニアとしてAutoReserveの開発にゼロから関わってきました。現在はバックエンドをメインに担当していますが、領域横断的に開発することを得意としています。
2022年の初めに AutoReserve にあるWebフロントエンドをすべて Vercel に移行しました。
Vercel に移行するのと同時に Turborepo を導入しました。現在、4サービスのWebフロントエンドを monorepo として運用しています。
AutoReserve は、AIが代わりに電話してくれる飲食店向け予約グルメアプリです。iOS / Android アプリ、 Web アプリを提供しています。
また、セルフオーダーシステム AutoReserve Order を提供しており、レストランすべての業務をサポートできるプラットフォームを目指しています。
背景
AutoReserve では React / React Native を活用し、iOS / Android / Web のプラットフォームに同時展開をしています。
iOS / Androidネイティブアプリは React Native で開発し、Web では react-native-web を活用しネイティブ側と同じ構造のコードで開発できるようにしています。
フロントエンドを1つの言語で開発し、最大限コードを共有することで、小さなチームでスピード感のある開発をできるようにしています。
Web には管理画面含め合計4つのサービスがあり、すべて React で開発されています。
1つ特徴を挙げると、Next.js を使わず Server-Side Rendering をしていないということです。代わりに Dynamic Rendering を使って、検索エンジン対策をしています。
SSRを利用しないことで、クライアントサイドでの動作のみを考えればよくなります。そのおかげで、アーキテクチャをシンプルに保つことができ、React Native側とコードが相互に再利用しやすくなっています。
簡単にキーワードを並べると以下のような技術が使われています:
- TypeScript
- React Native
- react-native-web
- redux, redux-saga
- swr
- Cypress
導入前の課題
Vercel 導入前には、Google Cloud 上のサーバーで自前でビルド環境・配信環境を整備して運用していました。 しかし、運用するサービスの規模が大きくなるにつれて、以下のような課題がでてきました。
デプロイの設定・運用コストが高い
デプロイ時に、GCPインスタンス上で yarn install && webpack build
をして nginx 経由で配信する仕組みになっていましたが、
ビルド時間がボトルネックになりやすく、リリース時に待ち時間が大きく発生していました。
ビルド時間は遅いときで約8~10分かかっていました。
ビルド時間を快適に保つためには、CPUリソースを適切に確保したり、適切にキャッシュ設定をしたりする必要があります。そのために、事あるごとにメンテナンスをする必要があり、運用コストが高い状況でした。
また、新しいサービスが増えるごとに、新たに nginx 等で設定をする必要があり、面倒でした。
コード共有が難しい
ネイティブとWebで共通する component や logic を共通ライブラリ(ar_shared)として切り出しています。
個々のWebアプリを別リポジトリとして運用して、個別リポジトリからは ar_shared を package として参照していました。
しかし、実際に運用してみると、リポジトリをまたぐ変更がしづらく課題を感じていました。
手元で yarn link する、開発時に共通ライブラリのアップデート時に GitHub Actions で version bump commit をする、などのテクニックが必要でした。
また、lintやCI設定など、リポジトリをまたいで共通化したい設定があっても、個々のリポジトリで設定しなければなりませんでした。
このような理由で、複数アプリケーションを monorepo 化する機運が高まっていました。
Vercel 導入を決めた理由
Vercel 導入を決めたのは以下のような理由でした:
- ビルドが速い
- Dynamic Rendering が可能
- monorepo をサポートしている
ビルドが速い
一番驚いたのは、特に設定をせずともビルドが速いことでした。
試したリポジトリではデプロイまでにかかる時間が 約8分 -> 約3分 になりました。
ビルドコンテナの立ち上がり時間等が速く、 Cloudflare Pages 等と比較しても圧倒的に速かった。
Dynamic Rendering が可能
Dynamic Rendering では、クローラーの User-Agent を区別し、クローラーに対しては headless chrome でレンダリング済みのHTMLを返します。 リバースプロキシ等を使い、配信の前段で User-Agent を元に出し分けをする必要があります。
Vercel では Rewrites を使うことでこの挙動に対応できます。
monorepo をサポートしている
monorepo をサポートしており、Turborepo を使ったビルド高速化の恩恵を受けられることがメリットでした。
Vercel の公式ドキュメントでも monorepo サポートが公言されていて、今後の機能拡張が期待できます。
他の選択肢
他のホスティングも検討しましたが、選択しなかった当時の理由は以下です:
- Cloudflare Pages
- monorepo をサポートしていない
- Netlify
- 日本にエッジサーバーがなくかなり遅い
- 参考: Netlifyが日本からだと遅い - id:anatooのブログ
導入時の工夫
Dynamic Rendering の設定
rewrites の設定を書きます。検索エンジン最適化の重要性が高いので、動作検証を検証環境でしてからドメインの切り替えを実行しました。
rendertron という headless chrome でHTMLを描画してHTMLを返すサービスに proxy してます。
vercel.json に以下のように設定を書けます:
{ "rewrites": [ { "source": "/:match*", "destination": "https://rendertron-host/render/https://autoreserve.com/:match*", "has": [ { "type": "header", "key": "user-agent", "value": ".*(?:Googlebot|Bingbot|Yandex|Y!J|Baiduspider|Twitterbot|facebookexternalhit|rogerbot|LinkedInBot|Embedly|quora link preview|Pinterest|Slackbot|vkShare|W3C_Validator|Linespider).*" } ] } ] }
curl でリクエストを飛ばすことでHTMLがレンダリングされているかどうかを確認できます:
curl -A "Googlebot" "https://your-app.vercel.app/restaurants/xxxxxxx"
sitemap 等を同一ホストパスで配信している場合は、それも設定しなければいけません。リリース時にこの設定を見落としていて、 sitemap が配信されない問題が発生して困りました。
{ "rewrites": [ { "source": "/sitemap.xml", "destination": "https://.../sitemap.xml" } ] }
monorepo への統合
git の履歴を保ちつつ、複数リポジトリを monorepo に統合するのには、 subtree merges と filter-repo を使いました。
https://docs.github.com/en/get-started/using-git/about-git-subtree-merges
https://docs.github.com/en/get-started/using-git/splitting-a-subfolder-out-into-a-new-repository
時間がかかったのは、turborepo の設定です。
turborepo を使うには yarn workspaces を設定する必要がありますが、yarn workspaces での依存解決は同じyarnだとは思えないほど複雑で、ビルドを通すまでにかなり時間をとられました。
大量の package を nohoist するなどの方法を使ってビルドが通るようにしました。最終的には、nohoistをやめ、アプリそれぞれの依存パッケージのバージョンを気合で揃えることで統合しました。
導入後の効果
狙っていた効果として、ビルドが速くなること、運用コストを減らすことが達成されたので満足しています。
また、monorepo化によって開発環境のセットアップが簡単になり、webフロント開発の変更はしやすくなったのを感じています。
featureブランチごとにデプロイする機能(Preview)が使えるようになり、変更の動作確認が簡単になったこともかなり嬉しいポイントです。
目玉機能の1つである global edge network での配信については、現状特にメリットを感じていないというのが正直なところです。
主要ユーザーが日本の場合、例えば東京にnginxサーバーを単に置くのと、スピード面では少なくとも体感差が出ないと思います。
導入後の課題
Vercel 導入後に現状感じている課題はいくつかあります。
料金がやや高い
コミッター数で課金される、並列ビルドで課金される、など課金ポイントが多く、料金が高くなりがちです。
業務委託等で関わってくれているメンバー数が多いため、開発者が1人増えるごとに課金が高まっていきます。
ビルドが詰まりがち
monorepo の変更内容によっては、すべてのアプリでビルドが trigger されることになります。
開発するタイミングが重なると、ビルドキューが詰まり、待ち時間が発生することがよくあります。
並列ビルドも課金対象であり($50/月)、たくさん並列数を用意しておくのには限界があります。
Ignored Build Step により、diffを見て必要なアプリだけビルドすることは可能ですが、結局共通ライブラリへの変更時にはあまり効果がありません。
リリース管理がむずかしい
複数アプリのリリースを monorepo でどうやって管理すべきか、についてはまだ良い解決策が見つかってません。
アプリによってリリースサイクルが違ったり、アプリ個別にリリースを制御したいケースがあるため、個別にリリースブランチを用意しています。
現状では、master と production-app-1, production-app-2, ... のようにリリースブランチを用意しています。
しかし、4コのアプリがあるため、1つ1つリリース作業をするのが面倒でリリースするのを忘れる、などの問題があります。
まとめ
Vercel で monorepo 開発運用をしている事例を紹介しました。
非 Next.js アプリでも Vercel に載せるメリットは十分にあることを紹介できたのではないかと思っています。
今後は、React Native のネイティブアプリを monorepo 統合するなど、少人数で大きな開発ができる仕組みを一層整えていく予定です。
ハローでは、生産性を追求する本物のエンジニアを募集しています。
少しでも気になる方は気軽に uiu にDMでお声がけください!