🍻

ファーストリリース前にモノレポ移行した話

2023/12/16に公開

これは株式会社エス・エム・エス Advent Calendar 2023の16日目の記事です。

もういくつ寝るとクリスマス〜

こんにちは。@kimukei です。

突然ですが、カイポケリニューアルプロジェクトでは、現在モノレポでアプリケーションの開発を進めています。
とはいえ、最初からモノレポだったわけではなく、ポリレポからモノレポへ途中から統合しました。
統合の経緯を簡単に述べると、このプロジェクトでは各バックエンドサービスで採用した技術スタックがほぼ同じ[1]で、モノレポで享受できるメリットが多そうだったため、まだファーストリリース前だしまずは実験的にやってみよう!となりそのまま現在にまで至った感じです。

現在でモノレポに移行してから約13ヶ月が経ちました。
今回は、モノレポにしたリポジトリのInitial Committerが私ということもあり(?)モノレポ移行の勘所や移行してどうだったかなどを私の目線から書いてみようと思います。

モノレポ移行時

リポジトリの設定

モノレポは触るディレクトリパスでオーナーシップを持つチームが変わるので、CODEOWNERS の設定は必須級です。
触ったパスによってPRにauto labelingしてくれるLabelerというActionも便利でした。
GitHub Actionsで動くCIも paths の指定がほぼ必須となります。でないと、関係ないCIが実行されまくってしまいます。

また、リポジトリが大きくなる将来を見越してCIなどでsparse checkouts[2]を積極的に導入していきました。

ブランチプロテクションルールもいくつか設定していきましたが、それについて書く前にまず前提となるブランチ戦略について簡単に説明します。
このプロジェクトではトランクベース開発[3]に影響を受けています。
mainline をデフォルトブランチとなる main ブランチで進めています。

そのため、main ブランチの更新頻度がめちゃくちゃ激しいです。毎朝 main ブランチを pull してくるとだいたい 100 commits 以上は積まれています。
リポジトリ自体の設定や main ブランチのブランチプロテクションルールなどについて、きつさとゆるさの匙加減だったり工夫が必要だったものがあるのでいくつか紹介します。

Require approvals の数が各チームの裁量では決めづらくなる

前述した通りこのプロジェクトのブランチ戦略はトランクベース開発の考えが基底にあり、複数チームが並列で開発し全チームのマージ先がmainブランチになっています。

そのため、mainブランチへの必須Approve数の設定には全チームの合意が必要になりました。
必須Approve数を1にしていたチームもあれば2にしていたチームもあり、その狙いは主にコードの標準化(たとえば3人の開発者のチームの時に2Approve必須だと全ての開発者がそのコードに目を通したことになる)が目的だったため、プロテクションルールの必須Approve数は数の少ない方に倒して、別途レビュワーの自動アサイン機能[4]などでコードの標準化を促していきました。

PR マージ合戦を避ける

Require branches to be up to date before merging の設定を有効化すると、

  1. PR レビューが通る
  2. main から遅れてるので update branch
  3. CI が通るまでに main が進む
  4. 2 に戻る

という、先にマージしたもん勝ちな構図が生まれる懸念があります。現在ではGitHubのmerge queueがこの問題をいい感じに解決してくれそうですが、まだその機能が使えないリポジトリもあるかと思います。
そんな時は疑似的にではありますが、マージ前に特定の名前のCIがパスすることを要求することができる[5]ので、それを有効化することでPRでは

  1. update branch
  2. Auto merge の有効化

という手順を取るとかなり保証されたコードをマージすることができました。

マージに必須なCI

上で少し触れた、マージ前に特定の名前のCIがパスすることを要求する設定ですが、これをモノレポで運用していく際にも注意点があります。
それは、マージするのにパス必須なCIの名前を全チームで揃える必要があるという点です。

