[ポエム]モバイルアプリのCI/CDのTips(共通編)


CI/CDの興味がなくても、全く整備されていない環境でも、apk/ipaのビルドとデプロイくらいは自動化する

テスト、Lintなどの設定抜きで、apk/ipaのビルド&デプロイをするだけであれば、CIの設定は簡単。その割にはメリットが大きい。
以下、メリット。

手作業が減る

そのまま。

レビューされていないコードの混入を防げる

「PCでビルドしたアプリをデプロイした。ただ、デバッグ向け機能をOnにするコード修正を残したままでビルドしてしまっていたので、リリースしたアプリでデバッグ向け機能が使えてしまう」といったことが防げる。

「そんなしようもないミス起こすわけないだろw」と感じると思うが、ビルドが手作業になっているプロジェクトだと、1年に1回くらいは誰かがやらかす感触がある。そして、この手の問題は症状が重く、大体一騒動が起きる。こういう問題を回避できるのは非常に価値があると思う。

言わずもがなだが、CIツールのビルド対象ブランチをレビュー済みのコードしか入れられないブランチにして、毎回コードリポジトリからチェックアウトするようにすれば実現できる。

アプリのビルド手順・方法を統一できる

「本来、依存ライブラリを「yarn install」で入手しないといけないが、ある開発者は「npm install」を使ってしまった。ビルドは通るものの、想定と異なるバージョンのライブラリが取得されてしまい、ある機能が動作しなかった」といったことが防げる。

CIツールを使う場合、所定のファイルにビルドコマンドを記述していくことになるが、そこに「yarn install」と記述すれば、依存ライブラリを取得には必ずyarn installが使われる。

CIツール固有の機能を使いすぎない(開発リソースに余裕がない場合)

CIツール固有の機能を使えば使うほど、CIツールの学習が必要になり、メンテナンスは難しくなる。

たとえば、Circle CIのorbを自作したとする1。リポジトリをまたいで処理を共通化できるので、すばらしいと思う。
ただ、orb自体に問題があった場合、orbのpublish方法、parameterの渡し方、parameterの型、orbの動作確認の方法などを分かっていないといけない。Circle CIに不慣れなメンバーがメンテナンスするハードルはかなり高い。

一方で、orbを使わずに、CIを設定したとしたらボイラープレートは書かないといけないものの、より少ない知識でメンテナンスでき、設定ファイルの可読性も高くなる。

どうしてもボイラープレートが嫌なのであれば、FastlaneのimportやGradleプラグインを自作する。Circle CIのorbよりは汎用性がある。FastlaneだったらiOSエンジニアには馴染みがあるし、GradleだったらAndroidアプリエンジニアやJava触る人だったら馴染みがある。Circle CIのorbよりはとっつきやすいし、学習のモチベーションも持てる。

CI固有の設定ファイルにビルドコマンドをベタ書きするのを避ける

なるべく、Fastlane、Gradle、Makefile、自作シェルスクリプトといった汎用的な仕組みでビルドコマンドを記述する。CIの設定ファイルに書くと、PCでは実行しづらくなる2。また、CIツールを変えたくなった場合に、引っ越ししづらい。

Fastlane、Gradleだと出来合いのプラグインがある分、ボイラープレート減らしやすい。

ビルド時間を最適化しすぎない(開発リソースに余裕がない場合)

上の話と被る。

ビルド時間を最適化しようとするほど、キャッシュ、Jobの並列実行、Job間のデータの受け渡し、コミットメセージに特定のwordが入っているときだけ何かのJobを実行する、といった汎用性の低いツールのといったCIツール固有の機能を使うのは避けられない。また、汎用性低めのツールの設定も必要になったりする。メンテナンスに必要な知識量が多くなってしまう。

Lint -> UnitTest -> ipa/apkのビルド -> デプロイのような流れがあるとして、これらを1つのJobで直列実行するのありだと思う。直列実行にすると、メンテナンスはしやすいし、可読性も高い。

PRに絡むJobのビルド時間は小さく保つ

PRに絡むJobのビルド時間が長いと、開発速度が落ちる、レビュー指摘に対応するモチベーションが下がる、頭のコンテキストスイッチ3が発生しやすくなるという問題が起きる。個人的には10分以上は待ちたくない。

PR以外だったらビルド時間にそれほど神経質になる必要ないと思うが(といっても1h以内を想定)、PR関連のJobだけはビルド時間の最適化した方が良い。実行するJobを選別する + 順番を付けずにJobを並列実行。

PR時にもipa/apkをビルドし、reviewerがビルドしないでもアプリの動作を見れるようにする

よくアプリのコードのレビューをするが、コードだけ見ていてもわからないことがあるので、ビルドなしでアプリの動作を見れるとレビュワーとしては嬉しい。「そんなに見たいなら、reviewerの方でアプリのビルドすれば良いじゃん」と思うかもしれないが、PRのブランチをチェックアウト -> 依存ライブラリ更新 -> ビルドとするのはかなり手間がかかる。「質問すればいいじゃん」と思うかもしれないが、質問するより自分で見た方が早いことも多い。

ただ、iOSの場合はPRブランチのpushのたびに、ipaビルドさせるかは悩み所。ipaのビルドがかなり遅いので。

ビルドの失敗箇所はすぐに復旧する

失敗している箇所は、すぐに原因を調査して対処するなり、その時だけの失敗なのか判断する。失敗したままの状態が当たり前になると、自動テストで失敗を検知しているのに、気づかずdevelopやmasterにマージしてしまう(自分がよくやらかす)。

