OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

fastlane、Crashlyticsを使ったiOSアプリのリリースプロセス自動化に挑む

大変ご無沙汰しております。麦芽系エンジニアのasmzです。ここ数ヶ月何だかいろいろ忙しくて、結局ビアガーデンに行けぬままシーズン終わってしまいましたね…。

さて、私のエントリは全般的にネタに走る傾向が強いんですが、今回は標題の通り割とまともな(?)エントリです。

というのもここ数ヶ月忙しかったのは、なにやらネイティブアプリ界隈をうろちょろしていたためでして、最近それがやっと一段落し、いくつか知見的なものが溜まってきたので、(ブログネタに困っていたし)ここらでまとめておこうかなと思った次第です。

で、前回エントリから今までにFeBeアプリAndroid版とかリリースされてたり(ぜひご利用下さい~)してるので、その辺の知見を書くのかと思いきや、流れに反して今回は大人の事情でiOSアプリのお話です。

iOSアプリのリリースプロセス

iOSアプリ開発されてる方はご存知かと思いますが、そもそもiOSアプリのリリース作業って結構やることが多いです。

  • Certificates(開発用証明書)、App ID、UDID、Provisioning Profile(AdHoc, AppStore)の準備
  • XcodeプロジェクトのCode Signing、Provisioning Profileの設定
  • AdHoc版をビルドして、ipaファイルをテスターへ配布(複数回)
  • AppStore版をビルドして、ipaファイルをiTunesConnectへアップロード
  • TestFlightでベータテスト
  • 申請

ここでは箇条書きでサクッと書いてますが、これら一つ一つがそれぞれいろんな手順を踏んで割とややこしかったりするし、証明書類なんかはこの作業をするMacに依存するので、リリースする人が複数人いたりするとそれぞれのMacへの設定が必要になります。

で、今でこそこの辺りをCIツールで自動化することは当たり前みたいになっていますが、オトバンクでは残念ながらこれまでその環境が整っていませんでした。ただ、そもそもこの作業自体「このクソ忙しいのに、やってられん!クソが!」と元から思ってはいたのと、とある新規iOSアプリ開発案件があったので「この機会に自動化してしまいたいなー、あんな作業なんか…」というのが今回のエントリに至る経緯となります。

自動化の方針

この作業の自動化にあたってツールの選定とかいろいろ調べたんですが、最終的には以下のサイトを大変参考にさせていただきました。(有益な資料の公開ありがとうございます!)

基本的には上記サイトとまるっと同じ構成で、プロセス全体の進行管理に「fastlane」、ベータ版配布に「Crashlytics Beta」を採用することにしました。

Android開発では既にDeployGateを使用していたので、ベータ版配布はそっちでも良かったんですが、Crashlyticsにしたのは単純に自分が使ったことないツールを試してみたかったのと、割とイケてるUIの解析系機能が付いてたからです。

f:id:asmz0:20151021235430p:plain

これが出来ればうちもモダンで快適な開発ができる!とキラキラと目を輝かせていました。そう、この頃は…。

で、今回の本題ですが…

結論から言うと、無事リリースプロセスの自動化は完了し、実際に使ってみるととても快適です。ただfastlane〜Crashlyticsの辺りの導入作業は結構いろんなところでハマりまして…。作業中はとても辛かった。。。

なので、今回のエントリは「基本的にはこんな感じで出来るハズ」というよくある構築手順の紹介と併せて、各作業内の「ハマりポイント」についても書いていきます。

同じようにハマって困っている人については参考にして頂ければと思いますし、初めてやる作業ばかりで正直このハマり方の解決方法として正しいのかどうかわからないところもあるので、敢えてここでアウトプットすることで、間違いなどあれば(優しく)教えてもらえるかも、という意図もあったりします。

それでは張り切って参りましょー。

fastlane概要

「そもそもfastlaneとは?」とかツールの導入方法などは話はちょっと長くなりそうなので、詳しくは上の方で挙げたサイトや以下のサイトなどご参照ください。

ざっくりというと、fastlane自体はリリースプロセスに関連する幾つかのツールをまとめる統合ツールで、それぞれのツール(actionと呼ばれる)をどう組み合わせて動かすかを制御する位置づけとなります。

