こんにちは、あらさんです。ポケモンSV最高です。
今回は面倒なXcodeの配布のための証明書をXcode13から提供されているCloud-managed certificatesを利用して自分たちの管理から脱却する内容です。
この記事は既に開発中のアプリを対象に書いています。新規でiOSアプリを作る場合はXcode Cloudを使うと一番はやいです。特に悩む部分もなくXcodeをポチポチするだけでTestflightへのデプロイまで終わります。Flutterを利用している場合は公式リファレンスを読んでCodemagic等でAndroid/iOS両方をセットアップすることが一番はやいと思います。
現在の痛み
色々な証明書のFastlane Matchを利用したGithubでのホスティングによる手動管理、プロジェクトにあふれるsigningStyleのManualをなんとかしてAutomaticに書き換えたい。
Fastlane Matchを利用することでCIでの証明書管理は飛躍的に快適になった、しかし管理するコストは変わらず更新作業などは発生する。Bitkeyでは様々なプロダクトを扱うためできる限り管理作業を自動化することにメリットがある。この現状を打破したい、というモチベーションが原点となる。
また、特定のCI/CD製品(弊チームではBitrise)に依存していることから抜本的脱却をして、どこで構築しても同じコードが使えることを目指す。
Fastlane Matchを利用して管理している現状
現状はgitにcertificate周りの使うものをまとめてFastlane matchを利用して管理している。
これについての詳しい内容はこちら。
使っていない人に向けてFastlane Matchをざっくり解説
FastlaneはGoogleが開発している開発の自動化をしてくれるスゴいツールである。例えば一般的に継続的デリバリーの仕組みを整える場合にはビルドバージョンを更新して、ビルドして、アップロードするプロセスがある。これをfastlaneの設定ファイルを記述して実行するだけで全自動化してくれるもの。
ただし上記で書いているものはXcodeのコマンドラインツールでも実現できる、必ず採用しなければいけないものではない。ビルドバージョンの更新ではagvtoolがあり、ビルドはxcodebuildがあり、アップロードにはaltoolがある。これは素直に利用できる。
しかしFastlaneにはXcode12まで採用する圧倒的な理由があった、それがFastlane Matchの存在である。Fastlaneの便利な仕組みにCode Signingを楽にしてくれるツールがついているため大変助かった。Xcode 13からは配布証明書を今回の話の中心であるCloud-managed certificatesにより管理できることからxcodebuildコマンドのみで良くなったため、移行することとした。
Xcode13からのCloud-managed certificatesで管理する未来
App store connect内部で証明書を管理する新しい仕組み、メリットは上記の期限切れが実質なくなること。管理が楽になる。
今までと違う部分に、ローカルで管理する必要があった証明書をApple側が持ってくれて、アーカイブ時に署名するタイミングで差し込んでくれるようになる。
Fastlane matchとの対比
Fastlane match | Xcode cloud-managed | |
---|---|---|
証明書の運用 | 配布証明書の作成が必要、更新期限が来たら更新作業が必要 | Appleが配布証明書を管理する、更新期限が来たら勝手に更新する。 |
証明書の管理 | 今までの開発環境では必ずローカルに証明書を持つ必要があったため、中央集権で管理できるような仕組みをmatchが整えた | Apple側が証明書を保持して署名までできる仕組みを整えたため開発者側で見る必要がなくなった |
ざっくりこんな感じに変わる
allowProvisioningUpdates
をxcodebuildのコマンドに適用するとApp store connectとの通信をするようになる。
証明書の管理とローテーション
新しい署名要求が受信されると、有効期限が切れる 90 日前に新しいクラウドマネージド証明書が自動的に作成されます。ソフトウェア署名要求を受信すると、最新の証明書が使用されます。アカウント所有者と管理者は、署名されたソフトウェアをデバイスで 90 日以上実行する必要がある場合、証明書、識別子、およびプロファイルで証明書のローテーションを開始することもできます。証明書の残りの有効期間 (多くの場合 180 日) が半分未満になると、手動で証明書をローテーションできます。
利用するためには、それぞれの開発者のアカウントに対してCloud-managed certificatesの利用の許可をする必要がある、これはAccount Holder, AdminがWeb画面で操作する。
また開発者のアカウントでCloud-managedを有効にすることの他に、App store connect APIを利用してコマンドラインでの利用もできる。ただしApp store connect APIのキーのロールはAdminが必要(細かな権限設定がないのでこれはセンシティブな扱いになりそう?)。
- 開発者個人のローカル端末でビルドしてデプロイする場合には、その開発者のアカウントにCloud-managed certificatesの利用の許可が必要。
- BitriseなどのCIでCloud-managed certificatesを使う場合には、App store connect APIを利用する方が楽なためApp store connect API Key(.p8)のAdmin Roleを発行して利用する
AppStoreConnect上で操作する
- AdminRoleのアカウントで他開発者の詳細画面を開くと「クラウド管理配布証明書へのアクセス」にチェックマークをつけて保存する。
- これを実行したとしてもXCode上での変化は見受けられないので注意
- DeveloperRoleの場合は下記の手順が必要になるので要注意
From a Admin Role perspective, this is incomprehensible and leads to guesswork for some. But the following led to the goal:
Developer Role: close Xcode
Admin Role: change the role of the user from "developer" to "admin" temporarily
Developer Role: restart Xcode and start the validation/distribution process again. it should run successfully
Developer Role: close Xcode
Admin Role: set the user's role back to "developer".
Developer Role: restart Xcode and start the validation/distribution process again. it should run successfully
あらさんのXcodeでarchiveしてOrganizerからuploadをした結果得られた出力を貼り付ける。
SUMMARY
Team: [REDACTED]
Certificate: Cloud Managed Apple Distribution (Expires [REDACTED])
Profile: iOS Team Store Provisioning Profile: [REDACTED] (Expires [REDACTED])
Symbols: Included
Architectures: arm64
Version: [REDACTED] ([REDACTED])

