🎊

50人以上の開発者が日々使用する10万commitオーバーのGitHubリポジトリを分離した

に公開

はじめに

こんにちは miyamu です。

2025年はテックリードとして様々な技術課題に向き合ってきました。
その中で個人的に最も大きかった技術課題として「リポジトリ分離」があります。以下の記事でも軽く紹介させていただいています。

https://zenn.dev/moneyforward/articles/5c011e4b44ae18

本記事では、この課題に対してどのように対応したかについて、筆を執りたいと思います。少し長くなりますが、お付き合いいただけると幸いです。

分離した背景

とある機能を提供する「GitHub リポジトリX」がありました。当初は1組織に属する2つのプロダクトの10名程度のエンジニアが利用していて、各エンジニアは片方だけではなく両方のプロダクトに対して開発・運用を行っていました。Railsを前提としており、それぞれのプロダクトに対するmodelファイルもここで管理されていたため、ActiveRecordの仕組み上、インフラ上の物理DBのテーブル構造にも依存していました。
またリポジトリ単位でデプロイを行う仕組みであったため、これら2プロダクトはコードベースだけでなくstg/prod環境へのコード反映のライフサイクルも共有していました。

その時点でも、リポジトリの分離について検討されてはいましたが、市場環境や組織状況や技術的状況を鑑み「共有リポジトリを継続する」という判断がなされていました。
私は分離をするかしないかの意思決定には加わっていませんでしたが、結果的に分離した当事者として振り返っても、当時の判断は一定の合理性があったように思います。

その後、当初よりも多くの機能が追加され、ありがたいことに各プロダクトも大きく成長しました。それに伴って組織状況が大きく変わりました。
まず、1組織の各エンジニアが両方のプロダクトを開発する体制から、2組織に分かれてそれぞれのエンジニアが各プロダクトに専念する体制に徐々に変化してきました。また、開発に関わるエンジニアの数も大きく増え、グローバル化し、リポジトリXの規模も10万コミットを越え、関わる人数も50人以上に成長してきました。

発生していた課題

そうなると一例として、以下のような課題が顕在化してきました。

課題1. 技術的複雑度の増大

リポジトリXを通じて、プロダクト間で一部コードを共有していたため、様々な技術的複雑性を生み出していました。

例えば、リポジトリX上のあるコードが「プロダクトA or プロダクトB or 共用」のどれであるかの判断が難しい、という問題がありました。これはJavaScriptファイルなどのフロントエンド関連ファイルにおいて特に顕著で、時折意図せずお互いのプロダクトに影響を与えてしまうことがありました。

また、フレームワークや言語のアップデートなど全体に影響を及ぼすタスクを行う場合、両プロダクトの知識が必要であるため、難易度が高いという問題もありました。影響範囲が両プロダクトにまたがることで、アップデートによって単体テストが落ちた場合の対処や、E2Eテストの計画が困難でした。

その上、2つのプロダクト間で共有しているコードの場合、プロダクトごとを判定する関数などにより分岐が行われている箇所がありました。これはコードの可読性を悪化させることに加え、AIコーディングにおける不要なコンテキストの増大を引き起こしていました。

加えてCIのコスト増にもつながっていました。上述の通り、とあるファイルの変更が両プロダクトに影響を及ぼす可能性があるので、品質管理のためPRごとに両プロダクト用のCIが動いている状況でした。リポジトリが分離されていれば、これらは半分で済むため、実質2倍のコストがかかっている状況でした。

課題2. リリース・運用上の制約

組織を跨いだプロダクト間でデプロイサイクルが同じであることで、リリースや運用上で制約が発生していました。

例えば、リポジトリXで開発中のコードをstg環境で確認したい場合、リリースブランチとは異なるブランチ上で確認が必要なため、リポジトリXに関わるエンジニア間で調整が必要でした。Slackワークフローで予約の仕組みを作るなどして緩和していましたが、人数が増えるにあたって利用予約がコンフリクトすることがしばしばありました。この場合、組織を跨いで調整が必要になり、やや調整コストが高まっていました。

また、リポジトリXに含まれるコードがデプロイされ、本番環境で監視やエラートラッキングシステムがエラー検知した場合や、ユーザーからの問い合わせがあった場合に、問題の切り分けが難しいという問題がありました。さらに修正を行うにしても、両方のプロダクトでリリース調整を行う必要があり、関係ないプロダクトでもその日のリリースを延期せざるを得ないということがしばしば発生していました。