もし、問題箇所にすぐに対処できないときは、いっそ、CIで実行しないようにする。本当の問題を見過ごしづらくなる4

不安定なJobのヘルスチェックとして、CIツールの定刻ビルド機能を使う

たとえば、E2Eテスト。E2Eテストを毎日定刻に実行し、不安定になっているテストケースがあれば、開発ワークフロー(develpやmasterにマージしたときのビルド)から外す。不安定の基準は、「7日連続実行して1度でも失敗したら不安定」などとする。テストケースを新規作成した後の様子見にも使える。「7日連続実行して全部成功したら、開発ワークフローに組み込む」というルールにすることで、不安定なJobが開発ワークフローに入り込むことを防げる。

スタイル系のLintは無理に適用しない、バグの原因になり得る箇所を検知するLintは適用する

「インデントの幅が8でなくて4です」「import文の順番が所定の順番でないです」といったスタイル系のLintは割とどうでも良い。対応に結構手間がかかる上、PRのコメントの形で指摘をする設定にすると、レビューのコメントが見づらくなる。自分がレビュワーのとき、スタイル系のLintがするような指摘はほぼしないというのもある(指摘するのは、「if (...)の後には{}を付けましょうくらい」)。

バグの原因になり得る箇所を検知するLintは適用しましょう。「(Android)最小APIレベルが19なのに、21から追加されたAPIを使っています」といった指摘は、普通にクラッシュの原因になるので、そういう指摘には価値を感じる。

CIでのビルド時のツールのセットアップ処理が多くなってきたら、自前のDocker Imageを作る

自前のDocker Imageで作り、Dockerfileでツールのセットアップ処理をする。CIのビルド設定の見通しが早くなる。

Docker Imageの作り方。
https://qiita.com/zigenin/items/b89667c58027f53ec549

ベタな(?)ワークフロー

アプリ

Git Flow

  • 作業ブランチ(feature/, fix/など)にプッシュ
    • Lint
    • UnitTest
    • apkやipaのビルドしてCIツールのアーティファクトに保存
  • developにプッシュ
    • 作業ブランチと同じ
    • Firebase App Distributionで配信
  • masterにプッシュ
    • 作業ブランチと同じ
    • TestFlight/PlayStoreの内部テストトラックに配信
  • タグ付け or GitHub上のprerelease
    • 製品版として公開開始(やりたいと思っているが自分でやったことはない)

GitHub Flow

  • 作業ブランチ(feature/, fix/)にプッシュ
    • Lint
    • UnitTest
    • apkやipaのビルドしてCIツールのアーティファクトに保存
  • masterにプッシュ
    • 作業ブランチと同じ
    • Firebase App Distributionで配信
  • タグ付け or GitHub上のprerelease
    • 作業ブランチと同じ
    • TestFlight/PlayStoreの内部テストトラックに配信
  • GitHub上のrelease
    • 製品版として公開開始(やりたいと思っているが自分でやったことはない)

Androidライブラリ(GitHub Flow)

  • 作業ブランチ(feature/, fix/)にプッシュ
    • Lint
    • UnitTest
  • masterにプッシュ
    • 作業ブランチと同じ
    • バージョン"main-SNAPSHOT"のライブラリをmavenリポジトリにデプロイ
  • vX.Y.Z形式のタグ付け
    • 作業ブランチと同じ
    • バージョン"X.Y.Z"のライブラリをmavenリポジトリにデプロイ

iOSライブラリ(GitHub Flow)

  • 作業ブランチ(feature/, fix/)にプッシュ
    • Lint
    • UnitTest
  • masterにプッシュ
    • 作業ブランチと同じ

Podfileでタグ名やコミットIDを指定する形でライブラリを取得するので、設定は簡素。

CIツールの使い分け

Bitrise、Circle CI、GitHub Actionsの範囲。自分がそれしか使ったことないから。

CIのメンテナンスにリソースを避けない、iOSアプリの設定で楽をしたい -> Bitrise
並列でJobを実行してビルド時間の最適化をしたい -> Circle CI or GitHub Actions
GitHub上のさまざまな操作をトリガーにビルドを実行させたい -> GitHub Actions

得意分野が違うのでどれか1つだけ使うのでなく、併用もあり。ただ、1つのアプリで3つ使うのはやりすぎ。

オンプレミスしばりがあるなら、GitLabサーバを自分で立てて、GitLab Runner使うのが良さげ。


  1. Circle CIをそれなりに使っていないと、概要はすぐに分かっても詳細は全くわからないと思う。正直、ここの文章読む気もなくなったと思う。その感覚がメンテナンスのしづらさを表している。 

  2. ビルド方法を変える場合、いきなりCIで実行して成功するか確認するのではなく、PCで実行して成功するか確認できるとメンテナンス性が上がる。後、CIツールでどうしてもビルド&デプロイができない時の緊急時への備え。 

  3. PRのブランチにプッシュしたときに40分待たされたとする。その時、そのPRと関係ない別の作業をしてしまうと思う。そして、40分後にビルドが終わったときに、40分前の作業の状態を思い出さないといけない。ビルド中は、常にビルド完了するか気にかけているので、頭のリソースが1%くらい浪費されている感もある。 

  4. apk/ipaのビルドは対処必須。デプロイ処理の失敗も、Store側に失敗原因がない限りは対処必須。対処できない場合でも、CIの実行対象から外さなくて良いと思う。