TUNAGのフロントエンドを段階的にリプレイスしている背景とインフラ構成

こんにちは、スタメンの手嶋、西川です。

普段はエンゲージメントプラットフォーム「TUNAG」の開発をしています。

プロジェクトの背景

これまでTUNAGは、プロダクトの成長に注力してきた一方で、内部品質や開発効率などに関する課題の解消が後手にまわっていました。

TUNAGが成長軌道に乗ってきた中で、エンジニア組織を拡大させつつ、継続的にプロダクトの価値を素早く世の中に届けていくためには、開発者体験(以下、DevEx)に関する改善が不可欠だと考えています。

これまでTUNAGのフロントエンドは、Ruby on Rails(以下、Rails)にReactを乗せる形で開発を行ってきましたが、Railsのインターフェイスに依存するが故に開発効率を妨げていたり、不具合の発生確率を高めてしまうというという問題があり、DevExとしては望ましくない状況でした。

また、フロントエンド単独でのデプロイやデプロイの切り戻しを行うことが出来ないなど、CI/CDに関しても多くの問題を抱えており、リリースに対してプレッシャーを感じてしまうエンジニアも一定数存在していました。

私たちのチームではDevExの改善、またユーザー体験の向上を目的として、2023年頭から本格的にフロントエンド環境をリプレイスするプロジェクトに取り組んでいます。

これまでは主にReactで開発を行ってきましたが、Reactの公式ページでもフルスタックフレームワークである「Next.js」や「Remix」が推奨されていることもあり、長期的な視点ではそのようなフレームワークを採用することで、ユーザー体験やDevExの向上に繋げることができると考え、今回Next.jsへのリプレイスを決断しました。

プロジェクトとしてはまだ道半ばではありますが、先日インフラの基盤変更も含めた1stリリースを完了させることができました。

この記事ではインフラに特化して、技術選定やリプレイスのフロー、プロジェクトの振り返りについて紹介します。(フロントエンドの技術選定や運用に関しては、こちらの記事をご覧ください。)

技術構成・選定

ここからは、フロントエンドのNext.jsへのリプレイスのために行った、インフラ基盤の構成変更についてご紹介します。

リプレイス前後のインフラ構成は以下の通りです。(※一部簡略化しています)

リプレイス前

リプレイス後

リプレイス前の構成ではALBに直接向けていましたが、リプレイス後はCloudFrontを前段に、オリジンにALBとVercelを配置する形に変更しました。

このリプレイスプロジェクトでは、ページ単位での段階的な移行を想定しており、旧環境(Rails)と移行後のページ(Next.js)を共存させる必要がありました。このため、段階的に移行が可能な構成としています。

新たに前段に配置したCloudFrontでは、パスパターンに基づき、ALB(Rails)とVercel(Next.js)へのリクエストを振り分けています。Next.jsへのリクエストパスには「m」をプレフィックスとして付けています。また、Vercelの前段にProxyを設置することは非推奨とされていることや、構成が複雑になること、構成変更に伴うリスクが大きくなることから、CloudFrontの役割を最小限にし、キャッシュをしない構成にしました。

また、社内リリース期間中などにNext.jsへのアクセスを制限する目的で、Lambda@Edgeと比較した上で、CloudFront Functionsを採用しました。主な選定理由は以下の通りです。

  • Lambda@EdgeはRegional Edge Cacheで実行されるのに対して、CloudFront FunctionsはEdge Locationで実行されるため、レスポンスをより高速に返せること
  • 用途が社外IPアドレスからのアクセス制限という軽量な処理であったため、CloudFront Functionsの最大実行時間の制限(1ms)に触れないこと(Lambda@Edgeは5秒)
  • Lambda@Edgeはリクエスト100 万件あたり0.60 USD+実行時間の料金がかかるのに対し、CloudFront Functionsはリクエスト100 万件あたり0.10 USDと安価であること

Next.jsのホスティング先としては、S3も候補に上がりましたが、Next.jsの機能をフルに活用するため、フロントエンドエンジニアが自律的に作業しやすくするためにVercelを選定しました。

そして、CloudFrontをALBの前段に新たに配置するにあたり、CloudFront経由のアクセスであることを保証するため、CloudFrontでカスタムヘッダーを付与し、ALBのリスナールールでそのヘッダーの有無を判定することで、ALBへの直接アクセスを制限しています。

構成変更における注意点

上記の構成変更を行うにあたって、大きく2つの注意点がありました。

1. 意図せず情報がキャッシュされてしまうリスク

前述したように、今回の構成変更でCloudFrontでのキャッシュは行なっていません。しかし、CloudFrontを配置することで、本来キャッシュすべきでない情報が意図せずキャッシュされるリスクがありました。そのリスクを回避するため、CloudFrontの設定や、Nginx、Vercelのキャッシュ設定について検証しました。特にHTTPヘッダーのCache-Controlの内容について、キャッシュすべきでないレスポンスにはprivateが含まれているか等の確認を行いました。

