iOSアプリのサブミット自動化と証明書管理の効率化

こんにちは。技術部モバイル基盤グループの @ です。

fastlaneCore Contributorを務めており、 社内ではプロのコードサイン解決者 *1 としての職務経験を積んでいます。

今回はクックパッドでのfastlaneを使ったiOSアプリのサブミット自動化と、証明書管理についての事例を紹介したいと思います。

CIによるiOSアプリサブミットの自動化

クックパッドでは、昨年の春頃よりiOSアプリのサブミットをチャットbot経由で行っています。

このように、Slack上でサブミットジョブを実行すると、CIでアプリがビルドされ、審査提出までを完全自動で行ってくれます。

f:id:gigi-net:20180516173922p:plain

審査提出には、ビルドや処理待ちの時間を含めると多くの工数がかかり、人為的なミスが起こる可能性もありましたが、 完全な自動化により、高頻度のアプリリリースに耐えられるようになりました。

アーキテクチャは以下の図のようになっており、チャットbotからJenkinsのジョブを実行し、そこでfastlaneを利用しています。

f:id:gigi-net:20180516173935j:plain

詳しく知りたい方は下記の資料をご覧ください。

この仕組みは、昨年まではクックパッドアプリでのみ行っていました。

しかし、今年に入ってからライブ配信アプリのcookpadTVや、レシピの投稿者がより使いやすいクックパッド MYキッチンなど、新規アプリの開発が活発になり、ほかのアプリでも自動サブミットの仕組みを導入する必要が出てきました。

このような仕組みをスケールするに当たって、一番の障害になるのがやはりコードサインです。 クックパッドでは、複数台のMac端末をCIサーバーとして運用しており、その全てに、多くのProvisioning Profileを配布、更新する必要がありました。 これらを手動で管理するのは現実的ではありません。

fastlane/matchを使った証明書管理

そこで、fastlaneのユーティリティの1つであるmatchを利用して、証明書やProvisioning Profileの管理、配布を自動化しました。

matchの仕組み

matchは、証明書や秘密鍵、Provisioning Profileを、git管理し、複数の環境で共有できるようにするツールです。 Apple Developer CenterのAPIを叩いて証明書やProvisioning Profileを作成し、暗号化を施してgitリポジトリに共有してくれます。

f:id:gigi-net:20180516173944p:plain

まず、iOSアプリケーションのリポジトリにMatchfileという設定ファイルを設置します。 これで、match利用時にデフォルトで設定されるパラメータを指定できます。

ここでは、コミット先のリポジトリを予め指定しています。

git_url "git@example.com:cookpad/certificates.git"

次に、開発者は開発環境からProvisioning Profileを作成します。matchはCLIを提供しているため、それを使うのが便利です。

typeを指定することで、ストア配布用のほか、AdHocビルドや、Enterprise配布用のProvisioning Profileも作成できます。

$ fastlane match --type appstore \
                 --app_identifier com.cookpad.awesome-app,com.cookpad.awesome-app.NotificationService

この操作で、Provisioning Profileが生成、コミットされました。

暗号化、復号化に利用するパスフレーズは、初回起動時のみ対話的に聞かれ、以後はmacOSのキーチェーンに保存されます。 また、MATCH_PASSWORDの環境変数で指定することもできます。

CIサーバーでビルドする際は、fastlaneを用いて、以下のように簡単に証明書の取得、コードサインを行うことができます。

Fastfile上に以下のように記述します。

# Sync certificates and Provisioning Profiles via git repository
match(
  app_identifier: ["com.cookpad.awesome-app", "com.cookpad.awesome-app.NotificationService"],
  type: 'appstore',
  readonly: true,
)

# Build iOS app with the profiles
build_ios_app

readonlyは、リポジトリやApple Developer Centerに変更を加えないようにするための設定値です。 CIサーバーからのProvisioning Profileや証明書の不用意な更新を防げます。

これにより、手元で証明書の更新、追加作業を行うだけで、全てのビルド環境で最新の証明書類が利用できるようになりました。

f:id:gigi-net:20180516173953p:plain

複数ライセンスでのmatchの利用

また、クックパッドでは、AppleのDeveloperライセンス(チーム)も、ストア公開用のライセンスのほか、社内配布用のEnterpriseライセンスを始めとした複数のライセンスを利用しています。