加えて、ライブラリのアップデート時も2プロダクトに対して影響範囲を調査する必要があり、確認コスト・修正コストが増大し、セキュリティを保つ困難さも発生していました。

課題3. 組織・マネジメント負荷の増大

2組織を跨いで1つのリポジトリを共有していたため、組織・マネジメント上の負荷が増大していました。

例えば、フレームワークや言語のアップデートなど全体に影響を及ぼすタスクを行う場合、組織的に別なため、どちらの組織にも共有が必要になります。これは調整コストの増大を招きました。
共有コードにおけるオーナーシップをどちらが持つかも悩ましい問題でした。プロダクト間の一部のシニアエンジニア間で連携を取りつつコードの責任を共有していましたが、その調整コストも高い状況でした。

2プロダクト間で、フェーズの違いがあったことも難しい問題でした。一例として、ある時期に片方のプロダクトは安定した品質を重視する一方、もう片方のプロダクトは新機能をスピーディーにリリースしてより成長することを志向する、のようにプロダクトのフェーズが違うことがありました。それぞれのフェーズに合わせてコードベースを最適化したくても、お互いが足を引っ張ってしまうこともありました。

加えて、両方のプロジェクトのことを把握して案件を推進するリーダークラスのエンジニアの負荷が高く、属人化が進んでいました。どうしても考慮漏れしてしまい、開発フェーズの後半で検知して手戻りする場面も。
リーダーだけでなく、プロダクトに参画したばかりのエンジニアも、両プロダクトの複雑なコードを知る必要があり、キャッチアップコストが高いため戦力化に時間がかかっていました。

リポジトリ分離の決断

...という感じで色々な課題が顕在化していました。ここには書ききれない課題も多く横たわっていました。

コンウェイの法則などでも言及されているように、組織構造とシステム構造には一定の相関関係があるとされています。2つの組織・プロダクトの多人数のエンジニアで1つのリポジトリXを共有するという状況は、組織構造とシステム構造のミスマッチです。

https://en.wikipedia.org/wiki/Conway's_law

とはいえ共有リポジトリが一概に悪いわけではなく、少人数・初期フェーズにおいては認知負荷と調整コストを最小化し、開発速度を最大化する選択として有効でした。しかし多人数で安定したプロダクト運用も重要になってくるフェーズにおいて、徐々に足かせになっていったのです。

このフェーズにおける共有リポジトリにより、端的に言うと「認知負荷」が増大していたと言えます。認知負荷の増大により、開発者体験が悪化し、開発生産性が低下している、という状態になっていました。これによりさまざまな問題が引き起こされていました。

これを解消するには、この段階でリポジトリを分離するのが最適であると判断しました。
分離時点で、異動などにより両プロダクトに精通しているエンジニアの割合も徐々に減ってきており、ここで分離できないと、もう知見のあるエンジニアがいなくなってチャンスがなくなる、という危機感もありました。

(補足)共有DBにおける課題

ここが悩ましいところだったのですが、リポジトリが共有されていた背景には、2プロダクト間共有DBの課題もありました。リポジトリX内の一部のRailsのmodelファイルがDBに依存していたため、リポジトリを分離するが共有DBは一部保持することになります。分離後に共有DBに依存するファイルを変更すると、お互いに意図せず影響を与えることになります。これまではコードベースを共有していたので、それぞれの共有DB関連ファイルが独自進化することもなかったのですが、今後は発生する可能性を考えてプロダクト間で調整が必要になります。

とはいえ、以下の記事で言及されている通り、共有DB問題も以前に比べると多くのものが解消されてきています。

https://xtech.nikkei.com/atcl/nxt/column/18/00001/11142/

リポジトリ分離検討時に改めて2プロダクトの数百テーブルのアクセス状況をDatadogなどを用いて調査したところ、共有テーブルは変更頻度の低い、いくつかのテーブル群だけであったことと、これまでの長年の運用知見から、これらで問題が起きる可能性は低いと判断しました。

できれば共有DBを完全に撤退してからリポジトリ分離を行いたかったのですが、様々な事情を総合的に判断した結果、先にリポジトリを分離した方が現状の課題解消に寄与すると判断し、ステークホルダーを説得して、一部共有DBを残した状態での移行を決断しました。