ENTITLEMENTS
[REDACTED]
get-task-allow
false
com.apple.developer.associated-domains
applinks:[REDACTED]
com.apple.developer.team-identifier
[REDACTED]
application-identifier
[REDACTED]
Organizerが内部的に利用したexportOptions.plistはこれ、記載されるkeyの内容は xcodebuild -help
に記載されている。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>upload</string>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>[REDACTED]</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
実践・CD構築
この内容ではGithub ActionsやBitrise, CircleCIなど特定の機能を利用しない。
今回はBitriseを利用した方法で実現するが以下の流れの1,2をそれぞれのCIで動くように変更するとどの環境でも動くはず、またローカルでも同じ方法で動く。実際にCLIで純粋に構築する場合には考える内容が多いため適宜注釈を付けつつ流れを抑える。
全体の流れは以下の通り。
- BitriseにAPIKey(.p8)、Key ID, Issuer IDを登録する
- Bitriseのワークフローで登録したKey ID, Issuer ID, PrivateKeyを取得する
- ビルド番号を一番大きいものに変更する
- 署名なしでアーカイブ(.xcarchiveを生成)する
- APIキーを利用してipaを生成する
- altoolを用いてAPIキーでAppStoreConnectに送信する
1. BitriseにAPIKey(.p8)、Key ID, Issuer IDを登録する
AppStoreConnectでキーを生成する。
💡 Cloud-managed Signingする場合には APIキーはAdminとして作る必要がある ことに注意。
APIキーを管理するアカウントにAPIキー情報を格納する。
格納場所はProfile Settings → Apple Service Connection → API Keys → Add API Keysで開ける。
APIキーをAppのIntegrationで紐付ける。
- アプリの設定からIntegrationでキーを紐付ける。
2. Bitriseのワークフローで登録したKey ID, Issuer ID, PrivateKeyを取得する
BitriseのワークフローでProfile Settingsに登録したキー情報を直接取得するスクリプトを記述する。
Apple Service Connectionで登録した情報は $BITRISE_BUILD_URL/apple_developer_portal_data.json
にjsonとして配置されている、そのためcurlでそのまま取得して良い。
💡 $BITRISE_BUILD_API_TOKEN
はワークフローの実行時に環境変数として渡されるもの、headerに BUILD_API_TOKEN
をキーとして渡すことを要求している。
一通り生成ができたら後続のscriptで環境変数として扱うためにbitriseが提供している envman
を使って登録する。
# Bitrise.yml
# workflowなどの構造は省略
steps:
- script:
title: Fetch Apple Connection Credentials
inputs:
- content: |
#!/bin/bash
curl -H "BUILD_API_TOKEN: $BITRISE_BUILD_API_TOKEN" -o apple_credentials.json $BITRISE_BUILD_URL/apple_developer_portal_data.json
# altoolを利用する場合、private_keys/AuthKey_${API_KEY_ID}.p8に
# キーがあることを要求するためディレクトリを作成
mkdir private_keys
API_KEY_ID=$(cat apple_credentials.json | jq -r '.key_id')
API_KEY_ISSUER_ID=$(cat apple_credentials.json | jq -r '.issuer_id')
API_KEY_PATH="private_keys/AuthKey_${API_KEY_ID}.p8"
cat apple_credentials.json | jq -r '.private_key' > $API_KEY_PATH
echo -n "$API_KEY_PATH" | envman add --key API_KEY_PATH
echo -n "$API_KEY_ID" | envman add --key API_KEY_ID
echo -n "$API_KEY_ISSUER_ID" | envman add --key API_KEY_ISSUER_ID
3. ビルド番号を一番大きいものに変更する
APIキーを利用してAppStoreConnectに送信するためにはaltoolを経由する必要がある。そしてaltoolを利用する場合には manageAppVersionAndBuildNumber
が利用できない。
💡 これを利用する場合には直接exportArchiveによりuploadをするオプションも存在するがAppleIDを要求するためAPIKeyだけで完結させる場合には難しい。
今回はagvtoolを利用してビルド番号をBitriseのワークフロー番号に書き換えている。
- script:
title: Update build number
inputs:
- content: |
#!/bin/bash
xcrun agvtool new-version -all $BITRISE_BUILD_NUMBER
4. 署名なしでアーカイブ(.xcarchiveを生成)する
今回の肝はここ、archiveする段階で何もせずにxcodebuildコマンドに allowProvisioningUpdates
を渡すと開発証明書を要求する。Cloud-managed certificatesでは配布証明書の自動管理にフォーカスしているためそのままarchiveすることは証明書がないことから署名できないため失敗してしまう。開発証明書を自前で管理してもよいが、ここで署名のタイミングについて考える。署名はipaを作成する段階で配布証明書によりされる、そのため開発証明書で署名することはこの時点で必要性がない。そこでオプションとして CODE_SIGNING_REQUIRED, CODE_SIGNING_ALLOWED
を用いて開発証明書による署名ステップを飛ばすことにする。そうすると署名スキップされるため開発証明書は管理しなくて良い。
💡 署名は必要ではないし許可もしないとするとスキップされるっぽい(要検証)。
xcodebuild \
archive \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
-workspace Root.xcworkspace \
-scheme "$DEPLOY_TARGET" \
-configuration Release \
-archivePath "$XCARCHIVE_PATH"
5. APIキーを利用してipaを生成する
xcodebuildコマンドの exportArchive
オプションを利用するとOrganizerの処理をCLIで実行できる。
-
archivePath
は入力されるxcarchiveを指定する。 -
exportPath
はipaの出力先のディレクトリを指定する。 -
exportOptionsPlist
はどのように処理するのかの設定を.plistファイルとして入力する。
xcodebuild \
-exportArchive \
-archivePath "$XCARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-authenticationKeyPath "$API_KEY_PATH" \
-authenticationKeyID "$API_KEY_ID" \
-authenticationKeyIssuerID "$API_KEY_ISSUER_ID" \
-allowProvisioningUpdates \
-exportOptionsPlist "$API_EXPORT_PLIST_PATH"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>[REDACTED]</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
ipaを作成した段階でどのように作られたかを表すDistributionSummary.plist
が同じディレクトリに生成される。これを見るとどの証明書を利用したのか分かる。ここでCloud managed Apple Distribution
が記載されていると証明書管理から無事開放されたことになる。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>[REDACTED]</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>bitcode</key>
<false/>
<key>buildNumber</key>
<string>[REDACTED]</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>[REDACTED]</string>
<key>dateExpires</key>
<string>[REDACTED]</string>
<key>type</key>
<string>Cloud Managed Apple Distribution</string>
</dict>
<key>entitlements</key>
<dict>
<key>application-identifier</key>
<string>[REDACTED]</string>
<key>beta-reports-active</key>
<true/>
<key>com.apple.developer.team-identifier</key>
<string>[REDACTED]</string>
<key>get-task-allow</key>
<false/>
</dict>
<key>name</key>
<string>[REDACTED]</string>
<key>profile</key>
<dict>
<key>UUID</key>
<string>[REDACTED]</string>
<key>dateExpires</key>
<string>[REDACTED]</string>
<key>name</key>
<string>iOS Team Store Provisioning Profile: [REDACTED]</string>
</dict>
<key>symbols</key>
<true/>
<key>team</key>
<dict>
<key>id</key>
<string>[REDACTED]</string>
<key>name</key>
<string></string>
</dict>
<key>versionNumber</key>
<string>[REDACTED]</string>
</dict>
</array>
</dict>
</plist>
6. altoolを用いてAPIキーでAppStoreConnectに送信する
ipaファイルを指定してaltoolに渡す。
💡 p8キーの存在はコマンドに現れないが暗黙的にコマンドを実行しているディレクトリで private_keys/AuthKey_<keyid>.p8
の形式で配置されていることを要求している。
# Required private_keys/AuthKey_<keyid>.p8
xcrun altool \
--upload-app \
-f "$EXPORT_PATH/`ls $EXPORT_PATH | grep .ipa`" \
-t ios \
--apiKey "$API_KEY_ID" \
--apiIssuer "$API_KEY_ISSUER_ID"
ローカル環境で実行する場合のTips
ローカル環境でXcodeの設定が済んでいる場合にはAPIは必要なく、以下のコマンドと送信する情報を表すexportOptions.plistで提出までできる。
💡 前提として利用するAppleIDがクラウド管理配布証明書へのアクセスの権限を付与されていることに注意。
xcodebuild \
archive \
-workspace Root.xcworkspace \
-scheme "$DEPLOY_TARGET" \
-configuration Release \
-archivePath "$XCARCHIVE_PATH" \
-allowProvisioningUpdates
xcodebuild \
-exportArchive \
-archivePath "$XCARCHIVE_PATH" \
-allowProvisioningUpdates \
-exportOptionsPlist "$ID_EXPORT_PLIST_PATH"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>upload</string>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>[TEAM ID HERE]</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>