1〜5分納品

専用 M4 Runner で macOS キューから解放

$21.5 / 日〜 · 物理マシン専有
クラウド Mac を構成
M4 · 16 GB Xcode バージョン固定可 launchd 常駐

FIELD NOTE · iOS ビルド

専用 M4 ノードで GitHub Actions Runner を登録:iOS パイプライン構築の実録

常設 Mac がなくても GitHub 上で iOS Archive を安定稼働させたい?本記事では JexMac 専有 Mac mini M4 物理ノードでの完全な構築手順を記録:SSH 初回接続、Runner 登録と launchd 常駐、Workflow ラベルルーティング、Xcode バージョン固定、ヘッドレス環境での Distribution 証明書インポートまで——各ステップに再現可能なコマンドと実測時間を付記。

構築目標:検証可能な iOS ビルドパイプラインを通す

Runner をインストールする前に、「構築完了」の受け入れ基準を定義し、Runner 導入後に Archive が署名段階で止まる事態を防ぎます。本実録のゴールは:指定ブランチへの push → GitHub Actions が専用 M4 ノードで起動 → コード checkout → xcodebuild archive 実行 → .xcarchive 正常生成。TestFlight アップロードは本記事の範囲外ですが、Archive が安定すれば fastlane や altool は追加ステップに過ぎません。

テストリポジトリは中規模の SwiftUI アプリ:Swift ソース約60ファイル、CocoaPods でサードパーティ依存管理、Release 設定は Manual 署名。ベース環境は JexMac シンガポールノード上の Mac mini M4(16 GB ユニファイドメモリ、256 GB NVMe)、Xcode 16.2 は xcodes でインストール・固定。

4m 08s
Clean Archive 実測(pod install 含む)
< 25s
セルフホスト job がキューから実行開始まで
0 swap
16 GB メモリでスワップなし
1台
専有物理マシン、仮想化オーバーセルなし

対照群は同一リポジトリを runs-on: macos-14 ホスト Runner で実行した結果:UTC 13:00–18:00(アジア太平洋の午後)の job キュー待ち中央値11分、最長19分;macos-14 プリインストール Xcode バージョンがローカル開発機と不一致で Swift 6 構文互換問題が発生したことも。セルフホストの価値はここで明確——待ち時間を分単位から秒単位へ、Xcode バージョンをノード上で完全固定

ホスト macOS Runner の3つの隠れコスト

多くのチームが最初に GitHub ホスト Runner を選ぶのは「運用ゼロ」だからですが、iOS シナリオでは隠れコストが請求額より厄介なことが多い。

第1類は時間コスト。GitHub の macOS プール容量は限られ、無料アカウントの月2000分は macOS で10倍ウェイト換算され、実質約200分。1日8回トリガー、1回6分ビルドのプロジェクトなら月約1440加重分——無料枠上限に近く、nightly ビルドを足すと超過しがち。

第2類は環境ドリフト。macos-latest タグは GitHub インフラ更新で基盤 Xcode が切り替わり、「昨日は緑、今日は赤」が何度も起きた。暫定 workaround は workflow に sudo xcode-select ステップを足すことだが、バージョン切替ごとに1〜2分追加され、ローカル開発機と完全一致も保証できない。

第3類はデバッグコスト。ホスト Runner は job 終了後に環境が破棄され、SSH で入って再現できない。キーチェーンダイアログ、プロビジョニングプロファイル期限切れなど「ログインして確認」が必要な障害は、ホスト環境では push を繰り返してログを試すしかない——User interaction is not allowed エラーで push を7回往復し、partition list 欠落にようやく辿り着いた。

実測環境の説明

本記事のコマンドと所要時間データはすべて JexMac 専有 Mac mini M4 物理ノードで実行。ノードはシンガポールデータセンター、テスト期間は2026年7月下旬。ハードウェア:Apple M4 · 10コア CPU · 16 GB ユニファイドメモリ · 1 Gbps 専有帯域。

ノード納品後:SSH 初回接続と環境ベースライン