公式サイトの図がわかりやすいのですが、fastlaneではいくつかのシーンを「lane(レーン、車線、小道 etc)」という単位で管理し、それぞれのlaneでやりたいactionを組み合わせて一つのプロセスとして管理します。(ただ実は今回の作った構成は、そのlaneの思想からちょっと外れているんですが…)

今回のlane全体構成

上記の自動化方針を実現するために、以下の様なlane構成を作りました。

というか紆余曲折の結果、このような構成になりました、といった感じなんですけどね。

[Fastfile]

platform :ios do
  before_all do
  end

  desc "Setup build enviroment"
  lane :setup do
    cocoapods      # Podsインストール
    team_id "XXXXXXXX"  # プロジェクトのTeamIDを指定
  end

  desc "Create keychain and import certificate"
  lane :keychain do
    # keychain設定とCertificatesインポート
    ...
  end

  desc "Build for AdHoc"
  lane :build_adhoc do
    # AdHoc用Provisioning Profileダウンロード
    ...
    # AdHoc用ipaビルド
    ...
  end

  desc "Build for AppStore"
  lane :build_appstore do
    # AppStore用Provisioning Profileダウンロード
    ...
    # AppStore用ipaビルド
    ...
  end

  desc "Delivery for Tester"
  lane :delivery_tester do
    # テスターへipa配布
    ...
  end

  desc "Delivery for AppStore(TestFlight)"
  lane :delivery_appstore do
    # iTunes Connectへipaアップロード
    ...
  end

  after_all do |lane|
  end

  error do |lane, exception|
  end
end

ちなみに、せっかくlaneという単位でまとめてactionを記載できるので、ホントはこんなに細かくlane分けなくてもいいといえばいいんですが、プログラマ的発想で単純に共通処理は外に括り出したかったのと、環境構築の最中にいろいろエラーとかでて、それぞれ単独で実行してちゃんと動くか何度も何度も繰り返していたので、結果的にこんな構成になった感じです。

以降、それぞれのlaneの中に書くactionについて説明していきますー

Keychain設定とCertificatesインポート

基本的にはこんな感じ

Xcodeでリリース作業するときは、そのMacのKeychainにiOS Dev Centerでいろいろやって準備したCertificatesが登録されているかと思いますが、それと同じことをCircleCI上のOSX VMでも行う必要があります。

こちらはfastlaneで用意されている「create_keychain」で専用のKeychainを作成し、「import_certificate」でそのKeychainにインポートしていきます。

  lane :keychain do
    # 独自のキーチェーンを用意して、証明書類を入れる
    create_keychain(
      default_keychain: true,
      unlock: true,
      timeout: 3600,
      lock_when_sleeps: true
    )
    import_certificate certificate_path: "path/to/AppleWWDRCA.cer"
    import_certificate certificate_path: "path/to/dist.p12", certificate_password: ENV['CERT_PASSWORD']
  end

なお、今回はプライベートリポジトリのプロジェクトなのでCertificatesはリポジトリ内に含めてしまっていて、action内でそのパスを指定していますが、credentialな情報なので必要に応じて外から注入するなどの対応が必要です。

ハマりポイント

ここでKeychainへインポートが必要なのは、具体的には以下2点です。

  • Apple Worldwide Developer Relations Certificate Authority
  • 開発用証明書(*.p12)

開発用証明書は、MacのKeychainからp12形式でパスワード付けて書き出しが出来ます。こちらは開発機のMacを変えるときなどに行う作業と一緒なので割とすぐ準備できたのですが、「Apple Worldwide Developer Relations Certificate Authority」って何よ、ってことで調べたら、以下でダウンロードできるんですね。

  • iOS Dev Center
    • Certificates, Identifiers & Profiles
      • 左のCertificates選択
        • 「+」で新規作成しようとした画面の一番下

f:id:asmz0:20151022182124p:plain:w300

(ちっ、わかりづらい…)

Provisioning Profileのダウンロード

基本的にはこんな感じ