たとえば、Aチームがオーナーシップを持っているサービスでPull Requestのトリガーでテストを実行するワークフローがあったとしましょう。
Aチームはこのワークフローの「run-test」jobがPRのマージには必須にしたいと思っています。

on:
  pull_request:
    paths:
      - 'serviceA/**'
  
jobs:
  run-test:
    ...

BチームもCIを構築しており、それをマージに必須にしたいと思っています。
その場合、mainブランチには「run-test」という名前のStatus checkを必須にして、BチームのそのCIのワークフローのjobも「run-test」で揃える必要が出てきます。
ちなみにこの設定は、複数の「run-test」jobが実行されている場合はすべての「run-test」が成功することを要求します。

ハマりポイントとして、どのチームにも属さないような変更のあった場合でも「run-test」という名前のStatus checkを必須にしているためとりあえず何もしなくてもパスするようなCIを1つ構築しないとうまくいきませんでした。

とりあえず「run-test」を実行してパスするが、課金は最小限に抑えたいと思い書いたワークフローがこんな感じになりました。

on:
  pull_request:
    types: [ opened ]

jobs:
  run-test:
    runs-on: ubuntu-latest
    timeout-minutes: 1
    if: false
    steps:
      - name: skip required-job
        run: echo "とりあえずrun-testというジョブをSuccessfulにするためのダミーのジョブです。"

GitHub Actions の仕様で、スキップされたjobのステータスは成功とみなします。
よって、if: false で常にスキップするjobを作っておくと、実行時間が0秒のjobになりました。

補足ですが、jobにはnameを設定することができるのでそっちの名前で設定して必須化する方が運用しやすいかもしれません。
たとえば、「Required CI」をStatus check必須にすると、それぞれ必須にしたいjobのnameは「Required CI」になります。

name: workflow A

...

jobs:
  run-test:
    name: Required CI
name: workflow B

...

jobs:
  build:
    name: Required CI

よく手動実行で使うワークフローが GitHub Actions のページで埋もれてしまう問題

弊プロジェクトだと、GitHub Actions をフル活用しています。
そのためモノレポだと、各チームのワークフローが一元管理され Actions ページのサイドバーがワークフローでいっぱいになってしまいます。

現状、ワークフローのグループ化などはできないのですべてが平たく並んでしまいます。
また、ここの並び順はワークフロー名のASCIIコード昇順なので workflow_dispatch で手動実行する頻度が高いもの(例えば手動デプロイ系など)はワークフロー名の先頭に ! を付与し優先順位を上位に持ってきたりしました。

name: !awesome-workflow

on:
  workflow_dispatch:

jobs:
  ...

また、実験的に実行したワークフローのログもサイドバーに残り続けるため、定期的にお掃除するワークフロー[6]も用意したりしました。

コードの移行

リポジトリ自体の移行はそれぞれオーナーシップを持つチームの裁量で順番に実行されました。

$ rsync -a ../repo-from/ ./repo-to/ --exclude '.git/' --exclude 'node_modules/'

みたいな感じでえいやって持ってきて[7]からコードベースを微調整をするチームもあれば、Gitのhistoryも含めて持ってくるチームもあったりって感じでした。
ここを思い切りよくできたのは間違いなくファーストリリース前だったからです。また、historyごと持ってくる際にコミットログなどで #10 のような参照があると移行後では参照は壊れてしまい関係ないIssueに紐づいてしまう点は少し注意です。

移行の際に、hraban/tomono のようなツールは役立つかもしれません。

移行初期

すんなり移行完了!おわり!となれば平和ですが、勢いで移行したこともありやはり期待した動作をしない箇所がいくつか出ました。
CI/CDが走らない、または走りすぎてしまう、とある変更を加えるとビルドが成功しない、など色々出てきます。
また、そういった問題が複数チームを跨いだところ、例えば共通の社内ライブラリやデプロイパイプラインやモノレポの設定などで出現してきます。
そのため、複数チームにまたがって開発者の安定した開発をサポートする基盤を整備するチームやリソースがあると成功しやすいと思います。ちょうど Platform Engeneering と呼ばれる領域の一部かもしれません。

