こんにちは。Sansan iOS アプリエンジニアの中川です。
前回に引き続き、モバイルアプリの CI / CD に関するテーマを取り扱いたいと思います。
今回は CI サービスとして Bitrise を利用されている方を対象に Tips を共有します。
- 課題感
- 解決策の模索
- 改善した結果
課題感
下の Bitrise のビルド一覧を見てもらえれば、一目瞭然ですが、iOS アプリで 20 分を超えるビルドがちらほら出てくるようになっていました。
Sansan では Bitrise の ELITE プランを利用していますが、 iOS アプリと Android アプリのどちらのビルドも同じビルドキューに入るので、 3 並列でビルドが実施できるとはいえ、ビルド時間が長いとビルドが滞留してしまう可能性も上がってしまいます。
片や Android アプリは長くても iOS アプリの半分の 10 分でビルドが完了していますs。
iOS アプリのビルドが滞留して、 Android アプリエンジニアから苦情が来てしまっては申し訳が立たないので、ビルド時間の短縮に挑戦しようと考えました。
解決策の模索
では、短縮できそうな部分はどこでしょうか?
Bitrise のビルドログには最後にどのステップにどのくらいの時間がかかったのか出力されているので、その内容を確認しましょう。
bitrise summary を見てみると、明らかに遅い 3 つのステップが見つかります。
iOS アプリをビルドするための環境セットアップを行っている Bootstrap
ステップと iOS アプリのテストを実行している xcode-test
ステップと iOS アプリの ipa を作成している xcode-archive
ステップです。
このうち、 xcode-test
ステップと xcode-archive
ステップは Bitrise から提供しているものを利用しているため、手を入れられそうな箇所が少なさそうなのと、純粋に Xcode での iOS アプリのビルドを早くしたほうが効果的と判断しました。
Bootstrap
ステップを深堀りをしてみましょう。
下記のビルドログは実際に出力されるものから途中経過のログといった説明に不要なログを取り除いたものです。
このログを元に改善ポイントを探してみます。
+------------------------------------------------------------------------------+ [REDACTED] (0) Bootstrap [REDACTED] +------------------------------------------------------------------------------+ [REDACTED] id: script [REDACTED] [REDACTED] version: 1.1.5 [REDACTED] [REDACTED] collection: https://github.com/bitrise-io/bitrise-steplib.git [REDACTED] [REDACTED] toolkit: bash [REDACTED] [REDACTED] time: 2019-07-09T06:06:00Z [REDACTED] +------------------------------------------------------------------------------+ [REDACTED] [REDACTED] + brew update + brew reinstall rbenv + echo 'eval "$(rbenv init -)"' + echo 'export PATH="$HOME/.rbenv/bin:$PATH"' + source /Users/vagrant/.bash_profile + curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor + bash + rbenv install 2.6.3 + sh ./Scripts/bootstrap.sh Generating Configuration Settings File... Generated Configuration Settings File :) Installing dependencies... Warning: the running version of Bundler (1.17.2) is older than the version that created the lockfile (1.17.3). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`. Fetching gem metadata from https://rubygems.org/........ Fetching CFPropertyList 3.0.0 Installing CFPropertyList 3.0.0 Fetching ZenTest 4.11.2 Installing ZenTest 4.11.2 Fetching RubyInline 3.12.4 Installing RubyInline 3.12.4 Fetching concurrent-ruby 1.1.5 Installing concurrent-ruby 1.1.5 Fetching i18n 0.9.5 Installing i18n 0.9.5 Fetching minitest 5.11.3 Installing minitest 5.11.3 Fetching thread_safe 0.3.6 Installing thread_safe 0.3.6 Fetching tzinfo 1.2.5 Installing tzinfo 1.2.5 Fetching activesupport 4.2.11.1 Installing activesupport 4.2.11.1 Fetching atomos 0.1.3 Installing atomos 0.1.3 Using bundler 1.17.2 Fetching claide 1.0.2 Installing claide 1.0.2 Fetching fuzzy_match 2.0.4 Installing fuzzy_match 2.0.4 Fetching nap 1.1.0 Installing nap 1.1.0 Fetching cocoapods-core 1.6.1 Installing cocoapods-core 1.6.1 Fetching cocoapods-deintegrate 1.0.4 Installing cocoapods-deintegrate 1.0.4 Fetching cocoapods-downloader 1.2.2 Installing cocoapods-downloader 1.2.2 Fetching cocoapods-plugins 1.0.0 Installing cocoapods-plugins 1.0.0 Fetching cocoapods-search 1.0.0 Installing cocoapods-search 1.0.0 Fetching cocoapods-stats 1.1.0 Installing cocoapods-stats 1.1.0 Fetching netrc 0.11.0 Installing netrc 0.11.0 Fetching cocoapods-trunk 1.3.1 Installing cocoapods-trunk 1.3.1 Fetching cocoapods-try 1.1.0 Installing cocoapods-try 1.1.0 Fetching colored2 3.1.2 Installing colored2 3.1.2 Fetching escape 0.0.4 Installing escape 0.0.4 Fetching fourflusher 2.2.0 Installing fourflusher 2.2.0 Fetching gh_inspector 1.1.3 Installing gh_inspector 1.1.3 Fetching molinillo 0.6.6 Installing molinillo 0.6.6 Fetching ruby-macho 1.4.0 Installing ruby-macho 1.4.0 Fetching nanaimo 0.2.6 Installing nanaimo 0.2.6 Fetching xcodeproj 1.8.2 Installing xcodeproj 1.8.2 Fetching cocoapods 1.6.1 Installing cocoapods 1.6.1 Fetching dotenv 2.7.2 Installing dotenv 2.7.2 Fetching osx_keychain 1.0.2 Installing osx_keychain 1.0.2 Fetching cocoapods-keys 2.1.0 Installing cocoapods-keys 2.1.0 Bundle complete! 2 Gemfile dependencies, 35 gems now installed. Bundled gems are installed into `./vendor/bundler` Updating spec repo `master` $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master fetch origin --progress remote: Enumerating objects: 12067 remote: Enumerating objects: 16504, done. remote: Counting objects: 100% (16504/16504), done. remote: Compressing objects: 100% (5182/5182), done. remote: Total 28571 (delta 13188), reused 13777 (delta 10939), pack-reused 12067 Receiving objects: 100% (28571/28571), 3.14 MiB [REDACTED] 17.97 MiB/s, done. Resolving deltas: 100% (18838/18838), completed with 2777 local objects. From https://github.com/CocoaPods/Specs 52b73bb13d5..6560482ee3b master -> origin/master $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master rev-parse --abbrev-ref HEAD master $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master reset --hard origin/master Checking out files: 100% (3028/3028), done. HEAD is now at 6560482ee3b [Add] JWHelperKit 1.1.3 warning: inexact rename detection was skipped due to too many files. warning: you may want to set your diff.renameLimit variable to at least 2823 and retry the command. CocoaPods 1.7.3 is available. To update use: `gem install cocoapods` For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.7.3 Analyzing dependencies Downloading dependencies Installing Crashlytics (3.12.0) Installing DZNSegmentedControl (1.3.3) Installing Fabric (1.9.0) Installing HMSegmentedControl (1.5.3) Installing MXPagerView (0.2.1) Installing MXParallaxHeader (0.6.1) Installing MXSegmentedPager (3.3.0) Installing R.swift (5.0.3) Installing R.swift.Library (5.0.1) Installing Sourcery (0.16.1) Installing SwiftLint (0.31.0) Generating Pods project Integrating client project Sending stats Pod installation complete! There are 7 dependencies from the Podfile and 11 total pods installed. Updated git hooks. Git LFS initialized. Installed dependencies :) Initializing AppConfiguration.swift... No config file provided or it does not exist. Using command line arguments. Scanning sources... Found 0 types. Loading templates... Loaded 1 templates. Generating code... Finished. Processing time 0.011717081069946289 seconds Initialized AppConfiguration.swift :) [REDACTED] [REDACTED] +---+---------------------------------------------------------------+----------+ [REDACTED] [32;1m✓[0m [REDACTED] [32;1mBootstrap[0m [REDACTED] 347 sec [REDACTED] +---+---------------------------------------------------------------+----------+
✂️ 都度、再インストールしている rbenv
+ brew update + brew reinstall rbenv
こちら、 Swift5 と Swift4.2 で並行してビルドできるようにワークフローを作っていた際にスタックによって、 rbenv があったりなかったりしていた問題をワークフローで吸収するために実行していたコマンドです。
Sansan iOS アプリでは Swift5 対応が無事、完了し、リリースされたので、 rbenv が最初からインストールされているスタックしか利用しなくなったため、やめられそうです。
✂️ 都度、インストールしている ruby 2.6.3
rbenv install 2.6.3
Bitrise から提供されている最新のスタックを利用しても、rbenv に ruby 2.6.3 はインストールされていないので、個別にインストールしてあげる必要があります。
こちらへの解決策は次の2つが考えられそうです。
- Bitrise にインストールされているバージョンの ruby を利用する
- ruby 2.6.3 を Bitrise のキャッシュに追加して、キャッシュに存在している限りはインストールを実行しない
前者はスタックによって変わる可能性もあり、その度に開発で利用する ruby のバージョンを変更するのは運用コストが高く、ワークフローごとに柔軟にスタックを変えられるメリットを享受できなくなってしまいます。後者の場合、キャッシュに追加してしまえば、インストールコストを無視できますし、 Bitrise のキャッシュはブランチごとに保存され、ワークフローやスタックを跨いで共有できるため、キャッシュの存在期間も長そうです。
後者で改善を進めてみます。
上記ページにキャッシュの利用方法が日本語で記載されているので、参考にしつつ、ワークフローにキャッシュの機能を追加します。
1. Git Clone Repository
ステップと Bootstrap
ステップの間に Bitrise.io Cache:Pull
ステップを追加する
2. キャッシュするディレクトリを環境変数に追加する
Sansan iOS アプリでは利用する ruby のバージョンを .ruby-version
で固定化しています。
なので、以下のコマンドで指定バージョンの ruby のディレクトリを取得しつつ、環境変数へ追加することができます。
envman add --key RBENV_RUBY_VERSION_HOME --value "$(rbenv root)""/versions/""$(< .ruby-version)"
上記のスクリプトは Bootstrap 直後に実行します。
3. ワークフローの最後のステップに Bitrise.io Cache:Push
ステップを追加する
前段階で環境変数に追加したキャッシュしたい ruby のディレクトリを Cache paths に指定します。
4. キャッシュが存在している限り、 Bootstrap
ステップで ruby のインストールを行わない
利用しているスタックに ruby 2.6.3 がインストールされていないという前提を利用して、パスチェックで存在していれば、キャッシュからダウンロードできている or スタックに ruby 2.6.3 がインストールされていると判断し、何もしないヌルコマンドを実行しています。そうでなければ、指定バージョンの ruby のインストールを実行します。
rubyVersionBinaryPath="$(rbenv root)""/versions/""$(< .ruby-version)""/bin/ruby" if [ -e "$rubyVersionBinaryPath" ]; then : else brew update && brew upgrade ruby-build || true # すでにインストール済みだったとしても異常終了にしない rbenv install "$(< .ruby-version)" fi
完成 🎉
キャッシュが存在している場合のビルドログは以下のようになります。
++ rbenv root + rubyVersionBinaryPath=/Users/vagrant/.rbenv/versions/2.6.3/bin/ruby + '[' -e /Users/vagrant/.rbenv/versions/2.6.3/bin/ruby ']' + :
✂️ 都度、インストールしている gem
Fetching CFPropertyList 3.0.0 Installing CFPropertyList 3.0.0 … (中略) Bundle complete! 2 Gemfile dependencies, 35 gems now installed. Bundled gems are installed into `./vendor/bundler`
Sansan iOS アプリでは CocoaPods を利用しており、その CocoaPods が依存している gem が大量にインストールされています。
こちらも頻繁に変わるものでも有りませんし、キャッシュに追加しても問題なさそうです 🤗
1. Bitrise.io Cache:Push
ステップに Gemfile.lock
に変更があったら、 gem インストール先ディレクトリをキャッシュに追加するように設定変更する
一つ前の rbenv でインストールしている ruby 2.6.3 のキャッシュへの追加でワークフローにはキャッシュの仕組みがもたらされているので、 Bitrise.io Cache:Push
の設定変更だけで済みます 🆒
Cache paths に ./vendor/bundler -> ./Gemfile.lock
を追記します。
./vendor/bundler
は Sansan iOS アプリで指定している gem のインストール先ですので、環境に合わせて、変更してください 🙏
設定内容の意図ですが、 Cache paths にディレクトリを指定するとキャッシュの変更があったかどうかを判定するのにすべてのファイルに対してチェックを行ってしまいますが、 ->
の後にファイルを指定することでそのファイルのチェックのみに抑制することができます。
gem であれば、 依存性の変更は Gemfile.lock
に現れるので、そのファイルを監視すれば OK ということになります。
完成 🎉
キャッシュが存在している場合のビルドログは以下のようになります。
gem の取得やインストールが発生せず、インストール済みの gem を使用するようになっています。
Using CFPropertyList 3.0.0 … (中略) Bundle complete! 2 Gemfile dependencies, 35 gems now installed. Bundled gems are installed into `./vendor/bundler`
✂️ 都度、インストールしている pod
Installing Crashlytics (3.12.0) Installing DZNSegmentedControl (1.3.3) Installing Fabric (1.9.0) Installing HMSegmentedControl (1.5.3) Installing MXPagerView (0.2.1) Installing MXParallaxHeader (0.6.1) Installing MXSegmentedPager (3.3.0) Installing R.swift (5.0.3) Installing R.swift.Library (5.0.1) Installing Sourcery (0.16.1) Installing SwiftLint (0.31.0)
gem と同じく pod も大量にインストールされていますね。
こちらもキャッシュに追加してしまいましょう 💪
1. Bitrise.io Cache:Push
ステップに Podfile.lock
に変更があったら、 pod インストール先ディレクトリをキャッシュに追加するように設定変更する
Cache paths に ./Pods -> ./Podfile.lock
を追記します。
完成 🎉
キャッシュが存在している場合のビルドログは以下のようになります。
pod のインストールが発生せず、インストール済みの pod を使用するようになっています。
Using Crashlytics (3.12.0) Using DZNSegmentedControl (1.3.3) Using Fabric (1.9.0) Using HMSegmentedControl (1.5.3) Using MXPagerView (0.2.1) Using MXParallaxHeader (0.6.1) Using MXSegmentedPager (3.3.0) Using R.swift (5.0.3) Using R.swift.Library (5.0.1) Using Sourcery (0.16.1) Using SwiftLint (0.31.0)
改善した結果
以下が改善後の bitrise summary です。
改善前は 347 sec かかっていた Bootstrap
ステップはキャッシュが存在していれば、 50 sec で完了するようになりました。
追加で Bitrise.io Cache:Pull
ステップとBitrise.io Cache:Push
ステップが増えていますが合わせて、 26 sec の増加なので、Bootstrap
ステップの増減率は ((50+26)/347-1)*100 = -78.097982709% となるので、約 78 % 高速化したことになります。
全体で見ると、他の遅い xcode-test
ステップと xcode-archive
ステップの処理時間のばらつきによって増減率が小さくなってしまっていますが、 25 %以上の高速化を得たことになります。今後、取り組む予定の Xcode での iOS アプリのビルドの高速化に期待したいです 🤗
以下は 20 分を超えるビルドが無くなり、 15 分くらいで収まるようになった iOS アプリのビルド一覧です。
ほっと一息つけそうです ☕️