RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

急成長中サービスを回しながら開発者体験も考えて Rails フロントエンドをモノレポ移行した話

ラクスル事業部エンタープライズ開発グループの宮田です。

印刷のラクスルの大企業向け EC プラットフォームである「ラクスルエンタープライズ」でサーバーサイド開発を担当しています。

今回の記事では、フロントエンドプロジェクトのモノレポ構成への移行についてご紹介したいと思います。

我々のチームのモノレポ移行の特徴は、今まさに Rails のフロントエンドとして提供されているプロジェクトにモノレポを取り込んだ点にあると考えています。今後、フロントエンドのモノレポ移行を検討される方へ参考にしていただければと思います。

モノレポを選んだ理由

モノレポとは一言で言うと複数のプロジェクトに分かれたサービスを単一のリポジトリで管理するアーキテクチャです。リポジトリが一つになることでデプロイの高速化や、プロジェクト内でのコードの再利用を促進できるといったメリットがあります。

弊社の印刷事業部では複数の印刷 EC サイトを運用してきましたが、モノレポの導入は初の試みとなります。そこで、導入に至った経緯をご説明したいと思います。

二重実装を減らしたい

我々が開発しているラクスルエンタープライズでは、ラクスルのユーザーの中の大企業顧客を対象とした印刷 ECサイト(以下、エンタープライズサイト)を運用してきました。

バックエンドとフロントエンドは別リポジトリで管理しており、同じスクラムに属するサーバーサイド、フロントエンドメンバーがそれぞれを管理しています。

モノレポ移行を検討するきっかけとなったのは、共通のバックエンドでエンタープライズ企業の顧客をユーザーとしたカスタマイズ EC (以下、専用注文サイト)を新たにローンチしたことです。以前からあるエンタープライズサイトでは、ラクスルの印刷 EC の利点を活かした購入体験を提供しており、こうした体験は新たに開発した専用注文サイトでも提供していきたいものでした。

しかし、現状ではエンタープライズサイトはバックエンドと別のリポジトリでフロントエンドサービスを管理し、RPC でフロントとバックエンドが通信しているという構成でした。それに対して、専用注文サイトでは、開発のスピード感を重視し、Rails の標準的なフロントエンド構成を採用していました。そのため、エンタープライズサイト向けに RPC ベースで作られた新機能を専用注文サイトに展開するためには、 API 実装が二度手間になるという課題がありました。

そこで提案されたのが、両サイトをバックエンドのリポジトリ内のモノレポで管理する案です。このようにすることで、リポジトリ内に追加された API 定義を両サイトから使えるようになり、二重実装を減らすことができます。

移行のタイミング

専用注文サイトはリリース後一年程度ですが、多くの顧客要望をいただいており、開発アイテムは常時数十件程ある状態でした。それに加えてリリースまで数ヶ月を要する大型の開発案件が半期の OKR にも設定されており、今後進行していく見込みでした。

サービスの成長速度を緩めたくはないが、今後開発者体験が悪くなるような事態も避けたいというのが開発者側としての心持ちです。

そこで、今後の開発スケジュールを考慮し、大型案件に着手する前のタイミングでモノレポ移行を段階的に進めることが決定しました。

モノレポ移行にあたっては、移行担当メンバーが割り当てられ(フロントエンド 2 名、サーバーサイド 1 名)、他のメンバーは別の新規開発・運用案件を進めるという体制が敷かれました。サービスの成長速度を緩めず、将来の開発者体験を見据えたアクションを取れた点が良かったと感じています。

また、開発案件が詰まっている状態で、直接的なリターンには結びつかないアーキテクチャ移行にリソースを割く決断ができたのは、エンジニアサイドだけでなく事業部サイドに開発者体験への理解があったからこそだと実感しました。

さらに、事業部サイドと調整している開発スケジュールの信頼性が高く、遅れが少ない状態で運用されているため、自信を持って移行のタイミングを設定できたという点も強調したいと思います。

移行作業について

ここからは移行作業の大まかな流れと、作業時に苦労した点についてご説明します。

モノレポ管理ツールとしては利用実績などを踏まえ、 Nx を選択しています。バックエンドの Rails アプリケーションに Nx でベースとなるプロジェクトを構築しました。

参考:https://nx.dev/

これまではフロントエンドのディレクトリごとで行なっていた依存ライブラリの管理をやめ、単一の pakcage.json を用意しています。

また、フロントエンドの各プロジェクトの build や lint といったタスクを実行するのもこのルートディレクトリとなります。

タスクの具体的な実行内容はプロジェクトごと用意された project.json に記述しています。

初期リリース時点では専用注文サイトのみがモノレポに移行しています。libs/clients 以下に移行しており、現状では専用注文サイトのフロントエンドは Rails の構成となっているため、現段階では Rails アプリケーションから利用するライブラリであるためこのような構成となっています。