分離の方法

まず、分離の方針として、お互いのリポジトリを独立させたかったため、親子関係が生まれるFork機能は採用しない方針としました。単に新しいリポジトリを切ってpushする方針で、分離を実現させるまでの様々な課題を解消していきました。

共有DBに関連するファイルについて

先述の通り共有DBが一部残った状態の分離だったため、modelファイルをどうするかが課題でした。
理想的にはPackwerkなどでパッケージ化したかったですが、パッケージ化するコストが大きく断念しました。

https://github.com/Shopify/packwerk

ここで共有DB中のテーブルには以下の特徴がありました。

  • DBスキーマの変更は新規にテーブル作ることがほとんど、この場合はテーブルはプロダクト間で共有されない
    • 共有されているテーブルを修正することはめったにない
  • 前述の通り、数百ある共有テーブルを全て洗い出したところ、そこまで総量は多くない

コードが乖離するリスクはありますが、現状でも共有DBに対するモデルの変更には十分注意を払って開発しているため、追加の認知負荷もそれほど大きくなく、それよりも共有リポジトリによる課題解消のリターンの方が大きいと判断しました。
緩和策として、共有しているテーブルの調査レポートを各エンジニアに共有しつつ、特に注意が必要なファイルの変更についてはDanger gemを用いて、2プロダクトのリードエンジニアを自動でレビュワーアサインを行うようにしました。

https://github.com/danger/danger

加えて、共有DBのうち簡単に分離可能なものは、このタイミングで先に分離するようにし、監視対象のファイルを最小化するようにしました。

共有DBマイグレーション用のスキーマファイルについて

共有DBのスキーマファイルもリポジトリXで管理されていました。分離後はこれらをなるべく共有しつつも同期したいという要求がありました。そのためGithub Actionのrepository_dispatchイベントを活用して以下のような仕組みを作り同期しました。(仕組みを考案し作成してくれた同僚のエンジニアに感謝!)

https://docs.github.com/ja/actions/reference/workflows-and-actions/events-that-trigger-workflows#repository_dispatch

CI/CD について

リポジトリ分離に当たって両プロダクトのCI/CDをすべて見直しました。100近くはあったので、見直しつつ必要な変更PRを準備しました。
CircleCIのProject IDに依存する仕組みが別のリポジトリのコードにあるなど、修正が必要なコードには暗黙的なものもいくつかありました。加えてCIを通じたコンテナイメージの作成と共有、反映などのデプロイフロー全体を網羅的に修正する必要があり、難しく手間のかかる作業でしたがデプロイに詳しいSREチームの手も借りて何とか根気よく取り組みました。
なおこれらのPRはデプロイサイクルに影響を与えるため、マージタイミングは分離作業中としました。

また、リポジトリに紐づくシークレットを全て収集 or 再発行し、事前に新リポジトリに登録しました。長年の運用で所有者不明になっているものもいくつかあったので、これを機に棚卸して、不要なものを削除しています。

加えて、GitHubリポジトリとCircleCIの設定値を各リポジトリで同一にしました。
ちなみに筆者が作業した時点では、CircleCIのセットアップはリポジトリに.circleci/config.ymlをコミットしないと実施できないようなUIになっていました。分離作業前に設定を同期し、レビューしておきたかったのでこれでは困ります。しかしここで裏技を発見しました!(笑)。
実はリポジトリがOrganizationで作成された段階で設定画面は作られているため、リポジトリ名からURLを直打ちすることで、コミット前にセットアップできます。この裏技を活用して事前に設定を同一化して各ステークホルダーにレビューをしてもらいました。
なお、見落としがちですがGitHubリポジトリのラベルについても移行が必要かどうかを確認しておきましょう。CIの条件でラベルを使用するものがあった場合、移行漏れがあると動作しなくなってしまいます。

各開発者への周知

リポジトリ分離に当たって、デプロイフローなど運用が変わるため、新たな運用ルールを定義して周知しました。英語話者も含まれているため、英語で記載して各エンジニアに全体周知をしました。必要に応じて英語で軽く口頭説明も加えています。

今回の手順ではブランチは移行されますが、PRやIssueやProjectなどのリポジトリ固有の情報は基本的に移行されません。幸いにもIssueやProjectはそこまで使われていない状態だったので、PRはブランチから新規作成するよう依頼しつつ、その他については必要に応じて各自で移行してもらうことで合意しました。

