ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Bitrise の bundler 2.x アップデート問題について私がどう考えて調査・対応したか。あるいは手軽さの影で。

どうも、休職中の @tobi462 です。

毎日仕事もせずに何をやっているかと言えば・・・そうですね、個人的推論に則った改善活動といったところでしょうか。

Bitrise の Xcode 11.3.x ビルドスタックの bundler が 1.x → 2.x に更新された

先週くらいに iOS 界隈を騒がせた小さな事件といえば、Bitrise の Xcode 11.3.x のビルドスタックのマイナーアップデートにおいて、bundler のバージョンが 1.x から 2.x に更新されたことでしょう。

Xcode 11.3 から Xcode 11.3.1 へのマイナーアップデートに関わらず、長らく bundler 1.x を利用し続けていた Bitrise が、このタイミングで bundler 2.x 系に更新したことは、Bitrise を利用している iOSアプリエンジニアの多くを混乱させたことでしょう。

ビルドエラーとの遭遇

私は数日前から、友人の iOSアプリ開発環境の改善に取り組んでおり、Bitrise を用いた CI 環境の構築も最低限終えたところでした。その日の朝、私は軽い改善を行おうとしていました。

ローカルでブランチを切って push したところ、以下のようなエラーに遭遇しました(コミットもせずに即 push したのは妥当な理由があるのですが、ここでは割愛します)。

f:id:yu_dotnet2004:20200124193535p:plain

どうやら ios-auto-provision のステップでエラーが発生しているようです。

エラー内容は次のようになっています。