初期リリース時点での Nx 管理下のルートディレクトリ (frontend/) の構成は以下の通りです。

frontend/
├── README.md
├── apps
├── babel.config.json
├── jest.config.ts
├── jest.preset.js
├── libs
│   ├── api-client
│   └── clients
├── node_modules
├── nx.json
├── package-lock.json
├── package.json
├── tools
│   ├── generators
│   └── tsconfig.tools.json
└── tsconfig.base.json

モノレポ移行前の Rails アプリケーションでは、異なるフロントエンドのプロジェクトが複数存在する状態でした。事情により移行を見送ったプロジェクトもあるのですが、今後は新規のプロジェクトは frontend/ 以下で管理されるため、見通しがよい状態となりそうです。

苦労した点

ここからは移行にあたって苦労した点をご説明します。

静的ファイルの配信

Rails のプロジェクトに移行することもあり、静的ファイル配信のためには一工夫が必要でした。

今回移行対象の Rails アプリケーションでは、minipack を使い、ビルド済みのアセットのディレクトリを指定して view ヘルパーを生成しています。移行後もこの構成を続けるためには、モノレポでのビルドで生成されたアセットを既存のディレクトリに移動する必要がありました。

そこで、モノレポ移行後はビルド時に dist/ 以下へ静的ファイルを生成し、静的ファイルを public/assets 以下に移行するためのカスタムコマンドを定義して、ビルド後にそれを実行するようにしています。具体的には、移行のためのスクリプトを tools/generators に用意し、それを実行するようにしました。

以上のように、プロジェクトとして独立していないフロントエンドのコードであっても、移行の対象とすることは可能であり、普段の開発運用を妨げずに移行することができます。なお、今後はエンタープライズサイトの Nuxt も移行し、アプリケーション別に管理されたディレクトリも追加される想定です。

package manager の違い

移行後のモノレポディレクトリの名称は frontend/ です。実は移行前にもこのディレクトリは存在しており、Rails アプリケーションのフロントエンドを管理していました。このディレクトリでは package manager として yarn を使用していたのですが、モノレポディレクトリでは npm を使用します。この違いが思わぬ挙動を引き起こしました。

開発時の動作確認にあたり、動作確認環境にて、モノレポ移行後のブランチと移行前の開発ブランチを切り替える必要がありました。

移行前のブランチがデプロイされた状態から、移行後をデプロイし、再び移行前へ戻したときに問題は起こります。

この間、package manager で見ると yarn → npm → yarn と入れ替わっています。依存関係の管理は変わらず node_modules/ のままですが、実はその内部の構成は npm と yarn で変わっていました。

そのため、 npm → yarn と入れ替わる際に、npm によって再構成された node_modules/ を yarn で参照することになり、依存関係が解決できなくなるという事態が発生しました。デプロイ時に依存関係を解決できないライブラリのインストールを求められて停止してしまい、強制的に停止する必要があったことを思い出します。

そこで、モノレポ移行のリリースに先立ち、デプロイ前に依存ライブラリを削除することで、まっさらな状態から依存関係を構築するようにしました。依存関係は普段 pakcage manager がよしなに解決してくれているのでブラックボックスとして扱っていましたが、今回のような移行時には内部の挙動まで意識する必要があり、個人的には興味深い経験でした。

リリースに先立つ動作確認

リリースに先立ち、開発時のユーザーストーリーをベースとして動作確認を行いましたが、アプリケーション内のフロントエンドコード全般の移行となるため、全ての挙動を網羅することは困難です。

そこで、リリース予定日の二週間ほど前から、他の開発案件のブランチにもモノレポ移行後のブランチを取り込み、予期せぬ挙動があった場合は連絡してもらうようにしました。

このようにある程度規模感が大きいリリースでは、想定ケースを網羅しきることは難しいため、ランダムな使用機会を増やすことで、期待通り動作することを一定担保することができるという考え方は有用だと感じています。


運用上の Tips

これまでサーバーサイドのメンバーのみが管理していた Rails プロジェクトにフロントエンドのモノレポが誕生したことで、開発運用周りでも細かな修正が必要でした。

具体的には Pull Request 作成時の PR テンプレートの編集や、レビューアサインの設定などです。

PR テンプレートでは、バックエンドとフロントエンドでテンプレートを分け、PR 作成時にどちらかを選択するという方法を取りました。

また、我々のチームでは Pull Request 作成時のレビュワーアサインに GitHub の CODEOWNERS を利用しており、この設定を修正する必要もありました。

余談ですがレビュワーの自動アサインは、予期せぬ変更の検知や PR 作成のメンバーへの周知など様々な場面で役立つため、モノレポ体制に限らず開発プロジェクト全般でオススメです。

フロントエンドメンバーも同じリポジトリを触るようになったため、モノレポ配下のファイルへの変更を検出して、フロントエンドメンバーにレビュワーがアサインされるようにできました。