CloudFrontでは、管理キャッシュポリシーの中から、キャッシュを無効にするポリシーであるCachingDisabledを使用しています。今回、CloudFrontのディストリビューションはCloudFormationで作成しており、以下のように設定しました。

CloudFrontDistribution:
  Type: 'AWS::CloudFront::Distribution'
  Properties:
    DistributionConfig:
      CacheBehaviors:
        - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # Managed-CachingDisabled

2. ダウンタイム発生のリスク

Route53のトラフィックのルーティング先をALBからCloudFrontに切り替える際や、CloudFront・CloudFront Functionsを更新する際にダウンタイムが発生しないことを、AWS公式ドキュメントや本番と同じ構成にしたステージング環境で確認した上で、構成変更を行いました。

実際のリプレイスでは、ダウンタイムを発生させることなく、構成変更を成功させることができました。

また上記2点を検証した上で、構成変更を行う前に、一部のメンバーがCloudFrontのIPアドレスをhostsに登録し、ローカルからCloudFrontに接続して動作確認を行いました。

さらに、これらの問題が発生した場合にはすぐに検知・対応できるよう、アラートを事前に設定しました。設定したアラートは以下の通りです。

  1. 5xx エラー率
  2. キャッシュヒットレート

    CloudFront がそのキャッシュからコンテンツを送信した対象のすべてのキャッシュ可能なリクエストの割合 (%)。今回の構成ではキャッシュをしないため、キャッシュヒットレートが0を超えた時にアラートが通知されるように設定しています。

  3. CloudFront Functionsのコンピューティング使用率

    関数の実行にかかった時間 (最大許容時間に対するパーセンテージ)。現状は35%前後で推移していますが、使用率が増加した場合に処理内容を見直すことができるよう、アラートを設定しています。

加えて、CloudFrontのログ分析のために、Athenaを用いたログ検索環境も整備しています。

リプレイスのフロー

冒頭にもご紹介した通り、リプレイスのプロジェクトは現在も続いており、主要なページから段階的に移行を進めています。

一度のリリースで1つのページ全体を置き換えてしまうと不具合が発生するリスクが高いため、以下のステップを経て細かくデプロイを行いながらリプレイスを進めています。

  1. Next.jsの新ページ(pages)を作成
    • この時点ではアプリケーション内に新ページへの導線は存在しません
  2. 新ページへのアクセスを社内IPに限定する
    • こちらの制御は上述のCloudFront Functionsで行っています
  3. 新ページに機能デプロイを細かく行う
    • 基本的にエンジニアの動作確認以外の目的では、ページへのアクセスがないため仮に不具合が発生しても、影響範囲を最小限に抑えることができています
  4. スコープを限定してリリース(社内リリース)を行う
    • TUNAGは弊社でも利用しているため、まずは社内限定でリリースを行っています
    • その際にRailsのコントローラーでリダイレクト処理を入れ、アプリケーション内でRails(旧環境)にアクセスがあった際に、新環境(Next.js)にリダイレクトさせています
  5. 全体にリリースを適用させる
    • 最後のstepとして上記のリダイレクト処理を全体に適用させました
    • 同時にCloudFront Functionsで行っていたアクセス制限(2)の設定を削除し、全てのユーザーからのアクセスを許可するように変更しています

上記のステップを踏むことで、不具合の可能性を減らしかつ開発者が安心してリリースを行うことができています。

振り返り

繰り返しとなりますが、フロントエンドリプレイスの目的は、DevExの改善とユーザー体験の向上でした。DevExの改善に関しては、Railsとの依存がなくなりシンプルな形で開発ができるようになったことで、社内のエンジニアから喜びの声が沢山あがっています。

またCI/CDに関しても、以前の環境と比較して約10倍のスピードで完了しており、ユーザーへの価値デリバリーという観点で非常に大きな改善を行うことができました。

またデプロイに際して、開発者が必要以上に気を張らなければならない問題も解消され、デプロイ頻度を大幅に増加させることができました。

以前のページと比較して表示速度に関するパフォーマンスも向上しており、ユーザー体験としても良い効果が出ています。

一方で、Vercelの機能にはまだ活用の余地があるため、今後も継続してキャッチアップしていきたいと思います。

最後に

TUNAGのフロントエンドリプレイスの背景、インフラの技術選定、リプレイスのフロー、振り返りについて紹介しました。同じ様にフロントエンドをリプレイスする際に、本記事が少しでも参考になれば幸いです。

株式会社スタメンではエンジニアを絶賛募集中です!!

興味を持っていただけたら、下記のリンクからご応募ください!

お待ちしております!

herp.careers