$ bundle install
/Users/vagrant/.rbenv/versions/2.6.5/lib/ruby/site_ruby/2.6.0/rubygems.rb:275:in `find_spec_for_exe’: Could not find ‘bundler’ (1.17.3) required by your /var/folders/6q/wgy6jtp12w5gzgm9lzcglpqw0000gn/T/bitrise069555720/step_src/Gemfile.lock. (Gem::GemNotFoundException)
To update to the latest version installed on your system, run `bundle update —bundler`.
To install the missing version, run `gem install bundler:1.17.3`
    from /Users/vagrant/.rbenv/versions/2.6.5/lib/ruby/site_ruby/2.6.0/rubygems.rb:294:in `activate_bin_path’
    from /Users/vagrant/.rbenv/versions/2.6.5/bin/bundle:23:in `<main>’
bundle install failed

Could not find ‘bundler’ (1.17.3) required by your /var/folders/xxx/Gemfile.lock.

私が構築したワークフローでは、bundler 2.x をインストールして利用するようにしていた為、このエラーログを見て何が起きているのか大体の察しがつきました。

ブランチはmasterから切って、何もコミットしていない状態で push したものなので、前日は問題なくパスしていたコミットSHAの内容が突然失敗したことになります。こうなると、こちらの問題ではなくビルドスタック側に変更があったのは間違いありません。

ビルドVMのマシン構成を確認する

Bitrise の Stack タブから『More information about this Stack』のリンクをクリックし、ビルドVMのマシン構成のシステムレポートを見てみると、案の定 bundler のバージョンが 2.x に更新されていました。

f:id:yu_dotnet2004:20200124193615p:plain

f:id:yu_dotnet2004:20200124193629p:plain

次に bundler が 2.x に更新されたのは、Xcode 11.3.x のビルドスタックのみなのか、Xcode 11.2.x 以前のビルドスタックも更新されているのか確認しました。

確認してみると、どうやら Xcode 11.2.x のビルドスタックの bundler は更新されておらず 1.x 系のままだったので、おそらく Xcode 11.3.x のビルドスタックのみだろうと予想をつけました。

ペンギン村Slackに速報を打つ

この時点で私は、Xcode 11.3.x のビルドスタックを使用し、かつ bundler を利用している場合(とくに 2.x 系を利用している場合は確実に)、私の環境に限らず問題が起こることを確信しました。

ペンギン村 Slack には #ci チャンネルが存在したため、そこで速報を打ち、同じ問題が発生する可能性があることを周知しました。

f:id:yu_dotnet2004:20200124193641p:plain

これは同様の事象に遭遇した際のトラブルシューティング時間をへらす目的での情報共有です。

状況を分析する

次に、私はこの状況に対して一歩立ち止まって考えてみました。

Q. Bitrise 側が Xcode 11.3.x のビルドスタックで bundler を 2.x 系に更新したのは意図的なものなのか?

意図的であると仮定すると、不自然なことがあると気づきます。それは公式のステップとして提供されている ios-auto-provision が bundler 2.x 系に対応できていなかったことです(なお、翌日に 2.x に対応されたバージョンがリリースされました)。

これはいわゆる『片手落ち』の状態で、ビルドスタックを bundler 2.x 系に更新したのが意図的であっても、Bitrise 社内で適切な情報共有ができていなかった事を意味します。この時点で、私は Bitrise 側が意図せずに bundler 2.x に更新してしまったのであろうとほぼ判断しました。

さらに考えてみます。

Q. Xcode 11.3 → Xcode 11.3.1 のマイナーアップデートにおいて、bundler 2.x へのアップデートという大きな変更をするだろうか?

少なくとも私であればやりません。

たとえ公式のステップが bundler 2.x に対応できていたとしても、非公式のステップが bundler 2.x に対応できていない場合は同様にビルドエラーとなり、混乱することは目に見えています。

これほど影響の大きい変更をする場合は、事前にユーザに一報し、それなりに準備期間を設けるのが自然でしょう。

この時点で私は、Bitrise 側が意図せずに bundler 2.x に更新したのだと確信しました(なお、これが真実であったかどうかは分かりません)。

次にこう考えてみました。

Q. Bitrise はこの『片手落ち』の問題に対して、どう対処するだろうか?

この問題に対して Bitrise が取りうる手段として次の2つが思い浮かびました。

  1. Xcode 11.3.x のビルドスタックの bundler を 1.x 系に戻す
  2. 公式で提供しているステップを bundler 2.x 系に対応する

もし、bundler 2.x に更新したのがミスだったとしたら前者の方法が素直に見えます。

しかし、最新のXcodeに対応したビルドスタックをいち早く提供することが売りの一つである Bitrise としては、あまりこの方法は取りたくないだろうと考えました。なぜなら、これは Bitrise 側の大きなミスを認めることになり、Bitrise の信用を落とすのには十分だからです。

なので私は「公式ステップの bundler 2.x への対応がちょっと遅れちゃった。ごめんね!」という体裁が取れる、後者の手段を採用する可能性が高いと予想しました。

Xcode 11.2.x 系のビルドスタックに戻す判断をする

そうすると、現時点で急いで対処するよりも、Bitrise 側の動きを待ってから対応したほうが費用対効果が高いと判断しました。Bitrise 側はこの問題を把握するのにそれほど時間を要しないでしょうし、明日には問題を修正してリリースするでしょう。

Xcode 11.2.x 系のビルドスタックに戻してビルドしてみたところビルドがパスしたため、私はこれを『一時的な対処』とし、あとは Bitrise 側の動向を待つことにしました。

これはプライベートなアプリ開発だったので、2日くらいちょっと古い Xcode バージョンで CI を回していたところで大きな問題は発生しません。

私は、仮に業務で開発しているアプリであっても、古い Xcode バージョンでビルド可能であるなら、それで一時的な対処をしたほうが費用対効果が高いと思い、ペンギン村 Slack に個人的な考えとして見解を共有しました。

f:id:yu_dotnet2004:20200124194919p:plain

この問題への対応優先度はプロジェクトや状況によって異なるのは当然ですが、この情報が役に立つこともあるだろうという判断からです。

動向を監視できるワークフローを組む

次に、Bitrise 側が問題に対処した場合に、すぐに検知できる仕組みが欲しいと考えました。

まぁ別に急いでいるわけではないですが、簡単に自動で監視できる仕組みが用意できれば、それに越したことはありません。

この時点では、私の友人のiOSアプリの Bitrise 環境ではデイリーで実行する、いわゆる『Nightly』なビルドワークフローは用意していない状況でした。

そこで、私は Xcode 11.3.x のビルドスタックで、全ステップを『Always Latest』にして常に最新が利用されるようにしたワークフローを組み立て、それを毎朝4:00に実行されるようにしました。

f:id:yu_dotnet2004:20200124194953p:plain

こうしておけば、Bitrise が『ビルドスタック側』か『公式ステップ側』のどちらを修正して対処したとしても、対応が完了した時点で、グリーンのビルド結果通知が得られるはずです。

bundler 1.x 系に戻った?

ここで予想以上に早い変化が起きたことを、CIのビルド結果から知ることになります。

Xcode 11.3.x のビルドスタックによるビルドで、bundler 1.x でビルドされているログが確認できたのです。

最初は急いでビルドスタックを切り戻したのかなと思いました。

対応にしてはちょっと早すぎる気がしましたし、私は前述の分析から Bitrise がこちらの手段を取るのはちょっと意外だと感じましたが、まぁそういう『誠意的』な判断をしたのだろうと予想しました。

が・・・。

量子力学的ビルド(ビルドVMガチャ)の発生

何回かビルドを走らせてるうちに、Xcode 11.3.x のビルドスタックにおいて、bundler のバージョンとして 2.x 系が利用されるケースと、1.x 系が利用されるケースの両方が存在することをビルドログから知ります。

あぁ、これはビルドVM全体への反映が済んでおらず、いわゆる『ビルドマシンガチャ』の状態になっていると判断しました。

私はこのあたりを詳しくはありませんが、急いで bundler 1.x 系に切り戻したところで、その変更が即座にビルドVMクラスタ全体に反映されるとは思いませんでした。

そうすると、現状は bundler 2.x 系と 1.x 系が重ね合わせの状態、すなわち我々が観測者だとすると『量子力学的ビルド』が発生しているのだと判断できました。

はい、量子力学的ってのはただ言いたかっただけです(たぶん流行らない)。

最終判断をする

すでに現状でエネルギーを割いて対処するのは費用対効果が高くないと判断していましたが、『量子力学的ビルド』(まだ言う)が発生していることで、ますます今日はやる価値がないと考えを強めました。

『量子力学的ビルド』が改善しないうちは、もはや無駄な試行錯誤を繰り返すだけだと考えたためです。(あるいは 2.x または 1.x のどちらが入っていてもパスするワークフローを組むかのどちらかです)

そして翌日

朝起きて Slack を確認してみたら、前述した『Xcode 11.3.x + Always Latest』なワークフローのビルド通知がグリーンになっていました。

そこで確認してみると、ios-auto-provision の最新バージョンがリリースされており、内容を確認すると bundler 2.x に対応した旨が書かれていました。

f:id:yu_dotnet2004:20200124194235p:plain

これで事態は一見落着かと思いきや、実は『量子力学的ビルド』はまだ続いていることに気づきます。

私は考えるのをやめました。

とりあえず Xcode 11.3.x のビルドスタックはまだ不安定と判断し、事態が収束したと判断できるまでは Xcode 11.2.x のビルドスタックを利用し続けることにしました。

手軽さの影で

トラブルシューティングの難しさの本質は、その問題がどこのどのレイヤで起こっているのかを推測しなければならないことです。

  • 実行時にライブラリのリンクエラーが発生した時
  • PUSH通知が正しく届かない時
  • Swift コンパイラが -1 で終了した時

などの問題に遭遇した時、私達は自分が当該領域について『思ったほど詳しくなかった』ことに気づきます。

それ自体は問題ではありません。

現代のソフトウェアにおいて全てを把握することは不可能に近いですし、トレードオフに従って優先度の高いものから学習するのは、とても理にかなった方法といえます。

しかし、内部的にどのようなことが起こっているのか、ざっくりでも把握しておくのは有効と私は感じます。

Bitrise の設定は WebUI からすべて行えますし、シンプルなワークフローであれば、ステップを組み合わせて必要な項目を入力するだけで完成します。

しかし、それは単に『抽象化』が行われているだけであって、(当たり前のことを、と思うかもしれませんが)内部的にはxcodebuildなどのコマンドが利用されてビルドが実行されています。ステップという魔法によってビルドされているのではありません。

ステップの実装まで見る必要はありませんが、内部的にどういった処理によってビルドが実現されているのか、気が向いたときにでもビルドログを眺めてみるのも面白いかもしれません。

親切さの影で

今回、この問題に遭遇したユーザの多くは fastlane を導入していた、と考えるのは流石に推測が過ぎるでしょうか?

fastlane はとても親切に作られており、bundler が導入されていない場合、自動的に bundler を導入して自身のバージョンを固定するようになっています。

これは拙著『iOSアプリ開発 自動テストの教科書』でも脚注で明記しています。

iOSアプリ開発自動テストの教科書〜XCTestによる単体テスト・UIテストから、CI/CD、デバッグ技術まで

iOSアプリ開発自動テストの教科書〜XCTestによる単体テスト・UIテストから、CI/CD、デバッグ技術まで

  • 作者:平田 敏之,細沼 祐介
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/06/27
  • メディア: 単行本(ソフトカバー)

Rubyにくわしい方はBundlerを利用してインストールすべきだと思うかもしれません。もちろん最初からBundlerをインストールしても構いませんが、後述の導入手順にしたがってセットアップを進めれば、最終的にGemfileが生成されてfastlaneのバージョンは固定されます。

これは初期導入コストが下がる一方で、利用者が Bundler について殆ど理解せずとも導入できてしまうという諸刃の件でもあります。

そして、たまたま運悪く落とし穴にハマった時に、自分が Bundler について何も理解していなかったことに気づくかもしれません。

最後に

今回起こったことは、結果だけ見ればたった一文で表現できます。

Bitrise における Xcode 11.3.x のビルドスタックにおいて、Bundler のバージョンが 2.x にアップデートされたので、公式ステップ以外の箇所で Bundler 2.x に対応していないステップまたはスクリプトがあった場合は対応が必要

ほとんどの問題の解決策は、エラーメッセージをコピペしてググれば大体すぐに調べられます。しかし、問題を解決するまでに至るアプローチについては共有されないことが殆どだと思います。

私はこの分野のプロフェッショナルではありませんが、今回の問題に対して、比較的短時間で推測を巡らせ、概ね妥当な判断・対応ができたと思います。

この記事は何かを解決するものではありませんが、世の中の誰かの知恵につながればと思い、記事にしてみました。