はじめに
iOSアプリのリリース作業、毎回ローカルのXcodeからArchive → Distribute... と手動で行っていませんか?
この作業は時間がかかる上に、ビルド環境が属人化しやすく、「自分のMacだとビルドできるのに、他の人のMacだと失敗する」といった問題の温床になります。
本記事では、GitHub Actions を使ってiOSアプリをビルド・署名し、TestFlightへ自動アップロードする 一連のパイプラインを構築する手順を解説します。
よくある解説記事では fastlane を使った構成が紹介されていますが、本記事では fastlane を使わず、Apple純正の xcodebuild と xcrun altool だけで完結させる構成 を採用します。Ruby環境を持ち込まなくて済むので、ワークフローがシンプルになり、Xcode更新時のfastlane追従問題からも解放されます。
認証方式には、Apple ID + パスワードではなく、より安全で2FAの影響を受けない App Store Connect APIキー を採用します。
前提条件
- Apple Developer Program に登録済み(有償)
- App Store Connect にアプリのレコードが作成済み
- GitHubリポジトリにiOSプロジェクトが push 済み
- macOS / Xcode でローカルビルドが通ることを確認済み
全体の流れ
構築するパイプラインの全体像は次のとおりです。
mainブランチへの push または手動トリガーでワークフロー起動- GitHub Actions の macOS runner 上で Xcode を準備
- 証明書とプロビジョニングプロファイルを一時的にキーチェーンへインポート
- ビルド番号を更新
xcodebuild archiveで.xcarchiveを生成xcodebuild -exportArchiveで.ipaにエクスポートxcrun altoolで TestFlight へアップロード- 一時キーチェーンを削除してクリーンアップ
push → Actions起動 → 証明書インストール → archive → export → altool → TestFlight
Step 1: 証明書とプロビジョニングプロファイルを準備する
CIでコード署名を行うには、以下の2つのファイルをBase64エンコードしてGitHub Secretsに格納します。
1-1. 配布用証明書(.p12)をエクスポート
ローカルの「キーチェーンアクセス.app」を開き、Apple Distribution または iPhone Distribution 証明書を右クリックして .p12 形式で書き出します。書き出す際に設定するパスワードは後で使うので控えておきます。
証明書が無い場合は Apple Developer サイトの Certificates から新規作成してください。
1-2. プロビジョニングプロファイル(.mobileprovision)をダウンロード
Apple Developer サイトの Profiles から、App Store 配布用のプロビジョニングプロファイルをダウンロードします。このとき、プロファイル名(例: YourApp AppStore)も必ず控えておいてください。後で ExportOptions.plist に記述します。
1-3. Base64にエンコード
ターミナルで次のコマンドを実行し、ファイルをBase64文字列化します。
# 証明書
base64 -i Certificates.p12 | pbcopy
# プロビジョニングプロファイル
base64 -i YourApp_AppStore.mobileprovision | pbcopy
pbcopy によってクリップボードにコピーされるので、このままGitHub Secretsに貼り付けられます。
Step 2: App Store Connect APIキーを発行する
2要素認証に振り回されないために、APIキーを使います。
- App Store Connect にログイン
- 「ユーザーとアクセス」→「Integrations」→「App Store Connect API」を選択
- 「+」ボタンからキーを生成(ロールは App Manager 以上)
- ダウンロードした
AuthKey_XXXXXXXXXX.p8を保存(再ダウンロード不可なので注意) - 発行時に表示される Key ID と Issuer ID を控える
.p8 ファイルも同様にBase64化しておきます。
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
Step 3: GitHub Secretsに登録する
リポジトリの Settings → Secrets and variables → Actions から、以下のSecretsを登録します。
| Secret名 | 中身 |
|---|---|
BUILD_CERTIFICATE_BASE64 |
.p12証明書のBase64文字列 |
P12_PASSWORD |
.p12エクスポート時に設定したパスワード |
BUILD_PROVISION_PROFILE_BASE64 |
.mobileprovisionのBase64文字列 |
KEYCHAIN_PASSWORD |
一時キーチェーン用の任意のパスワード |
APP_STORE_CONNECT_API_KEY_ID |
APIキーのKey ID |
APP_STORE_CONNECT_API_ISSUER_ID |
Issuer ID |
APP_STORE_CONNECT_API_KEY_BASE64 |
.p8のBase64文字列 |
Step 4: ExportOptions.plist を作成する
.xcarchive から .ipa を生成するときに必要な設定ファイルです。リポジトリに ios/ExportOptions.plist として commit しておきます(機密情報は含まれません)。
<?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>method</key>
<string>app-store</string>
<key>teamID</key>
<string>ABCD123456</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.yourapp</key>
<string>YourApp AppStore</string>
</dict>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>
Step 5: GitHub Actions ワークフローを作成する
いよいよ本丸です。.github/workflows/ios-testflight.yml を作成します。
name: iOS TestFlight Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
SCHEME: YourApp
WORKSPACE: YourApp.xcworkspace # .xcodeproj の場合は PROJECT を使用
CONFIGURATION: Release
EXPORT_OPTIONS_PLIST: ios/ExportOptions.plist
jobs:
deploy:
runs-on: macos-14
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Base64をデコード
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# 一時キーチェーンを作成
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# 証明書をインポート
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# プロビジョニングプロファイルを配置
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Set build number
run: |
# GitHub Actions の実行番号をビルド番号に利用(一意性が保証される)
BUILD_NUMBER=${{ github.run_number }}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" \
YourApp/Info.plist
echo "Build number set to $BUILD_NUMBER"
- name: Build archive
run: |
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-destination "generic/platform=iOS" \
-allowProvisioningUpdates \
CODE_SIGNING_ALLOWED=YES \
| xcbeautify
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
-exportPath "$RUNNER_TEMP/export" \
| xcbeautify
- name: Prepare API key for altool
env:
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
run: |
# altool は ~/.appstoreconnect/private_keys/ または ./private_keys/ から読む
mkdir -p ~/.appstoreconnect/private_keys
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode \
-o ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "$RUNNER_TEMP/export/YourApp.ipa" \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID"
- name: Clean up keychain and profiles
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
rm -rf ~/.appstoreconnect/private_keys
ポイント解説
ビルド番号に github.run_number を使う
fastlane の increment_build_number が不要になる代わりに、GitHub Actionsが自動採番する実行番号を利用しています。単調増加が保証されており、TestFlight が要求する「ビルド番号の重複不可」要件を満たせます。日付ベース ($(date +%Y%m%d%H%M)) でも構いません。
xcbeautify でログ整形
xcodebuild の生ログは非常に読みにくいので、xcbeautify でパイプしています。macos-14 runner には最初からインストール済みです。もし入っていなければ brew install xcbeautify を1ステップ追加してください。
APIキーファイルの配置場所
xcrun altool は以下のいずれかのパスからAPIキーを自動検出します。明示的にパス指定するフラグは無いので、所定のディレクトリに置く必要があります。
./private_keys/AuthKey_<KEY_ID>.p8~/private_keys/AuthKey_<KEY_ID>.p8~/.private_keys/AuthKey_<KEY_ID>.p8~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
一時キーチェーンを使う理由
CI環境のデフォルトキーチェーンに証明書を突っ込むと、後片付けが面倒な上にセキュリティ的にもよろしくありません。ジョブごとに作って捨てるのがベストプラクティスです。
if: always() で後片付け
ビルドが失敗しても必ず一時キーチェーンと鍵ファイルを削除するようにしています。self-hosted runner を使う場合は特に重要です。
Step 6: 動作確認
main ブランチにpushするか、GitHubの Actions タブから Run workflow で手動実行しましょう。
5〜10分ほどで完了し、App Store Connect の TestFlight タブに新しいビルドが表示されます。処理中 (Processing) ステータスから完了するまでさらに10〜30分ほどかかることがあります。
証明書管理の選択肢と推奨構成
ここまで .p12 を Base64 にエンコードして GitHub Secrets に登録する方式で構築しましたが、GitHub Actions には他にも証明書管理の選択肢があります。チーム規模や要件に応じて最適なものを選んでください。
選択肢1: Base64 で Secrets に格納(本記事の方式)
仕組み: .p12 と .mobileprovision を Base64 エンコードして GitHub Secrets に文字列として保存。
メリット
- 外部依存がなく、GitHubだけで完結
- 初期セットアップが最も簡単
.p12は通常数KB〜十数KBなので、Secrets の64KB制限に余裕で収まる
デメリット
- 証明書ローテーション時は手動で Secrets 更新が必要
- 複数アプリを扱う場合、Secrets の数が増えて管理が煩雑になる
- Secrets はファイルとして扱えないので Azure DevOps の Secure Files のような感覚では使えない
向いているケース: 個人開発、1〜3人程度の小規模チーム、1アプリのみ
選択肢2: 暗号化したファイルをリポジトリに commit
仕組み: .p12 を GPG や OpenSSL で暗号化してリポジトリにコミットし、復号パスフレーズだけを Secrets に置く。
# ローカルで暗号化
gpg --symmetric --cipher-algo AES256 Certificates.p12
# → Certificates.p12.gpg をリポジトリにコミット
ワークフローでは:
- name: Decrypt certificate
run: |
gpg --quiet --batch --yes --decrypt \
--passphrase="$CERT_PASSPHRASE" \
--output $RUNNER_TEMP/cert.p12 \
secrets/Certificates.p12.gpg
env:
CERT_PASSPHRASE: ${{ secrets.CERT_PASSPHRASE }}
メリット
- ファイルとして管理でき、Git の履歴でローテーションが追える
- Secrets のサイズ制限から解放される
- Azure DevOps の Secure Files に一番近い感覚
デメリット
- リポジトリを持ち出された場合、パスフレーズの強度が最後の砦
.gpgファイルがリポジトリに並ぶので見た目的に好みが分かれる
向いているケース: Azure DevOps からの移行、ファイルとして管理したい、プライベートリポジトリでの運用
選択肢3: fastlane match
仕組み: 専用のプライベートリポジトリ(または S3 / GCS)に暗号化した証明書群を置き、fastlane が自動で取得・インストール・復号までやってくれる。
メリット
- 複数アプリ・複数環境(dev/adhoc/appstore)の証明書をまとめて管理できる
- 新メンバーが
fastlane matchを1回叩くだけでローカル環境も整う - 証明書の自動再生成にも対応
デメリット
- Ruby と fastlane の導入が必要
- Xcode 更新時の fastlane 追従問題の可能性
- 脱fastlaneしたい本記事の趣旨とは逆行
向いているケース: 複数アプリ運用、5人以上のiOSチーム、証明書を頻繁にローテーションする
選択肢4: AWS Parameter Store + OIDC連携
仕組み: AWS Systems Manager Parameter Store に .p12 のBase64文字列とパスワードを SecureString として保存し、GitHub Actions から OIDC 経由で取得。長期クレデンシャルが不要なのが大きなポイントです。
Parameter Store と Secrets Manager の使い分け
AWS には似たサービスが2つあるため整理しておきます。
| 項目 | Parameter Store | Secrets Manager |
|---|---|---|
| 料金(標準) | 無料 | $0.40/secret/月 + API料金 |
| 料金(Advanced) | $0.05/10,000 API call | 同上 |
| 値のサイズ上限 | 標準4KB / Advanced 8KB | 64KB |
| 自動ローテーション | なし | あり |
| KMS暗号化 | SecureString型で対応 | 標準対応 |
.p12 は Base64 化すると 十数KB〜30KB 程度になることが多く、ここがサービス選定の分かれ目です。8KB に収まるなら Parameter Store が圧倒的に安く、超えるなら Secrets Manager か後述の S3 ハイブリッド方式が適切です。
サイズが8KBを超える場合の対処
サイズが大きすぎる場合は、S3に暗号化した .p12 を置き、Parameter Store には S3 のキーや復号パスワードだけを格納する ハイブリッド構成 が現実的にきれいです。Parameter Store のシンプルさと S3 のサイズ自由度を両立できます。
AWS側の準備
まずGitHub Actions用の OIDC IDプロバイダー をIAMで作成します(既に作成済みならスキップ)。
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
次に、Parameter Store にパラメータを保存します(AWS CLIの例)。
# 証明書本体
aws ssm put-parameter \
--name /ios/yourapp/dist-cert-base64 \
--type SecureString \
--value "$(base64 -i Certificates.p12)"
# .p12のパスワード
aws ssm put-parameter \
--name /ios/yourapp/p12-password \
--type SecureString \
--value "your-p12-password"
# プロビジョニングプロファイル
aws ssm put-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--type SecureString \
--value "$(base64 -i YourApp_AppStore.mobileprovision)"
# App Store Connect APIキー(.p8)
aws ssm put-parameter \
--name /ios/yourapp/asc-api-key-base64 \
--type SecureString \
--value "$(base64 -i AuthKey_XXXXXXXXXX.p8)"
GitHub Actions 用の IAM ロールを作成し、Trust Policy で 特定リポジトリの特定ブランチからのみ Assume Role できるよう絞り込みます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
権限ポリシーは最小権限で、対象パラメータパスへの GetParameter のみ与えます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
GitHub Actions 側のワークフロー
permissions ブロックで id-token: write を設定するのを忘れずに。これがないとOIDCトークンが取得できません。
permissions:
id-token: write # OIDCトークン取得に必要
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch secrets from Parameter Store
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Parameter Storeから取得(SecureStringは --with-decryption が必要)
aws ssm get-parameter \
--name /ios/yourapp/dist-cert-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $CERTIFICATE_PATH
aws ssm get-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $PP_PATH
P12_PASSWORD=$(aws ssm get-parameter \
--name /ios/yourapp/p12-password \
--with-decryption \
--query Parameter.Value --output text)
# キーチェーン作成と証明書インポート
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# 以降の archive / export / altool は Step 5 と同じ
メリット
- コストがほぼゼロ: 標準パラメータなら完全無料、Advanced でも極めて安価
- OIDC連携で長期クレデンシャル不要: アクセスキーをGitHubに置かなくて済む(最大のメリット)
- CloudTrailで監査ログ: 「いつ誰が証明書を取得したか」が全部記録される
- IAM/KMSによる細かい権限制御: 開発環境用と本番用でロールを分けるなど柔軟に運用可能
- 複数CI/CDで共有可能: GitHub Actions以外(CodeBuild、Jenkins、ローカル開発)からも同じ証明書を参照できる
- バージョン履歴: 過去のバージョンに巻き戻せる
デメリット
- AWSアカウントが必須(既に使っていないチームには初期構築コストが重い)
.p12のサイズが8KBを超える場合は工夫が必要(S3併用 or Secrets Manager)- OIDC設定の学習コストが多少ある(最初の一回だけ)
向いているケース: 既に AWS を使っている、監査ログが要件にある、複数のCI/CDツールから同じ証明書を参照したい、コストを抑えつつエンタープライズ品質の管理をしたい
選択肢5: AWS Secrets Manager + OIDC連携
仕組み: AWS Secrets Manager に .p12 を バイナリ(SecretBinary)として直接 格納し、GitHub Actions から OIDC 経由で取得します。Parameter Store と違い、Base64 への変換を意識せずにファイルとして扱えるのが大きな特徴です。
Parameter Store との違い
| 項目 | Parameter Store | Secrets Manager |
|---|---|---|
| 値の格納形式 | 文字列のみ(バイナリはBase64必須) | バイナリ直接OK (SecretBinary型) |
| サイズ上限 | 4KB (標準) / 8KB (Advanced) | 64KB |
| 料金 | 無料 〜 極めて安価 | $0.40/月/secret + API料金 |
| 自動ローテーション | なし | あり (Lambda連携) |
| 用途の方向性 | 設定値・小さな秘密 | 大きめの秘密・本格運用 |
.p12 が 8KB を超える場合や、ファイルとしての扱いやすさを重視するなら Secrets Manager が素直です。証明書1〜2枚なら月額1ドル未満で済むので、コストもさほど気になりません。
AWS側の準備
OIDC IDプロバイダーの設定は選択肢4と共通なので省略します。
Secrets Manager にバイナリとして登録します。fileb:// プレフィックスを使うのがポイントで、これにより AWS CLI がファイルをそのままバイナリとして送信してくれます。
# 証明書本体(バイナリのまま登録できる)
aws secretsmanager create-secret \
--name ios/yourapp/dist-cert \
--secret-binary fileb://Certificates.p12
# プロビジョニングプロファイルもバイナリとして
aws secretsmanager create-secret \
--name ios/yourapp/provisioning-profile \
--secret-binary fileb://YourApp_AppStore.mobileprovision
# App Store Connect APIキー(.p8)もバイナリとして
aws secretsmanager create-secret \
--name ios/yourapp/asc-api-key \
--secret-binary fileb://AuthKey_XXXXXXXXXX.p8
# パスワード類は文字列として
aws secretsmanager create-secret \
--name ios/yourapp/p12-password \
--secret-string "your-p12-password"
更新時は update-secret を使います。
aws secretsmanager update-secret \
--secret-id ios/yourapp/dist-cert \
--secret-binary fileb://Certificates_new.p12
IAMロールの権限ポリシーは Parameter Store の代わりに Secrets Manager を許可します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
GitHub Actions 側のワークフロー
SecretBinary で取得した値は API 応答上 Base64 エンコードされて返ってくるので、base64 --decode でデコードしてファイルに書き出します。
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch certificates from Secrets Manager
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# SecretBinary はBase64で返ってくるのでデコードして書き出し
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/dist-cert \
--query SecretBinary --output text | base64 --decode > $CERTIFICATE_PATH
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/provisioning-profile \
--query SecretBinary --output text | base64 --decode > $PP_PATH
# SecretString はそのまま取得できる
P12_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id ios/yourapp/p12-password \
--query SecretString --output text)
# APIキー (.p8) を altool が要求するパスに配置
API_KEY_ID="${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}"
mkdir -p ~/.appstoreconnect/private_keys
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/asc-api-key \
--query SecretBinary --output text | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
# キーチェーン作成と証明書インポート
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# 以降の archive / export / altool は Step 5 と同じ
メリット
.p12をファイルとしてそのまま登録・取得できる(Azure DevOps の Secure Files に最も近い感覚)- 64KB までサポート: サイズ制限を気にせず利用可能
- OIDC連携で長期クレデンシャル不要: 選択肢4と同じく、安全性が高い
- CloudTrailで監査ログ が標準で取れる
- 自動ローテーション: Lambda 関数を組めば証明書の自動更新も可能(ただしApple側のAPI連携も必要なので構築コストは高い)
- IAMによる細かい権限制御
デメリット
- コストが発生: $0.40/secret/月 + API料金。証明書3〜5個で月額数ドル程度
- AWSアカウントが必須
- OIDC設定の学習コストがある(選択肢4と共通)
向いているケース: ファイルとしての扱いやすさを重視、.p12 が大きい、複数アプリで証明書ファイルを多数管理する、自動ローテーションを将来的に視野に入れたい、Azure DevOps の Secure Files 相当の運用感が欲しい
選択肢6: 他のクラウドシークレットマネージャ
AWS以外を使っているチーム向けの選択肢として、以下も同様の考え方で利用できます。いずれもOIDC連携可能です。
| サービス | 料金感 | サイズ上限 | バイナリ対応 |
|---|---|---|---|
| Azure Key Vault | $0.03/10,000 ops | 25KB | 〇 (Certificate型) |
| Google Secret Manager | $0.06/月/secret | 64KB | 〇 |
| HashiCorp Vault (OSS) | サーバ費用のみ | 制限なし | 〇 |
特に Azure Key Vault は Certificate 型 が用意されており、.p12 形式での直接インポート/エクスポートに公式対応しています。Azure系を使っているなら相性が良いです。
選択肢7: GitHub Environments による追加保護
上記のどの選択肢と組み合わせても使えるオプションです。GitHub Environments を設定し、特定ブランチからのデプロイ時のみ証明書にアクセス可能 にしたり、手動承認ステップ を挟んだりできます。
jobs:
deploy:
runs-on: macos-14
environment: production # ← Environment に紐付けた Secrets が使える
Environment 単位で Secrets を分離できるので、本番配布用証明書を誤って開発ビルドで使う事故を防げます。
規模別のおすすめ構成
| 規模 | 推奨構成 |
|---|---|
| 個人開発・趣味アプリ | 選択肢1 (Base64 + Secrets) シンプルさが正義 |
| スタートアップ・小規模チーム(1〜3人) | 選択肢1 または 選択肢2 (暗号化ファイル) |
| 中規模チーム(複数アプリ or 5人以上) | 選択肢3 (fastlane match) または 選択肢5 (Secrets Manager) |
| AWSを業務利用、コスト重視 | 選択肢4 (Parameter Store + OIDC) |
| AWSを業務利用、運用重視 | 選択肢5 (Secrets Manager + OIDC) ファイルとして扱える |
| エンタープライズ・監査要件あり | 選択肢4 or 5 + 選択肢7 (Environments) の組み合わせ |
| Azure DevOpsからの移行チーム | 選択肢2 (暗号化ファイル) または 選択肢5 (Secrets Manager) Secure Files に最も近い |
よくあるトラブルと対処
No signing certificate "iOS Distribution" found
キーチェーンへの証明書インポートが失敗しているか、set-key-partition-list が抜けていると起きやすいエラーです。security find-identity -v -p codesigning $KEYCHAIN_PATH をステップに挟んで、証明書が見えているか確認すると切り分けがしやすいです。
error: exportArchive: "YourApp.app" requires a provisioning profile
プロビジョニングプロファイルの配置漏れか、ExportOptions.plist の provisioningProfiles のBundle IDとプロファイル名が一致していません。プロファイル名は「Apple Developerサイトで表示される名前」であってファイル名ではない点に注意してください。
altool: Invalid API key
.p8 ファイルのBase64化時に改行や余分な空白が混入しているケースが多いです。base64 -i file.p8 | pbcopy で生成してそのまま貼り付けるのが確実です。また、ファイル名が AuthKey_<KEY_ID>.p8 という命名規則を守っていないと altool が認識しないので、環境変数経由で正確に組み立てましょう。
xcodebuild: error: The operation couldn't be completed. No such file or directory
.xcworkspace を使っているのに -project を指定している(またはその逆)のケース。CocoaPods や Swift Package Manager のワークスペースを使っているなら -workspace を指定してください。
Parameter Store: ParameterNotFound または AccessDenied
AWS連携時に発生するエラー。次の順で確認します。
- パラメータパスに typo がないか(
/ios/yourapp/...の階層) - IAMロールの権限ポリシーで対象パスに
ssm:GetParameterが許可されているか - SecureString を使っているなら
--with-decryptionフラグを付けているか - KMSキーの
kms:Decrypt権限がIAMロールに付与されているか - Trust Policy の
sub条件がワークフロー実行ブランチと一致しているか
さらに改善するには
今回は最小構成でパイプラインを組みましたが、次のような拡張が考えられます。
- Slack通知:
slackapi/slack-github-actionを組み込んで、アップロード完了を自動通知。 - リリースノートの自動生成:
git logから変更点を抽出して TestFlight の "What to Test" 欄へ自動入力(altool単体では難しいので、App Store Connect API を直接叩く必要あり)。 - タグpushトリガー化:
mainへのpushではなくv*タグpushで発火させれば、より意図的なリリースフローになります。 - ユニットテストの前段実行: archive の前に
xcodebuild testを走らせて、テスト失敗時はデプロイを止める。
おわりに
fastlane を使わない構成は、ワークフローが直感的でわかりやすく、Ruby環境が不要で、Xcode更新時の fastlane 追従問題にも左右されない という利点があります。その代わり、ビルド番号管理などは自前で組む必要があり、複数アプリを運用する規模になると管理コストが増えてくるので、そうなったら fastlane match や AWS Secrets Manager / Parameter Store への移行を検討するのが現実的です。
初回セットアップの鬼門は 証明書まわり に尽きるので、この記事と「証明書管理の選択肢」セクションがその壁を越える助けになれば幸いです。
