Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

巨大なリポジトリのJenkinsからCircleCIへの移行においてshallow cloneとsparse checkoutで前処理を高速化する

はじめに

こんにちは!モバイルファクトリー Advent Calendar 2019 24日目担当の@PikkamanVです。 今回は運用中のプロダクトのCIをJenkinsからCircleCIへ移行するにあたり特にハードルが高かった点の解決方法を紹介します。 オンプレのJenkinsサーバでフルテストを回すのが前提となっていたリポジトリをCircleCIで扱うにあたり、shallow cloneとsparse checkoutを活用することでテストの前準備の高速化を図りました。

背景

今回扱うリポジトリは物理サーバ上に開発環境を用意することが前提になっており、テストも同様に単一の物理サーバ上で実行されていました。長期間の運用により総コミット数は90,000回を超え、リポジトリのサイズは30 GB弱となっていましたが、社内の強力なサーバ上でJenkinsを利用することで高速にテストを実行できていました。サーバ上に常にローカルリポジトリが存在するので、最新のコミットとの差分だけをリモートリポジトリから取得すればよかったためです。

しかし、CircleCIは通常の設定のままcheckoutするとまっさらな状態からリポジトリをコピーするので、歴史ある巨大なリポジトリではテストの前処理にサイズに比例して時間がかかる問題がありました。そこで、CircleCIにおいてgitのshallow cloneとsparse checkoutを活用することで高速にリポジトリをコピーし、テストの実行開始を早めることを目指しました。

shallow clone

今回のリポジトリは約5年間の運用を経て .git のサイズは約18GBにまで大きくなっていました。リポジトリ全体のサイズが約30GBなので、その占める割合が分かります。そのため、通常のgit clone を行うと大変時間がかかりました。しかし、想定しているCI環境においては対象のブランチの最新コミットに対してのみテストが実行できれば十分です。そこで、shallow cloneを使うことよりcheckoutの時間を短縮しました。

command: |
      git init
      git remote add origin "$CIRCLE_REPOSITORY_URL"
      git fetch --depth=2 origin "$CIRCLE_BRANCH"
      git fetch --depth=1 origin HEAD:refs/remotes/origin/HEAD
      git checkout "$CIRCLE_BRANCH"
      git reset --hard "$CIRCLE_SHA1"

sparse checkout

また歴史的経緯からリポジトリには画像などのサイズの大きいアセットが含まれており、約8.5GBを占めていました。これらをリモートリポジトリからコピーするには時間がかかります。よってsparse checkoutによって、サイズの大きいアセットはclone時に除外することを考えました。sparse checkoutは以下のようにcheckoutするディレクトリを .git/info/sparse-checkout に指定することで設定可能です。あるディレクトリAに含まれる特定のディレクトリBだけを除きたい場合は、ディレクトリA!ディレクトリA/ディレクトリB を両方指定することで実現できます。

command: |
      git config core.sparsecheckout true
      echo 'ディレクトリ1
      ディレクトリ2
      ...
      !除外するディレクトリ1
      !除外するディレクトリ2
      ...
      ' >> .git/info/sparse-checkout

しかし、除外したアセットに対してもそのファイルの存在を確認するテストなどが書かれていました。ですがサイズの大きなアセットこそが前処理の高速化においてボトルネックになっていたため、sparse checkoutはぜひ導入したい機能でした。ここでテストの中身に注目すると、テストされていた機能はめったに変更が入らず、また他の機能への影響も少ないものであることが分かりました。そこで、日中の開発中は対象のテストを除外して高速化を行う一方、1日1回すべてのテストを実行し、テスト実行時間と網羅性のバランスを取ることにしました。 まずは通常トリガされる build workflowの他に、1日1回実行される nightly workflowを用意します。nightly workflowでは、sparse checkoutによるアセットの除外を行わない checkout_code_full を設定しています。(上の例で !除外するディレクトリ1 などを削除するイメージです)

workflows:
  version: 2
  build:
    jobs:
      - checkout_code
      - frontend:
          requires:
            - checkout_code
      - backend:
          requires:
            - checkout_code
  nightly:
    triggers:
      - schedule:
          # UTC表記 平日の午前7時20分から実行
          cron: "20 22 * * 0-4"
          filters:
            branches:
              only:
                - develop
                - /base-.*/
    jobs:
      - checkout_code_full
      - frontend:
          requires:
            - checkout_code_full
      - backend_full:
          requires:
            - checkout_code_full

さらにアセット関連のテストも実行する backend_full ジョブにおいて環境変数 BUILD_TIME を設定します。

  backend_full:
    docker:
      - image: ...
        environment:
          ...
          BUILD_TIME: nightly
          ...

そして対象のテストの始めに環境変数を見てテストをスキップするかどうかの処理を追加しました。

use Test::More;
...

if($ENV{BUILD_TIME} ne 'nightly') {
    plan skip_all => 'No images';
}

subtest 'subtest_name' => sub {
    ...
}

done_testing;

こうして1日1回だけ実行されるnightlyジョブでアセット関連のテストを行うことができるようになりました。

まとめ

以上のように、shallow cloneとsparse checkoutを組み合わせた上にCircleCIのworkflowを使い分けることで

  • 日中の開発中はサイズの大きいアセットを除いてテストすることで実行時間を短縮し、開発のストレスを減少させる
  • 1日1回すべてのアセットを含んだテストを実行することでテストの網羅性を確保し、重大な開発の手戻りを防止する

の両方を実現することができました。巨大なリポジトリをCircleCIへ移行するにあたってヒントになれば幸いです。