要点
- まず失敗を分類する:ワークフロー YAML を触る前に、ログを Context(キャッシュ/状態)、Execution(CPU/メモリ/IO)、Permission(署名/シークレット)にマッピングする。
- クラウド VM の典型パターン:ステートレスなコールドスタート、マルチテナント IO 競合、セッション終了時のディレクトリ消去——この三つが揃うと「緑になるまで再実行」がデフォルトになる。
- 非対称な結論:CI 安定性の分水嶺は YAML の巧みさではなく、実行コンテキストが Job 間で生き残れるかである。
- 判断シグナル:同じコミットが連続2回失敗する、または warm ビルドでもタイムアウトするなら、専用セルフホスト Mac の検討時期だ。
- 実践パス:下記7ステップ Runbook でログ切り分けから環境受け入れまで進め、「キャッシュキーを足して再実行」のループを止める。
結論(先に)
GitHub Actions の失敗率の根本原因は、たいてい「シェルコマンド1行が間違っている」のではなく、クラウド VM が再利用可能なビルドコンテキストを提供できないことにある。
kvmboot サポートチケットでよく見る典型パターン:チームは GitHub ホストの macos-latest で PoC を緑にし、コスト削減のためサードパーティの「Mac クラウドホスト」や共有 VPS に移行する——そして「遅い」から「遅くて不安定」へ移行する。pod install がランダムにタイムアウトし、xcodebuild がときどき OOM し、codesign が errSecInternalComponent を投げ、2回再実行すると Job が再び緑になる。チームは retry、sleep、より大きな timeout-minutes を足すが、CI/CD ボトルネックの切り分けの本当の入口はもっと単純だ:Runner の実行環境はどのクラスか? Job 間で状態を保持できるか?
公式リファレンス:Understanding GitHub Actions、GitHub-hosted runners、Self-hosted runners。
1. クラウド VM が GitHub Actions を繰り返し失敗させる理由
GitHub Actions は二層に分かれる:コントロールプレーン(GitHub がワークフローをスケジュールし、コードを取得し、アーティファクトを保存)と実行プレーン(実際に xcodebuild を動かすマシン)。クラウド VM 上で Job が走ると、失敗はほぼ実行プレーンに着地する——実行プレーンの問題は、ホストが共有かどうか、状態を永続できるかと強く相関する。
1.1 ステートレス Runner:毎 Job がコールドスタート
GitHub ホスト Runner は使い捨て思想だ:Job が終わるとディスクスナップショットは回収され、DerivedData、CocoaPods インデックス、グローバル npm キャッシュは消える。多くのサードパーティ共有クラウド VM も同じパターンをコピーする——セッション切断や夜間メンテスクリプトが ~/Library、/tmp、または home 全体を消す。あなたは actions/cache を設定したつもりでも、パスが動く、Podfile.lock のハッシュが変わる、restore がタイムアウトするなどでキャッシュキーがドリフトする。2回目のビルドもフルコールドパスを歩き、壁時計時間が伸びて timeout-minutes が発火し、ログには「遅い」ではなく「GitHub Actions failed」と出る。
1.2 マルチテナント競合:予測不能な Execution
共有クラウド VM では CPU クォータ、ディスク IOPS、出口帯域が不透明なことが多い。隣接テナントが大規模 Flutter プロジェクトや DB バックアップを同時に走らせると、swiftc のリンクフェーズが遅くなり、メモリピークが積み上がって OOM killer が介入する——macOS では xcodebuild が無言終了したり Signal 9 で死ぬ。これらの失敗はコードとは無関係だ。隣がたまたまアイドルなときに再実行すると Job は緑になる。チームは「ネットワークのジッター」とラベル付けする。
1.3 署名と Keychain:毎 Job で Permission を再構築
iOS / macOS CI は codesign、notarytool、専用 CI Keychain に依存する。共有クラウド VM は GUI セッション制限、カスタムセキュリティポリシー禁止、Keychain の長期アンロック禁止がありうる。毎 Job が security create-keychain → 証明書インポート → アンロック → 署名 → 削除を繰り返す。いずれかのステップがタイムアウトや権限拒否に当たるとパイプライン全体が赤になる。拒否コード表の詳細は Apple Silicon クラウド Mac での iOS CI codesign と公証 を参照。
1.4 「動く」≠「安定して動く」
多くのチームは PoC で「1回緑のビルド」だけを検証し、分散(バリアンス)を無視する。CI 信頼性は次で測るべきだ:同一コミット10回の成功率、P95 所要時間、失敗が同じステージに集中するか。クラウド VM は専用ベアメタルより三指標すべてで劣る——リモート iOS ビルドにベアメタル Mac サーバーを選ぶ理由 の中核論点のひとつだ。
2. 4つの失敗モード:トラブルシュート前に分類
CI/CD ボトルネックの切り分けでは、最後のエラー行から逆算しない。まず問う:この失敗はどのカテゴリか?
2.1 タイムアウト
ログ署名:##[error]The job running on runner … has exceeded the maximum time、または6時間上限前のステップ死亡。よくある根:pod install / flutter pub get の遅さ、コールド DerivedData コンパイル、巨大すぎる actions/cache のアップロード/ダウンロード。クラウド VM では特に多い——ディスク書き込みが遅いと「キャッシュ restore」自体がボトルネックになる。
2.2 OOM / ディスク / Signal 9
ログ署名:xcodebuild が明確なエラーなく終了、Killed、No space left on device、inode 枯渇。16GB 共有 VM でシミュレーターとフル archive を並列実行すると容易に起きる。チューニング指針は Apple Silicon クラウド Mac での Runner メモリと swap ガバナンス を参照。
2.3 Codesign / Keychain / Provisioning
ログ署名:errSecInternalComponent、Provisioning profile doesn't match、resource busy。複数 Team ID や外注並行では共有環境は Keychain を分離できず、失敗は断続的に現れる。codesign と公証のトラブルシュート表 と突き合わせる。
2.4 環境ドリフト(キャッシュ miss / ツールチェーン不一致)
ログ署名:同じコミットがときどき通りときどき落ちる;Xcode version mismatch;CI だけ Module not found。根は Runner イメージの不統一や設計の悪いキャッシュキー——クラウド VM では「夜間に Xcode をアップグレード」するホストメンテも加わる。
3. 中核比較:ホスト Runner vs クラウド VM vs 専用 Mac
下表は7列ヘッダーを統一し、アーキテクチャレビューと調達文書で同じ視点を共有する。
| 選択肢 | Entry | Execution | Context | Cost | Permission 境界 | 最適な用途 |
|---|---|---|---|---|---|---|
| GitHub ホスト Runner | YAML を編集して開始 | 標準 macOS イメージ;カスタムカーネル不可 | ステートレス;actions/cache 依存 |
分単位;大規模リポは高額 | サンドボックス;シークレットは GitHub Secrets | 1日3回未満のビルド、PoC チーム |
| 共有クラウド VM(Mac VPS) | SSH + 手動 Runner 導入 | 安く見える;IO/メモリは予測不能 | 運用でディレクトリ消去;キャッシュが残りにくい | 低月額;再実行コストが高い | マルチテナント;Keychain 分離困難 | 軽い検証のみ——主リリースには不向き |
| 専用ベアメタル Cloud Mac mini | セルフホスト Runner + ラベルルーティング | Apple Silicon ベアメタル;Xcode 版固定 | DerivedData/Pods が Job 間で永続 | 日/週レンタル;リリース週 ROI | 専用 Keychain;監査可能 | iOS/Flutter リリース、コンプライアンスチーム |
YAML の工夫はステップ順序を最適化できるが、共有クラウド VM を再利用可能なビルドコンテキストに変えることはできない——それはアーキテクチャの決定だ。
4. シナリオマトリクス:チームが向かうべき先
| シナリオ | 1日のビルド回数 | 推奨 | クラウド VM に固執する場合 |
|---|---|---|---|
| 個人サイドプロジェクト | <1 | GitHub ホスト Runner | 偶発的失敗は許容 |
| 小規模 Flutter チーム MVP | 1〜3 | ホスト Runner + 軽量キャッシュ | Podfile.lock 固定;並列 Job 無効化 |
| リリース週の高密度ビルド | 5〜15 | 専用 Mac セルフホスト Runner | 失敗率がしばしば30%超——非推奨 |
| 複数 Team ID / 外注並行 | 任意 | ベアメタル Mac + 分離 ci ユーザー |
署名失敗はほぼ避けられない |
| Windows ホスト + リモート iOS ビルド | 3〜10 | Cloud Mac 実行プレーン + ローカル制御プレーン | 共有 VPS はジャンプホストのみ |
「リリース週の高密度ビルド」または「複数 Team ID」に当てはまるなら、クラウド VM ワークフローの微調整にさらに時間をかける ROI は低い——DerivedData を warm に保てる専用 Mac の受け入れを優先する。ビルド時間の内訳は Flutter CI:GitHub Actions で時間はどこへ消えるか を参照。
5. 推奨スタック A / B / C
チーム成熟度別の3つの組み合わせ可能スタック:
【スタック A — ホスト Runner 切り分け】(1日3回未満)
GitHub ホスト macos-14/15
→ actions/cache(Pods + DerivedData でキー分離)
→ Job ステージごとに timeout-minutes 分割
→ 同一ブランチの concurrency 制限
【スタック B — クラウド VM + セルフホスト Runner】(移行期——注意して使用)
共有 Mac VPS に Runner 導入
→ 永続ボリューム上に derivedDataPath 固定
→ launchd 管理 Runner(Mac mini Runner ガイド参照)
→ 週次ディスク/inode 点検
⚠ 隣接 IO スパイクには依然脆弱
【スタック C — 専用 Cloud Mac 本番】(リリースチーム推奨)
専用 M4 Mac mini + セルフホスト Runner
→ ci ユーザー + ラベルルーティング(ios / flutter)
→ Golden Image で Xcode + CocoaPods 固定
→ 長期 CI Keychain + match または手動証明書
→ コントロールプレーンは GitHub Actions のまま
スタック B はチケットで最も多い罠だ:Runner を入れただけで「本番準備完了」と感じるが、共有 VM 実行プレーンはそうではない。スタック C は排他的実行にかかっている——Mac mini GitHub Actions セルフホスト Runner セットアップガイド と Flutter + Mac mini セルフホスト アーキテクチャ概要 を参照。
6. よくある誤解
- 誤解1:すべての失敗に
retryを足す——環境の不安定さを隠し、Runner 分を燃やし、汚れた状態をリリースに押し込む可能性がある。 - 誤解2:巨大な単一キャッシュキー——Pod 版が上がると blob 全体が無効;
pods-cacheとderiveddata-cacheを分離する。 - 誤解3:共有クラウド VM で本番署名——Keychain は分離できない;
errSecInternalComponentは定期便で戻る。 - 誤解4:月額レンタルだけ比較——エンジニアの切り分け時間、再実行コスト、リリース遅延を無視;共有 VPS の TCO はしばしば高い。
- 誤解5:「ローカルでビルドできる」= CI 準備完了——ローカルは warm DerivedData とアンロック済み Keychain がある;比較は不公平だ。
- 誤解6:16GB VM で並列 Job——OOM 確実;
concurrency: group: ios-build, cancel-in-progress: trueを使う。
7. 7ステップ トラブルシュート Runbook
- 現場を凍結:失敗 Job のフルログをダウンロード;コミット SHA、Runner 名、
runs-onラベル、総所要時間、ステップ別時間を記録。 - ステージ帰属:上位3つの遅いステップをマーク(よくある:
pod install、xcodebuild、cache restore)し、各を Context / Execution / Permission にマップ。 - リソース確認:失敗時にディスク(
df -h)、メモリ(vm_stat)、swap 使用を点検;クラウド VM では並列 Job がないか確認。 - キャッシュ検証:同一コミットを2回実行;キャッシュヒット率と
pod installが依然大量のInstallingを出すか比較。 - 署名検証:
codesign -vvvだけのワークフローに分割しコンパイルノイズを除去;拒否コードに沿って Keychain を修正。 - 環境パリティ検証:
xcodebuild -versionとpod --versionをローカルと揃える;Runner イメージまたは Golden Image を固定。 - 移行判断:同じステージが連続2回失敗し再実行が不安定なら、長期セルフホストを決める前に専用 Mac の日単位 PoC(コールド vs warm 比較)を開始する。
切り分け中は GitHub の debug logging と workflow commands でより細かいタイムスタンプを取る。
8. FAQ
クラウド VM で GitHub Actions が失敗する最も一般的な理由は?
最も一般的なパターンは三層の重なりだ:ステートレス Runner によるキャッシュ miss(Context)、共有 VM リソース競合による OOM やタイムアウト(Execution)、毎 Job の Keychain/署名再構築(Permission)。一層だけ直しても定着しにくい——本記事の三次元フレームワークが必要だ。
再実行すると通ることがあるのは環境問題か?
はい。 断続的成功は不安定なリソースや状態を意味し、壊れたコードロジックではない。「緑になるまで再実行」を技術的負債として扱い、失敗率を追跡する;10%超は本番準備完了ではない。
クラウド VM のスペックを上げれば恒久的に直るか?
OOM と一部タイムアウトは緩和するが、直せないのは共有テナント IO 競合、セッション清掃によるキャッシュ無効化、Keychain をアンロックしたままにできないことだ。24GB の共有 VPS でも隣がディスクを叩けばランダムに失敗する。
ホスト Runner からセルフホストへ移行すべきタイミングは?
同じワークフローが週2回以上失敗し、ログが pod install / xcodebuild / codesign に集中する、または DerivedData キャッシュヒット率が長期50%未満のとき——専用ベアメタル Mac セルフホスト Runner を評価する。48時間の日単位レンタルでコールド/warm 受け入れは足りる。
Linux クラウド VM で iOS CI はできるか?
いいえ——ネイティブ iOS ビルドチェーン全体は無理だ。xcodebuild、codesign、シミュレーターは macOS が必要。Linux VM は Flutter Android ターゲットや一般バックエンド CI には向く;iOS 実行プレーンは macOS で、専用が共有クラウド VM に勝つ。
9. まとめ
CI/CD ボトルネックの切り分けの最初の問いは「どの YAML 行が間違っているか」ではなく、「どのクラスの実行環境か、コンテキストは Job 間で生き残れるか」だ。GitHub ホスト Runner は低頻度 PoC に合う;共有クラウド VM CI は安く見えるが Context、Execution、Permission に同時に地雷を埋め、GitHub Actions の失敗は断続的で再現困難で治しにくくなる。
iOS または Flutter リリースを担うなら道筋は:7ステップ Runbook を実行 → 専用 Cloud Mac を日単位で受け入れ → launchd セルフホスト Runner を稼働 → 失敗率と P95 で ROI を測る。安定性の分水嶺は実行コンテキストであり、もう1つのキャッシュキーではない。
専用 Cloud Mac で断続的な GitHub Actions 失敗を止める
kvmboot Cloud Mac mini M4 は専用 Apple Silicon ベアメタルを提供する:DerivedData と Pods を Job 間で保持でき、CI Keychain は安定し、隣接 IO 競合がない。GitHub Actions セルフホスト Runner の実行プレーンとして——日単位でレンタルし、コールド/warm を2回走らせ、現在のクラウド VM CI と失敗率・P95 を比較してから月契約を決める。