iOSアプリリリース作業の鬼門Provisioning設定ですが、こちらの作業もfastlaneのaction「sigh」というものが既に用意されています。こちらはiTunes Connectに自動で接続して、Provisioning Profileが無ければ作成、あればそのままその最新版をダウンロードしてくれます。

  desc "Build for AdHoc"
  lane :build_adhoc do
    # AdHoc用のProvisioning Profile指定
    sigh(
      adhoc: true
    )

    ...

  end

  desc "Build for AppStore"
  lane :build_appstore do
    # オプション無しならAppStore用
    sigh

    ...

  end

今回はAdHoc版とAppStore版でビルド処理を分けているので、それぞれのビルド用laneでsighを実行するよう指定しました。

なおここでsighが正常に完了すると、以下の変数でそのProvisioning ProfileのUDIDを取得することができます。こちらはそのままfastlane内の別処理で使用することが可能です。

lane_context[SharedValues::SIGH_UDID]

ハマりポイント

何かいくらググっても他の人はこの問題に引っかかってないのか情報が出てこなかったのですが、sighがiTunes Connectに接続するためには当然ながらパスワードが必要となり、初回はCLIでの実行時にパスワード入力プロンプトが表示されます。(1度入力すればKeychainに保存されて、次回以降は聞かれません)

ローカル環境でfastlaneを使ってる分にはいいんですが、今回CircleCI上で実行しているのでそれじゃ困るので、以下のようにKeychainに直接iTunes Connectのパスワード設定してからsighを実行するようにしました。

[Fastfile]

sh "~/path/to/itunes-login.sh"

[itunes-login.sh]

#!/bin/sh

security add-internet-password -s xxxxxxx -a xxxxxxx -w $ITUNES_PASSWORD  # CircleCIの環境変数で設定

Keychainへインターネットパスワードを追加するactionは見たところ用意されていないようなのと、fastlane上で直接コマンド実行するとコンソール上にそのままコマンドが表示されるらしく、パスワード見えちゃうかと思い、とりあえず自前のシェルスクリプトで。

(しかし、こんなことやってる人見かけないけど、みんなどうやってるんだろうか…?)

[2015/12/22追記]

どうやら環境変数FASTLANE_USER FASTLANE_PASSWORD にアカウント情報を設定しておけばOKのようです!(記事にコメント頂いたnerd0geek1さん、ブコメ頂いたgamellaさん、ありがとうございます!)

Xcodeプロジェクトのビルド、ipa生成

基本的にはこんな感じ

本題となるビルド作業ですが、こちらもfastlaneのaction「gym」を利用しています。(fastlaneなんでもできるな…)

ここで正常にビルドが完了すれば、プロジェクトルートディレクトリに「*.ipa」ファイルが生成されます。

  desc "Build for AdHoc"
  lane :build_adhoc do
    sigh( ... )

    ENV["PROFILE_UDID"] = lane_context[SharedValues::SIGH_UDID]

    # AdHoc版ビルド
    gym(
      clean: true,
      workspace: 'AsmzBeer.xcworkspace',
      scheme: 'AsmzBeer',
      use_legacy_build_api: true,
    )
  end

  desc "Build for AppStore"
  lane :build_appstore do
    sigh

    ENV["PROFILE_UDID"] = lane_context[SharedValues::SIGH_UDID]

    # AppStore版ビルド
    gym(
      clean: true,
      workspace: 'AsmzBeer.xcworkspace',
      scheme: 'AsmzBeer',
    )
  end

CodeSigningについてはこちらのfastlaneのドキュメントに記載の方法だと、sigh実行後に取得できるSIGH_UDIDを一旦環境変数に設定し、そのXcodeプロジェクトのProvisioning Profile設定でその変数を受けられるようにしておく、ということをするようです。

なお、AdHoc版のビルドで付いているuse_legacy_build_api: trueオプションは、こちらのissueによるものです。

XCode 7: The generated archive is invalid · Issue #115 · fastlane/gym · GitHub

この辺は今後何らか対応されれば不要になるのかなと思われます。

ハマりポイント

既に上記のuse_legacy_build_apiオプションについても実はだいぶハマっていたんですが、それ以外にも何故かCode Signing Errorが連発しまくっていました。

⌦  Code Sign error: No code signing identities found: No valid signing identities (i.e. certificate and private key pair) were found.