JexMac コンソールは支払い確認後、通常1〜5分で SSH 認証情報を配信。パブリック IP 取得後、Ed25519 キーで接続しパスワードログインを無効化してから Runner インストールを開始することを推奨——Runner プロセスは現在の macOS ユーザー権限で動作するため、SSH セキュリティベースラインを先に固める。

  1. 01
    SSH 公開鍵を登録

    ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<ノードIP>

    パスワードレスログイン確認後、/etc/ssh/sshd_configPasswordAuthentication no を設定し、sshd を再起動。

  2. 02
    Homebrew と xcodes をインストール

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    brew install xcodesorg/made/xcodes

    xcodes install 16.2 --experimental-fast-pass で Xcode 16.2 をインストール・固定(バージョンはプロジェクト要件に合わせて調整)。

  3. 03
    ビルドチェーンを検証

    xcodebuild -versionXcode 16.2 と Build 番号を出力するはず。

    xcodebuild -showsdks | grep iphoneos で iOS SDK 利用可能を確認。

  4. 04
    CocoaPods をインストール(プロジェクト使用時)

    sudo gem install cocoapods -n /usr/local/bin

    ノード上で事前に pod install を1回通し、Specs リポジトリキャッシュを準備。CI 初回ビルドが repo update で止まるのを防ぐ。

ベースライン設定完了後、ノード上に専用ディレクトリ(本記事では ~/ci-runner)を用意し Runner とビルド成果物を日常開発ファイルから分離。DerivedData サブディレクトリを予約し、後続 workflow で -derivedDataPath を明示指定、マルチプロジェクト並行時のパス競合を防止。

GitHub 側:Runner トークンとラベルルーティング設計

対象リポジトリ → Settings → Actions → Runners → New self-hosted runner、プラットフォームは macOS ARM64。ページにワンタイム登録トークン(有効1時間)とダウンロードリンクが生成される。

ラベル設計は workflow が正しいマシンにルーティングできるかを直接左右。当社の命名規則:

  • mac:すべての macOS セルフホストノード共通ラベル
  • m4:Apple Silicon M4 チップ識別、旧 Intel ノードと区別
  • xcode-16-2:Xcode マイナーバージョン固定、アップグレード時は workflow ロジックではなくラベルを変更
  • sg:データセンター地域(シンガポール)、マルチリージョン展開時に近接ルーティング

登録コマンドの --labels パラメータで上記ラベルを一括設定。workflow では配列形式でマッチ:

jobs:
  ios-archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    concurrency:
      group: ios-build-${{ github.ref }}
      cancel-in-progress: true

concurrency グループで同一ブランチの Archive 並行実行を防止——16 GB メモリの M4 ノードでは2プロジェクト同時 Clean Build も可能だが、DerivedData 競合で1回あたり所要時間が30%以上変動。複数プロダクトラインがあるチームはラベル分割(product-aproduct-b など)と複数ノードを推奨、1台で無理に並行させない。

公開リポジトリのセキュリティ境界

セルフホスト Runner は公開リポジトリで fork PR からトリガー可能で、悪意ある workflow が Mac 上で任意コードを実行する。必ずプライベートリポジトリまたは Organization レベルで有効化し、Runner プロセスは非管理者アカウントで実行。本番環境では GitHub Environment 保護ルールと組み合わせ、secrets を指定ブランチのみに制限することを推奨。

M4 ノードに Runner をインストールし launchd 常駐を設定

以下の手順は SSH セッションで実行。Runner バージョンは GitHub 登録ページ表示に従う(本例 v2.321.0)。

  1. 01
    ダウンロードして解凍

    mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runner

    curl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz

    tar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz

  2. 02
    対話式登録

    ./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended

    --unattended で対話確認をスキップ、スクリプト化デプロイ向け。Work folder はデフォルト _work のままで可。

  3. 03
    launchd サービスをインストール

    ./svc.sh install

    ./svc.sh start

    検証:./svc.sh statusactive (running) を表示。GitHub リポジトリ Runners ページに緑の Online 状態が出れば登録成功。

launchd はシステム再起動後に Runner を自動起動するが、注意点あり:Runner はインストール時の macOS ユーザー権限で動作し、そのユーザーは一度ログイン済み(または自動ログイン有効)である必要があり、そうでなければ launchd が Keychain にアクセスできない場合がある。ノード上に専用アカウント ci-bot を作成し、SSH 初回ログインでキーチェーン初期化後に svc.sh をインストール、以降の再起動は人手不要。

