はじめに
iOS開発を行うときにTestFlight配信などのデプロイ作業をCI/CDで自動化したいケースが多いと思いますが、証明書周りで毎回しんどい思いをしています。
作成するだけでもしんどいのに、それをCI/CD環境で使えるようにするのにも一苦労です。
fastlane matchという超便利なツールを発見したときは、証明書周りの負担がかなり軽減されて感動したものですが、それが当たり前になってくると結局fastlane matchのセットアップですら面倒に感じてきます。(欲深くてすみません)
そんなめんどくさがりな私が、Cloud-managed certificatesに出会い、面倒な証明書管理から脱却したので、その内容を記事にしようと思います。
Cloud-managed certificates にするとどうなる?
結論から書くと、iOS の開発でデプロイする時に必要となる配布用証明書(Apple Distribution Certificates)を開発者が管理しなくてもよくなり、全て Apple にお任せできるようになります。
この証明書が何に使うかは、詳しくはこちらを参照すると確認できるかと思います。ここではざっくりと以下の理解で良いと思います。
種類 | 意味 |
---|---|
開発証明書 | 開発の用途で実機でアプリをインストールする時に必要な証明書 |
配布用証明書 | TestFlight の配信やリリースなどで App Store Connect へアプリをアップロードする際に必要な証明書 |
証明書を自分で管理しようとすると、こちらで記載されている通りに、Apple Developerから Provisioning Profile や秘密鍵を作成することになりますが、まずこれが超面倒です。
チームに開発者が複数人いる場合は、作成した Provisioning Profile や秘密鍵を共有したりする必要がありますが、外部に漏れると困るものでもあるので配る方法や管理も面倒です。
前でも少し触れた、fastlen matchでかなり負担が減っているのですが、それでも証明書には有効期限(作成してから 1 年)があり期限切れになると再度作成する必要があるので、その手間はどうしても付きまといますし、セットアップにもそこそこのコストがかかります。
といった具合に、証明書を開発者が管理するのはかなり辛いですが、Cloud-managed certificates で Apple に証明書を管理して貰えば上記で説明したつらみが解消されます。
まず、証明書を作成する手間に関しては、Cloud-managed certificates では Apple 側で自動的に作成してくれるため、開発者が作成する手間はありません。
証明書に有効期限がある問題に関しても、Apple 側で有効期限の 90 日前になると、署名リクエストを受けた時に自動で更新してくれるため、開発者からしてみれば有効期限を意識することはなくなります。
Cloud-managed certificates のデメリット
ちょっと思い当たらないです、、
デメリットが思い当たらないと逆に不安になっちゃいますが、少なくとも開発者が管理している時と比較するとないんじゃないでしょうか?(心当たりある人教えてください)
強いて言うならば、Xcode Cloud を利用した時と比較した時には、Cloud-managed certificates のセットアップの方が手間だと思うので、初期の導入コストはデメリットになるかもしれないですね。
ただ、Xcode Cloudは他のCI/CDプラットフォームと比べると、カスタマイズ性が乏しいようなので(参考)、そういった理由でXcode Cloudを利用できない場合はCloud-managed certificates一択なのでは!と思います。
Cloud-managed certificates を導入するための前準備
Cloud-managed certificates は App Store Connect API(長いので、ここでは ASC API と呼ぶ)を使って Apple 側に ipa の署名などを行ってもらうため、まずは API キーのセットアップを済ませる必要があります。
厳密には ASC API を使わなくても Xcode で Apple Developer アカウントにサインインすることで Xcode 経由で導入することも可能らしいですが、CI などで運用しようとなるとその手は使えないので実運用するなら ASC API を使う手法一択と思っています。
Distribute apps in Xcode with cloud signing
ASC API のセットアップ
Cloud-managed certificatesにはASC API のチームキーが必要になります。
ドキュメントにある通り、チームキーの作成には Admin の権限が必要になるので、注意が必要です。
まず、App Store Connectへログインして、[ユーザーアクセス]->[統合]と移動し、以下赤枠ボタンを押下して、チームキーを作成します。
生成ボタン押下後に、以下画面に記載されている、Issur ID とキー ID、ダウンロードする API キーファイルは外部に漏れないように大切に保管しておいてください。
ASC API を使用するときに、後でどれも必要になります。
Xcode からプロジェクトの Signing 設定を Automatic Signing に設定する
Cloud-managed certificatesを利用するには、Xcode側でも設定が必要です。
Xcode の "Signing&Capabilities" から"Automatically manage signing"にチェックをつけて有効にしてください。
また、Team に自身の Apple Developer の Team を指定します。
こちらの記事でも記載されていますが、Cloud-managed certificates を利用するのに必要なのは配布用証明書による署名を行うときなので、この例であれば、Release Configuration に対してのみ"Automatically manage signing"にチェックをつけるでも問題ありません。
ただ、実際に開発証明書だけ証明書を手動管理したいケースはそうないと思うので、大体は全ての Configuration に対して"Automatically manage signing"を有効にすればいいと思います。
これで、Cloud-managed certificates を導入するための前準備は完了です!
Cloud-managed certificates による TestFlight の配信ワークフロー
続いて、Cloud-managed certificatesがどう機能するのか、CI/CD で Cloud-managed certificates を用いて TestFlight 配信を自動化する例を見ながら確認していきましょう。
環境
今回は Github Actions による CD ワークフローを構築する想定で、解説する
- CI/CD 環境:Github Actions
- Xcode16.2
TestFlihgt 配信ワークフローの yml ファイルの内容
Github Actionsのymlファイル
name: deploy_test_flight
on:
workflow_call:
env:
DEVELOPER_DIR: /Applications/Xcode_16.2.app
WORKSPACE_DIR: <xcodeprojファイルのパスを指定>
BUILD_SCHEME: <対象schemeを指定>
TEST_FLIGHT_EXPORT_OPTION_PATH: <ExportOptions.plistのパスを指定>
ARCHIVE_PATH: <xcarchiveのパスを指定>
EXPORT_PATH: <ipaファイルが作成される親ディレクトリのパスを指定>
SPM_CLONE_PATH: SourcePackages // SPMの成果物が格納されるパスを指定
SPM_PACKAGE_RESOLVED_FILE_PATH: <Package.resolvedのパスを指定>
jobs:
deploy_testFlight:
# 実行環境はmacosに設定
runs-on: macos-latest
env:
API_KEY_PATH: "private_keys/AuthKey_${{ secrets.ASC_KEY_ID }}.p8"
steps:
# チェックアウト(リポジトリからソースコードを取得)
- uses: actions/checkout@v4.2.2
# Xcodeの一覧出力
- name: Show Xcode list
run: ls /Applications | grep 'Xcode'
# Xcodeのバージョン選択
- name: Select Xcode version
run: sudo xcode-select -s '${{ env.DEVELOPER_DIR }}/Contents/Developer'
# Xcodeのバージョン出力
- name: Show Xcode version
run: xcodebuild -version
# ASC_API_KEYのファイルを生成
- name: Setup api key
run: scripts/create_api_key_file.sh "$API_KEY_PATH" private_keys "${{ secrets.ASC_SECRET_KEY }}"
# ASC_API_KEYの絶対パスを環境変数へ設定
- name: Setup env for api key path
run: echo "API_KEY_ABSOLUTE_PATH=$(pwd)/$API_KEY_PATH" >> $GITHUB_ENV
# Gemfileの依存ライブラリキャッシュ
- name: cache bundle directory
uses: actions/cache@v4.2.2
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-${{ hashFiles('Gemfile.lock') }}
# Gemfileの依存ライブラリインストール
- name: Install Gemfile lib
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
# SPMのライブラリのキャッシュ
- name: Cache Swift Packages
uses: actions/cache@v4.2.2
with:
path: SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles(env.SPM_PACKAGE_RESOLVED_FILE_PATH) }}
restore-keys: ${{ runner.os }}-spm-
# ビルド番号更新
- name: Increment build number
run: xcrun agvtool new-version -all ${{ github.run_number }}
# 依存ライブラリ解決
- name: Xcode Resolve Package Dependencies
run: set -o pipefail &&
xcodebuild
-project "${WORKSPACE_DIR}"
-scheme "${BUILD_SCHEME}"
-configuration Release
-clonedSourcePackagesDirPath SourcePackages
-skipPackagePluginValidation
-skipMacroValidation
-scmProvider xcode
-resolvePackageDependencies
# アーカイブ実行
- name: Archive app
run: set -o pipefail &&
xcodebuild
-clonedSourcePackagesDirPath $SPM_CLONE_PATH
-configuration Release
-scheme "${BUILD_SCHEME}"
-project "${WORKSPACE_DIR}"
-archivePath "${ARCHIVE_PATH}"
-authenticationKeyPath "${{ env.API_KEY_ABSOLUTE_PATH }}"
-authenticationKeyID "${{ secrets.ASC_KEY_ID }}"
-authenticationKeyIssuerID "${{ secrets.ASC_ISSUER_ID }}"
-allowProvisioningUpdates
clean archive
# IPAファイルを生成
- name: Create ipa
run: set -o pipefail &&
xcodebuild
-exportArchive
-archivePath "${ARCHIVE_PATH}"
-exportPath "${EXPORT_PATH}"
-authenticationKeyPath "${{ env.API_KEY_ABSOLUTE_PATH }}"
-authenticationKeyID "${{ secrets.ASC_KEY_ID }}"
-authenticationKeyIssuerID "${{ secrets.ASC_ISSUER_ID }}"
-allowProvisioningUpdates
-exportOptionsPlist "${TEST_FLIGHT_EXPORT_OPTION_PATH}"
# IPAをApp Store Connectへアップロードする
- name: Upload ipa
run: |
chmod -R 777 "${EXPORT_PATH}"
IPA_PATH=$(find "${EXPORT_PATH}" -name "*.ipa" -print -quit)
fastlane upload_testFlight asc_key_id:"${{ secrets.ASC_KEY_ID }}" asc_issuer_id:"${{ secrets.ASC_ISSUER_ID }}" asc_key_path:"$API_KEY_PATH" ipa:"$IPA_PATH"
create_api_key_file.sh
#!/bin/bash
# 引数チェック
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <path> <base_dir> <api_key_base64>"
exit 1
fi
# 引数の割り当て
API_KEY_FILE_PATH="$1"
BASE_DIR="$2"
API_KEY_BASE_64="$3"
# ASCのAPIキーファイル生成処理を行う
mkdir -p $BASE_DIR
echo "$API_KEY_BASE_64" | base64 --decode > "$API_KEY_FILE_PATH"
chmod 600 "$API_KEY_FILE_PATH"
Fastfile
default_platform(:ios)
platform :ios do
desc "指定されたipaでTestFlight配信を行う"
lane :upload_testFlight do |options|
api_key = generate_api_key(options)
ipa_path = options[:ipa]
upload_to_testflight(
ipa: ipa_path,
api_key: api_key
)
end
private_lane :generate_api_key do |options|
app_store_connect_api_key(
key_id: options[:asc_key_id],
issuer_id: options[:asc_issuer_id],
key_filepath: options[:asc_key_path],
in_house: false
)
end
end
以下から、TestFlight 配信に関わる要点を説明していきます。
CI 環境で TestFlight 配信を行うには、ざっくり以下ステップを踏む必要があります。
- リポジトリのチェックアウト
- ASC API キーファイルを CI/CD 環境へ配置する
- ビルド番号をインクリメントする
- アーカイブを生成する
- ipa を生成する
- ipa を App Store Connect へアップロード
※リポジトリのチェックアウトは Github Actions を扱う上での基本的なステップなので、ここでは説明を割愛します。
ASC API キーファイルを CI/CD 環境へ配置する
Cloud-managed certificates で TestFlight を配信する際にxcodebuild
コマンドで ipa の生成や App Store Connect へのアップロードを行う際に ASC API キーのファイルが必要になるため、先にファイルを CI/CD 環境へ配置します。
ただ先述した通り ASC API キーファイルは秘匿情報であり外部に漏れると困るので、Github Actions の secret variable として登録して、${{ secrets.ASC_SECRET_KEY }}
のようにsecrets
から参照して取得するようにします。
secret variable の登録方法は、こちらを参照していただきたい。
ここで登録する内容は、secrets.ASC_SECRET_KEY
と参照したいので、変数名はASC_SECRET_KEY
として、内容は base64 にエンコードした ASC API キーにします。
base64 にエンコードした ASC API キーを取得するには、以下コマンドをローカルで実行すればOKです。
base64 < <ASC KEYファイルのパス>
CI/CD 環境で ASC API キーファイルを生成ステップの内容が以下です。
処理簡略化のために自前のスクリプトを組んで、このステップではそのスクリプトを呼び出しています。
# ASC_API_KEYのファイルを生成
- name: Setup api key
run: scripts/create_api_key_file.sh "$API_KEY_PATH" private_keys "${{ secrets.ASC_SECRET_KEY }}"
スクリプトの内容は単純で、base64 にエンコードされている API キーファイルの内容をデコードして指定のパスにファイルとして保存しているだけです。
#!/bin/bash
# 引数チェック
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <path> <base_dir> <api_key_base64>"
exit 1
fi
# 引数の割り当て
API_KEY_FILE_PATH="$1"
BASE_DIR="$2"
API_KEY_BASE_64="$3"
# ASCのAPIキーファイル生成処理を行う
mkdir -p $BASE_DIR
echo "$API_KEY_BASE_64" | base64 --decode > "$API_KEY_FILE_PATH"
chmod 600 "$API_KEY_FILE_PATH"
またこのステップの後に、生成した ASC API キーファイルの絶対パスを取得するステップを挟んでいます。
xcodebuild
コマンドの-authenticationKeyPath
で ASC API キーファイルのパスを設定できますが、絶対パスで指定しないとエラーになってしまうため後々必要になります。(なんで相対パスを受けないのかは謎、、)
# ASC_API_KEYの絶対パスを環境変数へ設定
- name: Setup env for api key path
run: echo "API_KEY_ABSOLUTE_PATH=$(pwd)/$API_KEY_PATH" >> $GITHUB_ENV
ちなみに$GITHUB_ENV
にhoge=value
みたいな式を追記することで、後続のステップでは${{ env.hoge }}
のようにvalue
の値を参照できるようになります。
ビルド番号をインクリメントする
App Store Connect へアップロードする時に、制約がありすでにアップロード済みのビルドの中に、同一のアプリバージョン(CFBundleShortVersionString)がある場合、ビルド番号(CFBundleVersion)はアップロード済みの同一のアプリバージョンを持つビルド番号より大きいビルド番号である必要があります。
この制約の内容はこちらの記事でわかりやすく説明されていました!
以下ステップで、agvtool
コマンドを使ってプロジェクトのビルドバージョンを指定したバージョンに更新しています。
github.run_number
は Github Actions のワークフローが走るたびにインクリメントされるので、CI/CD でのみ App Store Connect へアップロードする場合は役に立ちます。
# ビルド番号更新
- name: Increment build number
run: xcrun agvtool new-version -all ${{ github.run_number }}
ただビルド番号の形式などにこだわりがなければ、Xcode 側で自動で App Store Connect の一番新しいビルド番号を確認してインクリメントしてくれる機能もあります。
ここでは詳細は割愛しますが、以下 WWDC のビデオでその話が触れられていたので、気になる方は見てみてください。
アーカイブを生成する
xcodebuild
コマンドでアーカイブの生成を行います。
こちらの記事でもある通り、通常であれば開発証明書を求められるが、App Store Connect へ ipa をアップロードするのに必要なのは ipa 生成時に配布証明書で署名だけなので、CODE_SIGNING_REQUIRED="NO"
と CODE_SIGNING_ALLOWED="NO"
で開発証明書による署名をスキップします。
# アーカイブ実行
- name: Archive app
run: set -o pipefail &&
xcodebuild clean archive
-clonedSourcePackagesDirPath $SPM_CLONE_PATH
-configuration Release
-scheme "${BUILD_SCHEME}"
-project "${WORKSPACE_DIR}"
-archivePath "${ARCHIVE_PATH}"
CODE_SIGNING_REQUIRED="NO"
CODE_SIGNING_ALLOWED="NO"
ipa を生成する
前のステップで生成したアーカイブ(.xcarchive フォルダ)を使って、ipa ファイルを生成していきます。
ipa 生成時もアーカイブ生成と同じくxcodebuild
コマンドで行います。
# IPAファイルを生成
- name: Create ipa
run: set -o pipefail &&
xcodebuild
-exportArchive
-archivePath "${ARCHIVE_PATH}"
-exportPath "${EXPORT_PATH}"
-authenticationKeyPath "${{ env.API_KEY_ABSOLUTE_PATH }}"
-authenticationKeyID "${{ secrets.ASC_KEY_ID }}"
-authenticationKeyIssuerID "${{ secrets.ASC_ISSUER_ID }}"
-allowProvisioningUpdates
-exportOptionsPlist "${TEST_FLIGHT_EXPORT_OPTION_PATH}"
ここで ASC API を使って Apple のクラウドで管理している配布用証明書を使って署名するので、ASC API を使用するための認証情報をオプションで指定します。
オプション | 指定する値 |
---|---|
-authenticationKeyPath |
前に作成した ASC API キーファイルの絶対パス |
-authenticationKeyID |
ASC API キー ID |
-authenticationKeyIssuerID |
ASC API の IssuerID |
ASC API キー ID と ASC API の IssuerID は ASC API キーセットアップ時に控えたものを設定します。
以下のオレンジ色の部分ですね。
こちらも先述した通り、秘匿情報なので、ASC API キーファイルと同様に Github Actions の secret variable として設定しておきましょう。(設定方法は ASC API キーファイルの時と同じなので省略する)
もう一つ ipa を作成する上で大事な事項として、ExportOption を指定して、どういう用途の ipa を作成するのかを指定する必要があります。
例えば、App Store へ配信するのか、TestFlight へ配信するのかをここで指定します。
ExportOption は plist 形式で表現され、ipa 作成時のxcodebuild
コマンドでは、-exportOptionsPlist
で plist のファイルパスを指定できます。
肝心の plist の内容は、こちらの記事でわかりやすく取得方法が記載されています。
参考として、私は以下のような plist を使用しています。
<?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>generateAppStoreInformation</key>
<false/>
<key>manageAppVersionAndBuildNumber</key>
<false/>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string><自身のアカウントのteamId></string>
<key>testFlightInternalTestingOnly</key>
<true/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
ipa を App Store Connect へアップロード
ようやく ipa の生成までできたので、App Store Connect へアップロードします。
ipa のアップロードは、Apple 標準のツールでaltool
コマンドがありますが、アプリバージョンを明示的に指定する必要があったりと使い勝手が悪いので Fastlane を使用しました。
# IPAをApp Store Connectへアップロードする
- name: Upload ipa
run: |
chmod -R 777 "${EXPORT_PATH}"
IPA_PATH=$(find "${EXPORT_PATH}" -name "*.ipa" -print -quit)
fastlane upload_testFlight
asc_key_id:"${{ secrets.ASC_KEY_ID }}"\
asc_issuer_id:"${{ secrets.ASC_ISSUER_ID }}" \
asc_key_path:"$API_KEY_PATH" \
ipa:"$IPA_PATH"
default_platform(:ios)
platform :ios do
desc "指定されたipaでTestFlight配信を行う"
lane :upload_testFlight do |options|
api_key = generate_api_key(options)
ipa_path = options[:ipa]
upload_to_testflight(
ipa: ipa_path,
api_key: api_key
)
end
private_lane :generate_api_key do |options|
app_store_connect_api_key(
key_id: options[:asc_key_id],
issuer_id: options[:asc_issuer_id],
key_filepath: options[:asc_key_path],
in_house: false
)
end
end
Fastlane のセットアップ方法などは、本記事の本題ではないので割愛しますが、以前 Fastlane での TestFlight 配信の方法を記事にしているので、よければこちらを見てみてください!
以上でCloud-managed certificatesでのTestFlight配信のワークフローは完成です!
感想
Cloud-managed certificates を使ってみて、やはり証明書の管理にかかるコストがなくなると最初の開発基盤を整えるコストはかなり抑えられるなと感じました。
fastlane match を使っていた場合だと、最初に手動で証明書を作成して、証明書を格納するリポジトリを作ってと、それだけでもかなり手間ですが、Cloud-managed certificates だと最初に ASC API のセットアップを終えるとそれを使うだけで良くなりますし、有効期限で再作成しなきゃと頭の片隅でずっと思っていなきゃいけないストレスからも解放されました。
総じて、手動管理よりも楽ちんだなと感じました!
おわり
以上が、Cloud-managed certificates を使って証明書の管理から脱却方法です。
今回は TestFlight の自動配信までを取り上げたが、App Store への配信に関しても、今回の例をほぼそのまま転用できると思もいます。(ExportOption を変えてアップロードするときに fastlane deliver を使うようにする)
この記事が誰かの参考になれば嬉しいです!
記事の内容で誤りがあれば是非コメントいただけると幸いです!