解せないのは、これが出ている場所が自プロジェクトではなく、CocoaPodsで入れたライブラリ(Alamofire)だったことです。

とりあえずCodeSigning関係のエラーなので、先ほど書いたこちらのfastlaneのドキュメントの部分で、何かやりかた間違えてるのかな―といろんな書き方してみたんですが、このアプローチでは何故か何度やっても同じエラーとなりました。

で、いろいろ試行錯誤した結果、以下のようにxcodebuildに追加でPROVISIONING_PROFILEPRODUCT_BUNDLE_IDENTIFIERパラメータ指定してやることでビルド成功することがわかりました。

    # xcodebuild 引数設定
    xcodebuild_args = {
      PROVISIONING_PROFILE: lane_context[SharedValues::SIGH_UDID],
      PRODUCT_BUNDLE_IDENTIFIER: "beer.asmz.AsmzBeer",
    }

    xcodebuild_args = xcodebuild_args.map do |k,v|
      "#{k.to_s.shellescape}=#{v.shellescape}"
    end.join ' '

    # ↑ここまではxcargsに渡す"OPTION1=value1 OPTION2=value2 ..."みたいな
    #   形式を作っているだけです

    gym(
      clean: true,
      workspace: 'AsmzBeer.xcworkspace',
      scheme: 'AsmzBeer',
      xcargs: xcodebuild_args
    )

ちなみにPRODUCT_BUNDLE_IDENTIFIERの設定が無いと、以下の様なエラーとなりました。

⌦  Code Sign error: Provisioning profile does not match bundle identifier: The provisioning profile specified in your build settings (“beer.asmz.AsmzBeer AppStore”) has an AppID of “beer.asmz.AsmzBeer” which does not match your bundle identifier “org.cocoapods.Alamofire”.

この結果から察するに、自プロジェクトではPROFILE_UDIDを変数として受けるように設定しているし、bundle identifierは元からXcodeプロジェクト上で設定しているけど、CocoaPodsで入れたプロジェクトにはそれらの設定が入っていないことが原因かな?という感じです。(ちょっとこの辺は間違ってたらすみません。。。あとAlamofire以外でも同じエラーになるのかは今回試せてません)

プロジェクトによると思いますが、CocoaPodsのように基本的に外部から導入するものは今回リポジトリ管理しておらず、CocoaPodsの各プロジェクトの設定を直接変えるような事もしない方針のため、上記のxcodebuildのオプションとして指定する方法のまま行くことにしました。

(この辺も、他に同じ問題に引っかかっている人がなかなか見つけられていない…何か別な方法あるんですかね?)

Crashlytics BetaでAdHoc版ipaのテスター配布

実はここに来るまでに何度も心が折れかけたのですが、やっとipaファイルが生成されて、ついにベータ版配布です!

基本的にはこんな感じ

既に上の方で書いた通りベータ版配布にはCrashlyticsを使いますが、fastlaneにはCrashlyticsを起動させるactionが既に用意されているので、やはりfastlaneも使います。(ここまで散々苦労させられてきたが、やはり頼らざるを得ないなこいつには…)

まずこれを使うにはFabricとCrashlyticsが導入されている必要があります。導入手順はこちらが大変参考になりましたのでご参照下さい。

また、テスター情報の登録は別途https://fabric.ioにて準備しておく必要があります。ちょっと既にこのエントリだいぶ長くなってきたので、この辺の手順は割愛させていただきます。(Fabric、Crashlyticsだけでもう1エントリ書けそうだな…)

(2015/11/04 追記)こちらにFabricについての説明をまとめましたので、あわせてご覧ください。

engineering.otobank.co.jp

Fastfileへの記述は、以下の様な感じです。

  desc "Delivery for Tester"
  lane :delivery_tester do
    crashlytics(
      crashlytics_path: './Pods/Crashlytics/Crashlytics.framework',
      api_token: ENV['CRASHLYTICS_API_TOKEN'],
      build_secret: ENV['CRASHLYTICS_BUILD_SECRET'],
      ipa_path: './AsmzBeer.ipa',
      groups: 'tester'
    )
  end

一点注意事項として、groupsオプションに設定する内容はCrashlyticsのDashboardページで登録したグループの「groupAlias」であり、「group名」ではありません。(ややこしい)