Runner ログは ~/ci-runner/actions-runner/_diag/、各 job 終了後に Worker_*.log が生成。「job はノードに割当済みだがステップ出力なし」系の問題は、GitHub UI の Annotations より RunnerListener ログの方が情報が完全。

最小構成 Workflow:checkout から Archive まで

登録完了後、リポジトリに .github/workflows/ios-archive.yml を作成。以下は検証済み最小版、TestFlight アップロードなし、Archive 生成に特化:

name: iOS Archive
on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]

jobs:
  archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: xcodes select 16.2

      - name: Install pods
        run: pod install --deployment
        working-directory: ios

      - name: Build archive
        run: |
          xcodebuild archive \
            -workspace ios/MyApp.xcworkspace \
            -scheme MyApp \
            -sdk iphoneos \
            -configuration Release \
            -archivePath ./build/MyApp.xcarchive \
            -derivedDataPath ~/ci-runner/DerivedData \
            CODE_SIGN_STYLE=Manual \
            CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
            PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
        env:
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

      - name: Upload xcarchive artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-xcarchive
          path: ./build/MyApp.xcarchive
          retention-days: 7

意図的に残した設計選択:

pod install --deployment で Podfile.lock 固定、CI とローカル依存の不一致を防止。-derivedDataPath はノード上の固定ディレクトリを指定、複数ビルドでコンパイルキャッシュ再利用、増分ビルドは4分から約1分40秒へ短縮。upload-artifact で xcarchive を GitHub にアップロード、SSH 権限のない同僚でもダウンロード検証可能——artifact は7日保持、QA スポットチェックに十分。

ヘッドレス環境:キーチェーンと Distribution 証明書インポート

Archive ステップの成否は、CI 環境が GUI なしで Distribution 秘密鍵にアクセスできるかに依存。ホスト Runner はシステムキーチェーンを事前設定済みだが、セルフホストノードは自分で管理——多くのチームが「Runner は入ったが Archive で署名エラー」で止まる原因もここ。

推奨:各 job で一時キーチェーンを作成、p12 インポート後すぐ署名に使用、job 終了で自動破棄、秘密鍵のディスク長期常駐を回避。

# "Build archive" ステップの前に追加
- name: Import signing certificate
  run: |
    security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security default-keychain -s build.keychain
    security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security set-keychain-settings -t 3600 -u build.keychain

    echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
    security import cert.p12 \
      -k build.keychain \
      -P "${{ secrets.P12_PASSWORD }}" \
      -T /usr/bin/codesign \
      -T /usr/bin/xcodebuild

    security set-key-partition-list \
      -S apple-tool:,apple: \
      -s -k "$KEYCHAIN_PASSWORD" build.keychain
    rm -f cert.p12
  env:
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

GitHub Repository Secrets に3つの変数を事前設定:KEYCHAIN_PASSWORD(一時キーチェーンパスワード、ランダム文字列で可)、CERTIFICATE_P12_BASE64(Distribution 証明書の Base64 エンコード)、P12_PASSWORD(p12 エクスポート時に設定したパスワード)。

エラーキーワード よくある根本原因 対処方法
User interaction is not allowed 秘密鍵 partition list 未設定、codesign が UI を表示しようとする set-key-partition-list コマンドを再実行(上記スクリプト参照)
errSecItemNotFound 証明書が誤ったキーチェーンにインポート、または default-keychain 未切替 import 前に security default-keychain -s build.keychain が実行されていることを確認
could not find signing certificate CODE_SIGN_IDENTITY 文字列とキーチェーン Identity が完全一致しない security find-identity -v -p codesigning build.keychain を実行し完全名称をコピー
Provisioning profile doesn't match プロビジョニングプロファイル期限切れ、または Bundle ID / Capability 不一致 Apple Developer ポータルで再生成、ダウンロード後 secrets または match で同期

インポート成功後、job ログで1コマンド迅速検証可能:

security find-identity -v -p codesigning build.keychain | grep Distribution

1 valid identities found を確認してから Archive ステップへ進めば、「署名失敗だがどこで止まったか不明」の調査時間を大幅削減。