最後に各エンジニアからの質問に答えつつ、2組織2プロダクトの各ステークホルダーに対して丁寧に説明をして回りました。いわゆる根回しというやつですね。
多数のエンジニア・組織が関わるリポジトリにおける大きな変更であったため、これらをやっておいたことで分離後の移行が非常にスムーズに進んだように思います。

分離手順について

分離手順については非常に頭を悩ませました。
前例があまりない作業であり、似たような他社・社内事例はあるが、自分たちの規模にフィットするのかが不明瞭な状況でした。色々と調べたり、同僚エンジニアとディスカッションした結果、git clone --mirror, git push --mirrorを使うことにしました。これはGitHub公式にも記載のある手順です。
リポジトリ分離するまで存在も知らないし、全く使ったことないコマンドだったので、手元で適当なリポジトリに対して色々と操作をして理解を深めました。

https://docs.github.com/ja/repositories/creating-and-managing-repositories/duplicating-a-repository

git push --mirror におけるGitHub上の制約

デフォルトではgit push --mirrorをすると、PRの情報までpushしようとします。
しかしGitHubは外部からのPRの情報を受け取れないようで、以下のようなエラーになります。(おそらく他のVCSなら受け取れたりするのかもしれません)

! [remote rejected] refs/pull/1/head -> refs/pull/1/head (deny updating a hidden ref)

そのため、事前にgit show-ref | cut -d' ' -f2 | grep 'refs/pull' | xargs -r -L1 git update-ref -dというワンライナーを実行し、PRの情報であるrefs/pullを削除しておく必要があります。

※ちなみにgit push --mirror --dry-runもできるので、実際にコマンド実行前に、dry-runで何がpushされるかを確認しておくことをおススメします。

GitHubのプッシュサイズの制限

特に懸念があったのは、GitHubのプッシュサイズの制限です。GitHubは1回のpushで2GiBまでしか送信できません。

https://docs.github.com/ja/get-started/using-git/troubleshooting-the-2-gb-push-limit

これは以下のようなコマンドで確認しました。出力例は以下です。

$ git count-objects -vH
count: 18
size: 88.00 KiB
in-pack: xxxxxx
packs: 1
size-pack: xxx.xx MiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytes

ここでsize-pack: xxx.xx MiBの値が2GiB以下なら大丈夫です。事前に確認しておくことをおススメします。

また、事前にpushサイズを最小限にするために不要なブランチを削除しました。github.com/xxxorg/xxxrepo/branches/allのようなURLでブランチ一覧並びに、最終更新日時が分かるので、これらを参考に削除しつつ、並行して各開発者に呼び掛けて不要なブランチを削除するように依頼しました。

デフォルトブランチの変更

git push --mirrorした場合、デフォルトブランチが適当なブランチに設定されてしまいます。おそらく最初にpushしたブランチになるのかなと思います。これだとデフォルトブランチがおかしい状態でCIなどが走ってしまうので大変危険です。そのためpush前にCIを無効にしておき、デフォルトブランチを修正した後に再度有効にすることを強くお勧めします。GitHubの場合はSettings > Actions > GeneralDisable actionsで設定できます。

CircleCIの場合はProject SetupTrigger on...の設定を全て削除することで対応できます。

移行戦略

移行戦略をどうするかは非常に悩みました。なるべく進行中の開発案件に影響を与えず、いざとなったら切り戻せるようにする方針で設計しました。
また、テストするにしても後片付けを考えると結構大変なのと、実際にやってみてダメだったら戻す方がラクだったので、ある程度はぶっつけ本番を許容しました。

色々考えた結果、週末の通常業務終了後に新リポジトリに対して移行を行い、念のため土日のバッファを設けることにしました。なお、それでもうまくいかなければ仕切り直して古い方を使い続け、失敗したリポジトリの処遇は後日考える方針としました。

手順書の執筆

上述した注意点や移行戦略を盛り込んだ手順書をスプレッドシートに記載しました。事前準備手順を含めると、大体100行以上の大作に。各ステークホルダーにレビューをしてもらいつつブラッシュアップしました。