ハマりポイント

ハマりどころというよりは、私が全体フローを理解していないまま始めたために、最初はTestFlightと同じ感覚でAppStore版を配布していてうまくいかなかった、というのが正直なところです。

改めてテスターへの配布までをまとめると、以下の様なフローとなります。

  1. 招待メールをユーザに送信
  2. ユーザがそのメールのリンク先よりiOSプロファイルをインストール
  3. その端末のUDIDがCrashlyticに送信される
  4. UDIDを開発者がiOS Dev CenterのDevicesに登録する
  5. その端末で使用できる設定を入れたProvisioning Profileを生成
  6. そのProvisioning Profileを使って作ったipaがCrashlyticsにアップされれば、ユーザがインストール可能になる

AppStore版でやってると、必然的に5.の作業が出来なくて詰まります。

iTunes Connectへのアップロード

ついにこれで最後になりますが、こちらはもうfastlaneのaction「deliver」で一発です。最後の最後にして、やっと初めてハマりポイントなくすんなり行きました…。あ、上で説明したiTunes Connectへの接続は、ここでも必要ですけどね。

  desc "Delivery for AppStore(TestFlight)"
  lane :delivery_appstore do
    deliver(
      force: true,
      skip_screenshots: true,
      skip_metadata: true
    )
  end

今回はipaファイルのアップまでしか対応していないので、skip_screenshotsskip_metadataの設定を入れていますが、スクリーンショットやAppStore紹介文などのメタ情報もここからアップ可能です。つまり、この辺もリポジトリ管理出来るってことですね。

CircleCI側の設定

以上までで準備したfastlaneのプロセスについて、実際に動かすためにcircle.ymlに以下のように設定しました。

...

(fastlaneやCocoaPods導入のために、bundle installなどこの辺で適宜)

...

deployment:
  development:
    branch: develop
    commands:
      - bundle exec fastlane setup
      - bundle exec fastlane keychain
      - bundle exec fastlane build_adhoc
      - bundle exec fastlane delivery_tester
  production:
    branch: master
    commands:
      - bundle exec fastlane setup
      - bundle exec fastlane keychain
      - bundle exec fastlane build_appstore
      - bundle exec fastlane delivery_appstore

まぁこの辺は見た通りですね。実運用ではこれ以外のfeatureブランチにpushされた場合は、自分の端末だけにAdHoc版を配布するようにしてたりします。

これで、一連のプロセス自動化は完成です!

実際に使ってみて

良かった点

まぁさすがにこれまで全く自動化出来てない状態からのスタートだったから、ということもありますが、特にベータ版配布が相当楽になった実感があります。

作ってる側はホントに作る作業に集中してるだけで、裏で勝手に配布されてテストされてる、という状況ですからねー。

イマイチな点

fastlaneのissueにも上がってるんですが、sighの処理でiTunes Connectに接続するとき、結構頻繁に302エラーになってCI止まることがあります。

リトライすれば動くんですが、そもそもビルド自体結構時間かかるのでその上2回くらい同じエラーとか出たりすると割と「うがー」ってなります。

まとめ

もともとCIで何かしら自動化すること自体、作業効率的に良くなることは割と周知の事実なわけですが、特にiOSアプリは元の手順がめんどくさいだけあって、これが自動化出来ると費用対効果がかなり大きい感じです。

あと、めんどくさいことが減るだけで結構開発に集中できるので、開発自体のモチベーション上がったりします。(基本的にめんどくさがり屋なので…)

余談

そもそもこの環境構築時、何でか自分のローカル環境でのfastlane実行がうまく行かず、CircleCI側では一応進んでいるからということで、仕方なく全ての作業をCircleCIへpushして動作を確認する、という異常に効率の悪いやり方を強いられていました。(実は今もローカル環境ではうまくいかない)

で、そんなことをやってた結果、この対応をしていたブランチのコミットログは以下のようにとても悲惨な感じになりました。。。

f:id:asmz0:20151022235913p:plain

そして、初めてビルド成功した時のコミットログが泣ける…

f:id:asmz0:20151023000101p:plain

あ、安西先生……!