トラブルシューティング実録:追加 push が1回必要だった3つの問題

上記手順どおりでも初回構築で以下が起きうる——すべて実際に踏んだ落とし穴、ログ特徴と解決策付き。

ラベル不一致:job が永遠にキュー待ち

現象:GitHub Actions UI で job 状態 Queued、Runners ページのノードは Online。原因は workflow の runs-on ラベルと登録時が完全一致しない——例:workflow が xcode-16.2(ドット)、登録が xcode-16-2(ハイフン)。GitHub ラベルマッチは完全一致、1文字違いでルーティングされない。

解決:リポジトリ Runners ページでノード名をクリック、実際のラベル一覧を確認し YAML にコピペ、手入力しない。

launchd 再起動後 Runner Offline

現象:ノード reboot または JexMac 側メンテ再起動後、Runner が Offline、手動 SSH で ./svc.sh start が必要。原因は launchd plist の UserName と SSH ログインユーザー不一致、またはそのユーザーが初回グラフィカル/session ログイン未完了。

解決:./svc.sh install./svc.sh start を同一ユーザーで実行することを確認;再起動後 ./svc.sh status を確認。失敗時は /Library/Logs/GitHubActionsRunner/ の stderr ログを確認。

DerivedData 権限競合

現象:2回目ビルドで Unable to write to DerivedData または一部 .o ファイル permission denied。原因は1回目 job が root または別ユーザーで DerivedData ディレクトリを生成、後続 job に書込権限なし。

解決:DerivedData パスを統一し workflow 冒頭にクリーンアップ:rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData。または各 Archive 前に該当プロジェクト hash サブディレクトリのみ削除、再利用可能なコンパイルキャッシュは保持。

常設 Mac がない場合:ビルドリズムに合わせて専用ノードをレンタル

ここまで来れば気づく:GitHub Actions セルフホスト Runner の技術ハードルは高くない、本当のボトルネックは24時間オンラインで Xcode バージョン制御可能な macOS 物理マシンがあるか。ローカル MacBook は CI ホスト向きでない——8 GB メモリ機種は Archive 時にファン全開で頻繁 swap;会社調達 Mac mini は固定資産承認、データセンター托管、証明書ローテ時の現場メンテが必要。

当社のやり方:イテレーション冲刺期(例:リリース前2週間)に JexMac 専有 Mac mini M4 ノードを Runner ホストとしてレンタル、リリース安定後は週単位更新または解放。標準構成 16 GB ユニファイドメモリ、256 GB NVMe、1 Gbps 専有帯域、日額 $21.5〜、支払い後1〜5分で SSH / VNC 接続、契約縛りなし。5拠点——シンガポール、日本(東京)、韓国(ソウル)、中国香港、米国東部——チーム所在地に応じて選択、git fetch と CocoaPods Specs 同期レイテンシ低減。

GitHub ホスト macOS Runner との比較:ピーク時キュー10〜20分 vs セルフホスト25秒以内開始;macOS 分は10倍ウェイト課金 vs 固定日額でコスト予測可能。自社購入ハードウェアとの比較:upfront 投資ゼロ、バージョンアップ時はラベル変更または Xcode 再インストール、調達サイクル待ち不要。

同一ノードをリモート開発デスクトップ兼用——ブラウザ VNC で UI デバッグ、Instruments でパフォーマンス計測、CI アイドル時間を無駄にしない。2〜3人小チームでは「1台 M4 物理マシン = Runner + リモート Mac 開発環境」が、ホスト Runner 分購入とローカルメモリ増設を分けるより経済的なことが多い。

物理マシン専有 · 1〜5分納品

Runner を M4 ノードに接続

記事内の全コマンドは JexMac 専有 Mac mini M4 物理ノードで検証済み。開通 → SSH 接続 → 手順どおり Runner 登録、最短1時間で Archive 通過。日額レンタル、冲刺終了後解放可、年間契約縛りなし。

標準構成
チップApple M4 · 38 TOPS
CPU10コア(4P + 6E)
メモリ16 GB ユニファイドメモリ
ネットワーク1 Gbps 専有帯域
SLA99.9% 可用性
納品1〜5分自動開通