モノレポになったことで、共有する仕組みやライブラリの流布がやりやすくなった分、それらのオーナーシップを明確にし、安定供給を図ることの重要性がより浮き彫りになりました。

移行初期ではそれらの課題が少し宙に浮きがちでした。また、修正をする際も複数チームが使っている仕組みだったりするのでレビューサイクルも非同期でなかなか高速化しません。
そんな時はIssue/PRの作り方、特に背景の説明やIssueの定義、どう解決しようと思ってどう実装したのかなどの情報を適切にまとめ、適用したい側と適用される側の認知の齟齬を埋めることの重要性が高まります。
と、難しそうなことを書いたものの、まず重要なのは見ようと思われるものを出すことだと感じます。
タイトルだけ見て「うっ、重そうだ明日見よう」とさせずに「なんやこれ!」と思うような遊び心も悪くないかなと思います。[8]

これは実際のPRのタイトルです。( 改めて見てもなんやこれって感じですが

中身は単純な Renovate のrate limitの緩和や実行タイミングの調整などで数行の設定ファイルの diff なのですが、意外とそういうPRでも他のチームのレビューが必要となるとフィードバックが1日以上開いたりすることがあります。
そこで「なんやこれ!」の魔力が活きてくるかもしれません。

また、こういったPRでチーム間の会話も生まれやすかったりします。[9]
高いドメインの壁で知らず知らずのうちに築かれたチーム間の障壁をどんどん遊び心で溶かしていきましょう![10]

おわりに

モノレポ移行してから一年以上が経過しましたが、ちょうど最近社内でモノレポ化してどうだったか振り返り会のようなものが開かれ、デメリットよりメリットの方が大きかったというフィードバックが得られました。
もちろん、技術スタックが違いすぎてモノレポ化の恩恵が得られづらいものもありますので、用法・用量守って適切な単位でのモノレポ化を!と言いたいところですが、それが難しいところでもあります。
現に統合後にデメリットの影響の方が大きいと判断して分離したリポジトリもあったりします。
弊プロジェクトの場合は運用のウエイトが小さいタイミングだったため、モノレポに統合または分離がしやすいタイミングだったというのは間違いなくあります。
とはいえ、純粋な統合と分離の作業自体は重くありませんのでチャレンジの壁はそこまで高いものではないと感じました。

それでは!楽しんでいただけたら幸いです。
Happy Hacking!

脚注
  1. メイン言語がKotlinでフレームワークがSpring BootでRDBがPostgreSQLで通信がGraphQLという点で採用スタックがだいたい同じでした ↩︎

  2. ちょうど2023年に本家のactions/checkoutがsparse checkoutできるようになったんですよ! https://github.com/actions/checkout/pull/1369 ↩︎

  3. おおよそ https://trunkbaseddevelopment.com/ で紹介されている手法です ↩︎

  4. https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team#about-auto-assignment の設定 ↩︎

  5. GitHub の Branch protection rule にある Require status checks to pass before merging > Status checks that are required. の設定 ↩︎

  6. ワークフローの実行ログがすべて消え、ワークフローファイルがデフォルトブランチに存在しないとサイドバーからもそのワークフローの項目が消えるようです。 ↩︎

  7. まだファーストリリース前だしまずは実験的にやってみよう! ↩︎

  8. ファーストリリース前の勢いみたいなところもあります ↩︎

  9. フルリモートだとマジでこういうなんでもないようなきっかけが振り返ってみると結構大事だったりします ↩︎

  10. ちょうど弊チームEMの hotpepsi さんに「kimukeiさんのPR好きなんですよね。アドベントカレンダー書いてくださいよ(意訳)」とこの話題になったため少し触れさせていただきました ↩︎

Discussion