iOS アプリの CI 環境を CircleCI 2.0 に移行したら結構便利

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2017 の投稿記事です。

皆さんこんにちは/こんばんは! キッズリー という保育園向けサービスで 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 を切り替えることで各環境向けのアプリをビルドできるようにしています

Configuration で接続先環境を切り替える

サンプルプロジェクトでは、 AdHoc_DevelopmentAdHoc_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 設定ファイルの記法も大きく変更されており、 jobworkflow などの概念が新たに導入されました。

サンプルプロジェクトの config.yml は下のリンクからご覧になれます。

ここでは、 CI タスクを実行する単位である job として、 dependenciesbuild-and-testupload-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.lockPodfile.lock などのファイルのチェックサムを元に決められるようになったのも嬉しいところです。キャッシュのリストア後に bundle installpod 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 installpod 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)コマンド中の adhocdevelopmentappstore などに変えると、デバッグ用の証明書や 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 側でも設定しておきます。

DeployGate のトークンや match のパスワードを環境変数に設定

また、Certificate や Provisioning Profile を管理するプライベートリポジトリに CI 環境からアクセス可能にする必要があります。この設定については クラスメソッドさんのブログ記事 が分かりやすかったので、困ったときは読んでみてください。5)証明書の管理も GitHub のプライベートリポジトリでやる場合は、 CircleCI の設定画面でボタンを 1 つ押すだけでよいのですが、 BitBucket のプライベートリポジトリを使う場合は少しがんばる必要があります (やり方はクラメソさんの記事で紹介されています) 。

CircleCI 2.0 導入後

画像のように、 YAML で定義したワークフロー・ジョブがコードの push の度に実行されるようになりました。

ワークフローは CircleCI 上でも可視化される

また、次のようにジョブを指定して 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 を行う方法を考えてみました。

(注) この節の内容はバッドノウハウの類なので、 fastlane match を使える環境ならそれを使うのに越したことはないと思います!

.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 上で設定します。

base64 エンコードした .cer と .p12 を環境変数に設定

.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 に比べてビルド環境のスペックがアップしている (らしい) ことが影響しているのではないかと思います。

CircleCI 2.0 導入前後の速度比較

また、 Lint ツールの実行などは Xcode 関連コマンドを必要としないため、実行イメージに macos を使わずとも実行できます。そういったジョブにおいては、 CircleCI 2.0 で新たに追加された Docker コンテナのサポートを活用してさらなる高速化が可能かもしれません。

様々な可能性を内包している CircleCI 2.0 に、今すぐあなたも乗り換えてみませんか!?

参考リンク

脚注

脚注
1 4 月から同じチームにもう 1 人の保坂さんが新卒で入社したので、わたしは主にハンドルネームで呼ばれています。
2 キッズリーの実際のプロジェクトでは InHouse ビルドを行っていますが、今回のサンプルでは多くの環境で試しやすいように AdHoc ビルドで開発版を配布する想定にしました。
3 setup_circle_ci を match の実行前に実行することにより、 CI 環境で実行するときのみ Apple Developer Portal 上の Certificate や Provisioning Profile に対して変更を行わないように (readonly モード) することも可能です。
4 コマンド中の adhocdevelopmentappstore などに変えると、デバッグ用の証明書や AppStore 用の証明書を管理することもできます。より詳しい使い方については fastlane match のドキュメントを参照してください。
5 証明書の管理も GitHub のプライベートリポジトリでやる場合は、 CircleCI の設定画面でボタンを 1 つ押すだけでよいのですが、 BitBucket のプライベートリポジトリを使う場合は少しがんばる必要があります (やり方はクラメソさんの記事で紹介されています) 。