matchではライセンスごとに別のgitブランチを作成することで、複数のライセンスの証明書類を、1つのリポジトリで管理することができます。 CLIでは、以下のようにgit_branchオプションを渡します。

$ fastlane match --type enterprise \
                 --app_identifier com.cookpad.awesome-app,com.cookpad.awesome-app.NotificationService \
                 --git_branch enterprise \
                 --team_id $ENTERPRISE_TEAM_ID

この場合も以下のようにビルド時に証明書類を取得できます。

# Sync certificates and Provisioning Profiles via git repository
match(
  app_identifier: ["com.cookpad.awesome-app-for-inhouse", "com.cookpad.awesome-app-for-inhouse.NotificationService"],
  git_branch: 'enterprise',
  team_id: enterprise_team_id,
  type: 'enterprise',
  readonly: true,
)

# Build iOS app with the profiles
build_ios_app

運用してみての問題点

一見便利なmatchですが、今回大規模に運用してみて、下記のような問題に直面しました。

1ライセンス当たり同時に1つの証明書しか扱えない問題

matchの一番の問題点は、同時に管理できる証明書が1つに制限されてしまうという問題です。

Apple Developer Centerでは、1ライセンス当たり同時に2つの証明書を作成することができますが、matchでは、新しく証明書を作成したい場合は、match nukeと呼ばれる機能を使い、既存の証明書と、それを利用しているProvisioning Profileを全てrevokeする必要があります。

そのため、証明書がexpireする前に、新旧2つの証明書を用意し、徐々に切り替えていくという方法を取ることができません。

この問題はissueにもなっており議論されていますが、今のところ対応されておりません。

Provisioning Profile作成時に証明書のIDを渡すことで複数の証明書を管理できる仕組みを個人的に検討しており、そのうち開発したいと考えています。

Enterpriseライセンスの証明書更新で困る問題

サブミット用の証明書やProvisioning Profileは、revokeしても、すでにApp Storeにリリースしているアプリは影響を受けません。 上記の問題の影響を大きく受けるのがApple Developer Enterpriseライセンスです。

Enterprise証明書でApp Storeを経由せずに配布しているアプリは、証明書がrevokeされた瞬間に、全てのインストール済みの端末でその証明書を使って署名したアプリが動作しなくなります。

例えばクックパッドでは、最近Cookpad Studioという、ユーザーさんが実店舗で料理動画を収録できるサービスを展開しています。

こちらでは、全国のスタジオでEnterpriseライセンスで配布した業務用アプリを利用しているのですが、証明書のrevokeにより動作しなくなる危険性があります。

多くの端末を利用しているので、全国で同時に更新作業をするのは容易ではありませんが、現在のmatchでは即時のrevokeしかできないため、証明書の更新時に問題が発生することが予想されます。

この問題も、上記と同様に複数証明書の存在を許容することで解決できるでしょう。

複数ライセンス利用時にコミット先が制約できない問題

複数ライセンスの運用についても洗練されていない部分が目立ちました。

現在のmatchでは、Provisioning Profile作成時の操作ミスにより、1つのブランチに複数のDeveloperライセンスを混在させることができてしまいます。 これにより、不要な証明書が発行されてしまったり、解決が面倒な状態が発生してしまいます。

これは各ブランチにTeam IDを指定するファイルを含んでしまい、別のDeveloperライセンスで作成した証明書のコミットを禁止するなどの機能で対応できそうなので、このような仕組みを提案、実装したいと思っています。

まとめ

このように、iOSアプリのコードサイン周りは非常に複雑で、特殊な訓練や知識が必要になりますし、プロダクト開発において本質的ではない問題が発生しがちな領域です。

全てのアプリ開発者が開発に注力できるよう、今後もfastlaneの開発などを通して、生産性向上へ貢献していきたいと思っています。

クックパッドのモバイル基盤チームでは、アプリ開発者の生産性を向上させたいエンジニアを募集しています。

iOS アプリケーションエンジニア(開発基盤) Android アプリケーションエンジニア(開発基盤)

*1:Professional iOS Code Signing Issue Resolver. fastlaneのauthorである@の役職でもあります

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/