iOS アプリの CI 環境を CircleCI 2.0 に移行したら結構便利
保坂智之
皆さんこんにちは/こんばんは! キッズリー という保育園向けサービスで iOS アプリを担当している保坂 (智) です。1)4 月から同じチームにもう 1 人の保坂さんが新卒で入社したので、わたしは主にハンドルネームで呼ばれています。最近は ソウルに行ったり スキューバダイビングしたり していました。がんばルビィ。
弊社では以前から多くのプロジェクトで CI 環境に CircleCI を活用しています。わたしが担当しているキッズリーの iOS プロジェクトでも、 CircleCI 1.0 の頃からユニットテストの実行や DeployGate でのテスト版の配信などを CI ジョブとして実施していました。
そんな折、 Circle CI 2.0 が iOS プロジェクトのビルドにも対応した とのことなので、今回思い切って CI 環境を Circle CI 2.0 に移行してみました。その結果、何が便利になったのか、どういうハマりポイントがあったのかなどを紹介したいと思います。
CircleCI 2.0 導入後のイメージ
説明のために、実際の構成に近い iOS のサンプルプロジェクトを用意しました。ライブラリ管理に CocoaPods を使い、テスト実行や開発版配信などのタスク自動化に fastlane を使った、よくある構成のプロジェクトです。
ところで、キッズリーの iOS アプリはほとんどの画面で API サーバーとの通信を行います。キッズリーの API サーバーには、本番アプリから参照される Production 環境の他に、開発用の Development 環境や Staging 環境などがあります。そのため、キッズリーの iOS プロジェクトでは接続先のサーバー環境ごとに Configuration を用意し、 Configuration を切り替えることで各環境向けのアプリをビルドできるようにしています 。
サンプルプロジェクトでは、 AdHoc_Development
と AdHoc_Staging
という 2 種類の Configuration を用意しています。前者は Development 環境向けアプリを、後者は Staging 環境向けアプリを AdHoc ビルドすることを想定した Configuration になっています。2)キッズリーの実際のプロジェクトでは InHouse ビルドを行っていますが、今回のサンプルでは多くの環境で試しやすいように AdHoc ビルドで開発版を配布する想定にしました。
DeployGate 配信を行う fastlane タスクを用意し、タスク実行時にビルドに使う Configuration を指定できるようにすれば、開発版のアプリを配信する作業が楽になりそうです。また、 develop
ブランチに PR がマージされたタイミングで自動的に Development 環境用のアプリが DeployGate に配信される仕組みも実現したいと思います。
CircleCI 2.0 導入作業解説
ここからは、 Circle CI 2.0 の導入方法について、順を追って具体的に説明していきます。
job と workflow の定義
まず大きなところで言うと、 CircleCI 2.0 では、設定ファイルの置き場所がプロジェクトルートの circle.yml
から .circleci/config.yml
に変わりました。また、YAML 設定ファイルの記法も大きく変更されており、 job
や workflow
などの概念が新たに導入されました。
サンプルプロジェクトの config.yml
は下のリンクからご覧になれます。
ここでは、 CI タスクを実行する単位である job
として、 dependencies
、 build-and-test
、 upload-to-deploygate
の 3 つを定義しています。それぞれのジョブの役割は以下の通りです。
ジョブ名 | 内容 |
---|---|
dependencies | 依存関係の解決(とキャッシュへの保存) |
build-and-test | ユニットテストの実行 |
upload-to-deploygate | DeployGate 配信 |
コードが push された際にこれらのジョブが実行されるワケですが、その実行順や依存関係を定義するのが workflow
という概念です。
今回の構成では、まず dependencies
タスクで依存関係を解決し、その後ユニットテストの実行と DeployGate 配信を並列に行うようにしてみました。ユニットテストの実行と DeployGate 配信を並列に行うことにより、 CI ジョブ全体の完了が以前より早くなることが期待されます。
実際の workflow
の定義は下記のようになりました。
workflows:
version: 2
build:
jobs:
- dependencies
- build-and-test:
requires:
- dependencies
- upload-to-deploygate:
requires:
- dependencies
filters:
branches:
only:
- develop
filters
の部分で upload-to-deploygate
ジョブが実行されるブランチを制限しているため、開発版の配信は develop ブランチが更新されたときのみ実行されます。この記述により、develop
ブランチに PR をマージしたときに自動的に DeployGate 配信するという挙動が実現できます。
キャッシュのセーブとリストア
CircleCI 2.0 で大きく使い勝手が向上したのがキャッシュの扱いです。以前はキャッシュのタイミングなどをユーザー側で指定できませんでしたが、 Circle CI 2.0 ではキャッシュをセーブ・リストアするタイミングをカスタマイズできるようになりました 。
また、キャッシュのキーを Gemfile.lock
や Podfile.lock
などのファイルのチェックサムを元に決められるようになったのも嬉しいところです。キャッシュのリストア後に bundle install
や pod install
などのコマンドが実行されるため、CI ジョブ全体の実行時間削減が期待できます。
実際に、 pod install
の結果をキャッシュするには、ジョブ定義の steps
内に次のような記述をします。
- restore_cache:
# Podfile.lock を元にキャッシュキーを決定し、リストアを試行
key: v1-pods-{{ checksum "Podfile.lock" }}
- run:
name: Install CocoaPods
command: |
# cocoapods の repo を S3 経由でダウンロードする
curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf
bundle exec pod install
- save_cache:
# Pods 以下をキャッシュに保存
key: v1-pods-{{ checksum "Podfile.lock" }}
paths:
- Pods
今回の構成では、このように bundle install
や pod install
の結果を dependencies
ジョブでキャッシュしています。以降のジョブでは、キャッシュをリストアしてビルドやテストの際に参照する仕組みになっています。
Fastfile の編集
Circle CI 2.0 では、以前のウェブ UI から .mobileprovision / .cer / .p12 を設定する方式に代わり、 fastlane match による Code Signing が推奨されています。そのため、 DeployGate 配信を行うには、 fastlane match を使って Code Signing を行うよう fastlane の設定ファイルを記述する 必要があります。
ちなみに、 fastlane match を実行すると自動的に Certificate や Provisioning Profile の生成が行われるため、共有の Developer Program を使っている場合などは操作に注意する必要があります。3)setup_circle_ci
を match の実行前に実行することにより、 CI 環境で実行するときのみ Apple Developer Portal 上の Certificate や Provisioning Profile に対して変更を行わないように (readonly モード) することも可能です。
fastlane match 対応を行った結果、 ipa ファイルを生成する adhoc
タスクの定義は次のようになりました。
lane :adhoc do |options|
match(type: "adhoc")
gym(
scheme: "CircleCI2Example",
configuration: options[:configuration] || "AdHoc_Development",
export_method: "ad-hoc",
output_directory: "builds",
output_name: "adhoc")
...
end
Fastfile
の全体は下のリンクからご覧になれます。
fastlane match の初期化と設定
今回は AdHoc ビルドに使う Certificate や Provisioning Profile を fastlane match で管理したいので、下記のコマンドを実行し、初期設定を行います。4)コマンド中の adhoc
を development
や appstore
などに変えると、デバッグ用の証明書や AppStore 用の証明書を管理することもできます。より詳しい使い方については fastlane match のドキュメントを参照してください。
$ bundle exec fastlane match adhoc
途中で Certificate や Provisioning Profile を管理するプライベートリポジトリの URL (プロジェクト本体のリポジトリとは別) を求められるので、 GitHub や BitBucket で予めプライベートリポジトリを作成しておき、その URL を入力してください。その他、Apple Developer Portal にログインする際の username や、ビルドするアプリの Bundle ID などもここで設定します。
設定が完了すると、次のような Matchfile
が生成されます。
プロジェクトの変更
プロジェクトを新規作成するとデフォルトで Automatically manage signing
がオンになりますが、 fastlane match を使う際はこの設定をオフにする必要があります。
CircleCI 2.0 ドキュメントの Setting Up Code Signing for iOS Projects を参考にプロジェクトの設定を行います。
CircleCI の設定
match の初期設定で入力したパスフレーズや、DeployGate 配信のための API Key などを環境変数として CircleCI 側でも設定しておきます。
また、Certificate や Provisioning Profile を管理するプライベートリポジトリに CI 環境からアクセス可能にする必要があります。この設定については クラスメソッドさんのブログ記事 が分かりやすかったので、困ったときは読んでみてください。5)証明書の管理も GitHub のプライベートリポジトリでやる場合は、 CircleCI の設定画面でボタンを 1 つ押すだけでよいのですが、 BitBucket のプライベートリポジトリを使う場合は少しがんばる必要があります (やり方はクラメソさんの記事で紹介されています) 。
CircleCI 2.0 導入後
画像のように、 YAML で定義したワークフロー・ジョブがコードの push の度に実行されるようになりました。
また、次のようにジョブを指定して CircleCI の API を叩くことで、 CircleCI の外から DeployGate の配信をトリガーできるようにもなりました。
$ curl -X POST --header "Content-Type: application/json" -d '{
"build_parameters": {
"CIRCLE_JOB": "upload-to-deploygate",
"ADHOC_CONFIGURATION": "AdHoc_Development"
}
}
' https://circleci.com/api/v1.1/project/github/recruit-mp/kidsly-ios/tree/develop?circle-token=XXXX
ビルド時の Configuration を変えるのも、 API に渡す ADHOC_CONFIGURATION
を変えるだけで対応できます。これなら、 Slack 経由での ChatOps なども簡単に実現できそうですね。
最高〜
余談: Circle CI 1.0 の Code Signing サポートを再現する
上記では fastlane match の導入手順を紹介しましたが、実際のところ、 Circle CI 1.0 から 2.0 にマイグレーションするためにプライベートリポジトリを新たに作ったりするのは少し面倒です。また、社内で 1 つの Apple Developer Team を共有している場合など、いきなり Certificate や Provisioning Profile の管理を自動化するのが難しいケースもあるでしょう。
そういったケースでは、 Circle CI 1.0 の頃のように、プロジェクトごとに使用する Certificate や Provisioning Profile などを CircleCI に設定するやり方が便利です。しかし、残念ながら CircleCI 2.0 では、 Certificate や Provisioning Profile をアップロードする Code Signing のウェブ UI が提供されていません (2017年12月15日現在)。
そのため、この制限をなんとか回避して、以前と同じ使い勝手で Code Signing を行う方法を考えてみました。
.cer / .p12 ファイルのアップロード
まず、キーチェーンで .cer
ファイルと .p12
ファイルを適当にエクスポートします。.p12 の書き出し時にはパスフレーズが要求されるので、なるべくセキュアなパスフレーズを設定し、覚えておくようにします。
続いて、書き出した .cer と .p12 を次のコマンドで base64 文字列に変換します。
$ cat foo.cer | base64 | pbcopy
# クリップボードの内容を CircleCI の環境変数に設定する (INHOUSE_CER_BASE64)
$ cat foo.p12 | base64 | pbcopy
# クリップボードの内容を CircleCI の環境変数に設定する (INHOUSE_KEY_BASE64)
base64 エンコード後の文字列と、 .p12 書き出し時のパスフレーズを下記のように CircleCI 上で設定します。
.mobileprovision をリポジトリに含める
Code Signing 時に使用する .mobileprovision ファイルは、リポジトリ内に追加して commit / push しておきます。今回のサンプルプロジェクトのように Bundle ID が複数ある場合は、各 Bundle ID 用の .mobileprovision をリポジトリに追加します。
証明書類をセットアップする fastlane タスクを用意
CircleCI 1.0 の Install Code Signing Credentials
と同等の処理を行うタスクを次のように定義します。このタスクを実行すると、環境変数で定義した .cer や .p12 、およびリポジトリ内の .mobileprovision ファイルがビルド環境にインストールされます。
desc "Install Code Signing Credentials"
lane :setup_provisioning_profiles do |options|
create_keychain(
name: ENV["KEYCHAIN_NAME"],
password: ENV["KEYCHAIN_PASSWORD"],
unlock: true,
timeout: 3600)
`curl -OL https://developer.apple.com/certificationauthority/AppleWWDRCA.cer`
`echo #{ENV['INHOUSE_CER_BASE64']} | base64 -D > inhouse.cer`
`echo #{ENV['INHOUSE_KEY_BASE64']} | base64 -D > inhouse.p12`
import_certificate(
certificate_path: "fastlane/inhouse.cer",
keychain_password: ENV["KEYCHAIN_PASSWORD"])
import_certificate(
certificate_path: "fastlane/inhouse.p12",
certificate_password: ENV["INHOUSE_KEY_PASSWORD"],
keychain_password: ENV["KEYCHAIN_PASSWORD"])
import_certificate(
certificate_path: "fastlane/AppleWWDRCA.cer",
keychain_password: ENV["KEYCHAIN_PASSWORD"])
# カレントディレクトリは Fastfile と同じディレクトリなので、親ディレクトリを検索する
Dir.glob("../*.mobileprovision").each {|filename|
puts filename
FastlaneCore::ProvisioningProfile.install(filename)
}
end
setup_provisioning_profiles
の実行後に gym
アクションを実行すれば、正しく Code Sign 済みの ipa パッケージができあがるはずです。よかったですね。
おわりに
こうしたノウハウを得ながら CircleCI 2.0 に移行した結果、以前は 16 分半ほど掛かっていた CI ジョブの実行時間が 12 分半まで短縮 されました。CircleCI 2.0 で以前よりもキャッシュを活用しやすくなったことや、 CircleCI 1.0 に比べてビルド環境のスペックがアップしている (らしい) ことが影響しているのではないかと思います。
また、 Lint ツールの実行などは Xcode 関連コマンドを必要としないため、実行イメージに macos
を使わずとも実行できます。そういったジョブにおいては、 CircleCI 2.0 で新たに追加された Docker コンテナのサポートを活用してさらなる高速化が可能かもしれません。
様々な可能性を内包している CircleCI 2.0 に、今すぐあなたも乗り換えてみませんか!?
参考リンク
- Migrating Your iOS Project From 1.0 to 2.0
- Simplify your life with fastlane match
- fastlane match を既存の鍵・証明書・PPで使う
- iOS アプリの継続ビルドを CircleCI に変更した
脚注
↑1 | 4 月から同じチームにもう 1 人の保坂さんが新卒で入社したので、わたしは主にハンドルネームで呼ばれています。 |
---|---|
↑2 | キッズリーの実際のプロジェクトでは InHouse ビルドを行っていますが、今回のサンプルでは多くの環境で試しやすいように AdHoc ビルドで開発版を配布する想定にしました。 |
↑3 | setup_circle_ci を match の実行前に実行することにより、 CI 環境で実行するときのみ Apple Developer Portal 上の Certificate や Provisioning Profile に対して変更を行わないように (readonly モード) することも可能です。 |
↑4 | コマンド中の adhoc を development や appstore などに変えると、デバッグ用の証明書や AppStore 用の証明書を管理することもできます。より詳しい使い方については fastlane match のドキュメントを参照してください。 |
↑5 | 証明書の管理も GitHub のプライベートリポジトリでやる場合は、 CircleCI の設定画面でボタンを 1 つ押すだけでよいのですが、 BitBucket のプライベートリポジトリを使う場合は少しがんばる必要があります (やり方はクラメソさんの記事で紹介されています) 。 |