どうしても実際にやってみないとわからない手順もあったので、当日までは手順書を穴が開くほど確認しました。確認可能な手順はスモールにテストをしたり、脳内でシミュレーションをしたり。手順書中にも、ビルドしたコンテナイメージのサイズが変わっていないことを確認する項目を設けるなど、何重にもチェックポイントを設けるように入念に手順を作りこみました。

いざ分離の実施!

上記の準備を行い、決行日を決め、いざ作業開始!
週末に、その日のデプロイが終了した後、各エンジニアに共有リポジトリへのコミットを禁止し、手順書に従って分離作業を開始しました。作業は立ち合いのエンジニア数名とオンラインで会話しつつ確認しながら進めました。ちょっとしたイベント気分。

...結果、なんと無事故で終了!事前に入念に周知していたことで、他のエンジニアも移行後の手順をチームに周知したりと手伝ってくれたおかげもあり、週明けにはまるで初めから分かれていたかのように別リポジトリで運用が開始されました。移行後も特に問題は発生せず、独立したデプロイフローで開発作業が進みました。

移行作業中も想定外のことがほぼ発生せず、新リポジトリでDependabotを止めるの失念していてたくさんPR作られて焦る(笑)くらいのことしか起きませんでした。慢心しないようにしようと心がけつつも、この時ばかりは流石に自画自賛しました笑。

無事完了のアナウンスも終了!

SaaSを提供する1エンジニアとして技術的負債についての考え方

50名以上かつ複数の組織で共有するリポジトリXは、まさに技術的負債と呼べるものでした。

我々のサービスはToB向けのバックオフィスに対して提供しているため、何か問題が起きてもすぐにサービス終了することはできません。開発と運用は分離しておらず、作って終わりでもなく「作ってからがスタート」です。技術的負債はこれをあらゆる面から阻害します。
使ってくれているユーザーに対して中長期的に持続的に価値提供を行うためにも、技術的負債については定期的に課題発見して解決していくことがサービス提供者の責任と考えています。

加えて、プロダクトがビジネスを存続させ、結果として所属している福岡開発拠点を存続させているという事実も見過ごせません。ありがたいことにプロダクトが成長しているからこそ、歴史が積み重なり、時に市場や組織の環境に適応できずに技術的課題が生まれてくるものです。プロダクトにご飯を食べさせてもらっている身としては清算すべきものを清算し、また「自分たちの生きる場所を自分たちで守るため」にも、定期的な技術負債の解消に取り組んでいきたいと思っています。

リポジトリ分離による大きな技術的負債解消を通じて

リポジトリ分離は長年の歴史による組織的・技術的なステークホルダーも多く、技術的にも考慮すべきことの多い、総合格闘技のようなタスクでした。
私のこれまでのエンジニア人生の中でも、間違いなく最も難易度の高いタスクだったと思います。
やっている最中は頭の中が整理できない時期や、プレッシャーに押しつぶされそうな時期もありましたが、様々な方のご協力で何とか課題解消を主導し、そして実現することができました。

これまで数年にわたり様々な問題を発生させていた「大きな山」を自らの手で崩せたのはエンジニア冥利に尽きます。これだけの多くの人を巻き込んだ大きな変更にも関わらず、分離後も問題なく動いているのは、自らのエンジニアとしての成長を感じました。

これによって今後色んな挑戦をしやすくなったと思います。現在は分離に伴い可能になった不要コードの削除に取り組んでいますが、これが完全に実現出来たらCIのコストが格段に下がりますし、不要なコンテキストが削減されることでAI活用もよりしやすくなるでしょう。新機能の開発や運用もこれまで以上にやりやすくなっていくと思います。
加えて、これまで同じコードベースで活動してきた仲間が多くいるため、それぞれのプロダクトで技術的挑戦を行い、そのナレッジを横展開する、ということもできます。すでに不要コードの削除について、知見の共有がなされています。これからが楽しみです。

私個人としては、今後も他の技術負債の解消を進めつつ、解消した環境を活かして機能開発にもコミットしていきたいです。やはりエンジニアとしては、自分の手で直接作ったものを誰かに使ってもらい、そして喜んでもらいたいですからね~。
なので、技術的負債を解消するだけでなく、解消したからこそ取り組める価値提供にまで繋げていけると最高です。
...という想いで、これからも福岡の地で頑張っていければと思います!

Money Forward Developers

Discussion