はじめに
こんにちは!モバイルファクトリー 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へ移行するにあたってヒントになれば幸いです。