iOSGitHub ActionsTestFlightXcodeCI/CDCode Signing

GitHub ActionsでiOSアプリをビルド・署名してTestFlightに自動アップロードする(脱fastlane版)

Sloth255
Sloth255
·12 min read·2,650 words

はじめに

iOSアプリのリリース作業、毎回ローカルのXcodeからArchive → Distribute... と手動で行っていませんか?
この作業は時間がかかる上に、ビルド環境が属人化しやすく、「自分のMacだとビルドできるのに、他の人のMacだと失敗する」といった問題の温床になります。

本記事では、GitHub Actions を使ってiOSアプリをビルド・署名し、TestFlightへ自動アップロードする 一連のパイプラインを構築する手順を解説します。

よくある解説記事では fastlane を使った構成が紹介されていますが、本記事では fastlane を使わず、Apple純正の xcodebuildxcrun altool だけで完結させる構成 を採用します。Ruby環境を持ち込まなくて済むので、ワークフローがシンプルになり、Xcode更新時のfastlane追従問題からも解放されます。

認証方式には、Apple ID + パスワードではなく、より安全で2FAの影響を受けない App Store Connect APIキー を採用します。

前提条件

  • Apple Developer Program に登録済み(有償)
  • App Store Connect にアプリのレコードが作成済み
  • GitHubリポジトリにiOSプロジェクトが push 済み
  • macOS / Xcode でローカルビルドが通ることを確認済み

全体の流れ

構築するパイプラインの全体像は次のとおりです。

  1. main ブランチへの push または手動トリガーでワークフロー起動
  2. GitHub Actions の macOS runner 上で Xcode を準備
  3. 証明書とプロビジョニングプロファイルを一時的にキーチェーンへインポート
  4. ビルド番号を更新
  5. xcodebuild archive.xcarchive を生成
  6. xcodebuild -exportArchive.ipa にエクスポート
  7. xcrun altool で TestFlight へアップロード
  8. 一時キーチェーンを削除してクリーンアップ
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キーを使います。

  1. App Store Connect にログイン
  2. 「ユーザーとアクセス」→「Integrations」→「App Store Connect API」を選択
  3. 「+」ボタンからキーを生成(ロールは App Manager 以上)
  4. ダウンロードした AuthKey_XXXXXXXXXX.p8 を保存(再ダウンロード不可なので注意)
  5. 発行時に表示される Key IDIssuer ID を控える

.p8 ファイルも同様にBase64化しておきます。

base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy

Step 3: GitHub Secretsに登録する

リポジトリの SettingsSecrets and variablesActions から、以下の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 しておきます(機密情報は含まれません)。

ios/ExportOptions.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>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 を作成します。

.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 できるよう絞り込みます。

trust-policy.json
{
  "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 のみ与えます。

permission-policy.json
{
  "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トークンが取得できません。

.github/workflows/ios-testflight.yml
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 を許可します。

permission-policy.json
{
  "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 でデコードしてファイルに書き出します。

.github/workflows/ios-testflight.yml
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連携時に発生するエラー。次の順で確認します。

  1. パラメータパスに typo がないか(/ios/yourapp/... の階層)
  2. IAMロールの権限ポリシーで対象パスに ssm:GetParameter が許可されているか
  3. SecureString を使っているなら --with-decryption フラグを付けているか
  4. KMSキーの kms:Decrypt 権限がIAMロールに付与されているか
  5. 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 への移行を検討するのが現実的です。

初回セットアップの鬼門は 証明書まわり に尽きるので、この記事と「証明書管理の選択肢」セクションがその壁を越える助けになれば幸いです。