参考:https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

新しいコードベースの運用に移行する際は、こうした細やかな開発運用上の修正は忘れがちですが、認知負荷を抑えながら普段の開発を続けていくためには重要だと感じています。

デプロイメントラインの整理

バックエンドのリポジトリにフロントエンドのプロジェクトが合流することで、開発時の CI やデプロイ内容も見直す必要がありました。

デプロイ

バックエンドリポジトリでは、Capistrano を使った自動デプロイを行なっており、一連のデプロイタスクの一環として各フロントエンドプロジェクトのビルドを行なっていました。

nx run ではプロジェクト名や環境(production, stage など)を指定できます。初期リリース時には専用注文サイトのみを移行したため、以下の一行への置き換えだけで修正できました。

プロジェクトによらず単一のコマンドでビルド周りを管理できるのはモノレポ管理ツールを採用する大きなメリットだと感じています。

nx run clients-original-site:build:productiopn

差分ビルドを適用した CI

開発時に CI 実行は欠かせないですが、フロントエンドのコードが合流したことにより、その実行内容も見直す必要が出てきました。

我々のプロジェクトの自動化テストは Rails や Nuxt といったアプリケーション単位での結合・単体テストを行なっています。

そこで CI を実行する際にもサーバーサイドのコード修正の際には、サーバー側のジョブ(build, lint, test 等)を走らせ、フロントエンドのジョブは実行されず、その逆もまた然りという状態が開発体験上望ましいと考えられます。

現在利用している CircleCI では orbs という再利用可能な CI ジョブがあり、技術動向をリサーチした結果、その中でも特に path-filtering はモノレポ移行での使用実績が多いと分かりました。

参考:https://circleci.com/developer/ja/orbs/orb/circleci/path-filtering

そこで、以下の理由から差分ビルドの実現のために path-filtering を採用しました。

  1. 運用にあたってのナレッジが豊富に存在する
  2. CircleCI 本体により提供され今後も安定して運用されそう

path-filtering による差分ビルドの実行では、設定ファイルとは別にワークフローの実行内容を記述するファイルを用意します。

設定ファイル側で差分を検出するディレクトリと、そのとき有効化する実行フラグを指定するようになっています。以下に示すのは記述のイメージです。

version: 2.1

setup: true

orbs:
  path-filtering: circleci/path-filtering@0.1.3

workflows:
  version: 2
  build_and_test:
    jobs:
      - path-filtering/filter:
          config-path: .circleci/workflow.yml
          mapping: |
            (?!docs/.*|frontend_ops/.*|frontend/.*).* build-backend true
            frontend/.* build-frontend true
            # ...
version: 2.1

parameters:
  build-backend:
    type: boolean
    default: false
  build-frontend:
    type: boolean
    default: false

jobs:
  build-backend:
    docker:
      - image: cimg/ruby:3.0.3-node
    steps:
      # ...

workflows:
  version: 2
  build-rails:
    when: << pipeline.parameters.build-backend >>
    jobs:
      - build-backend
      # ...

path-filtering を使用した差分ビルドの注意点として以下のような点があります。

  1. mapping で差分を検出するディレクトリを指定するとき、正規表現が不適切だと python のパースエラーのスタックトレースが出てくるだけで失敗の原因がわかりづらい
  2. mapping で指定ディレクトリ以外の差分を検出して実行したいときはパス指定時に ptyhon の正規表現で書くとよい
(?!excluded_dir1|excluded_dir2).* build-project true
  1. 実行されるワークフローがないときは CI がエラーとなるため、ワークフローの指定に使うパイプライン変数が全て false の場合に実行する空のジョブが必要となる
  2. セットアップの設定ファイル(実行時の条件分岐を書くファイル)に加えてジョブ単位で設定ファイルを足そうとしたが、2 個以上ファイルを足すとエラーとなる

モノレポ移行時に限らず、今現在大規模なリポジトリを運用している方々にも是非 path-filtering による差分ビルドはオススメしたいです。

おわりに

今回の記事ではモノレポ移行への道のりについてご紹介しました。

モノレポ移行のようなビジネス観点上のリターンの見えにくい開発は、日々のプロダクト開発の中での優先順位は低くなりがちな印象があります。今回なぜ着手できたのかといえば、年単位での長期的な開発ロードマップを整備していたことで、現在のリポジトリ運用が将来的に開発生産性を低下させることを現時点で予想できたという点が大きいのではないかと感じています。

開発者体験の向上の文脈では、顧客要望に迅速に対応するために開発スピードを上げた結果として蓄積された「過去の技術的負債」の返済に言及されることが多いと感じていますが、

今回のように、将来に備えたアーキテクチャ移行へ開発リソースを割くことは、さながら「未来への先行投資」と考えられる点が対照的で興味深いと感じました。今後はこういった「攻めの開発者体験向上」の観点を持って開発を進めたいと思っています。