FLARES LLC

Technical document

大規模CI/CDの実行時間と費用を同時に減らす設計原則

CI/CDを速くする時は、最初に高性能なランナーを増やすのではなく、実行する仕事を減らし、必要な仕事を細かく分け、重要度に沿って流し、最後に単価の低い計算資源へ移します。速度と費用を同時に改善する鍵は、最大並列数ではなく、変更からフィードバックまでに使った総CPU時間と待ち時間を一緒に管理することです。
CI/CD / コスト最適化約18分公開日 2026年8月10日更新日 2026年8月10日
一つの大きなCI処理を細粒度の並列タスクへ分け、ARM・Spot・キャッシュへ振り分ける技術者のイラスト

Summary

この文書の要点

  • 高速化の最初の手段はランナー増強ではなく、依存グラフとテスト影響分析で実行対象を減らすことです。
  • ARMとSpot VMは一律に適用せず、CPUアーキテクチャ互換性と中断耐性を確認したジョブだけを段階的に移します。
  • PR、main、Nightlyを優先キューへ分ける時は、ファストトラックと同時に予約枠、aging、最大待ち時間を設け、低優先ジョブの飢餓を防ぎます。
  • Fail-FastとPre-warmingは待ち時間を短くしますが、診断情報の欠落と待機費用を増やさないよう、停止条件と暖機対象を計測から決めます。

CIが遅い時は、実行時間と待ち時間と総計算量を分けます

大規模なCI/CDでは、パイプラインの経過時間だけを短くしても費用が下がるとは限りません。100分の直列処理を10台で10分にできても、同じ処理を同じCPU時間だけ実行していれば、ランナー単価や起動費用を含む総額は増える場合があります。反対に、実行対象を半分へ減らせれば、経過時間と費用の両方を下げられる余地があります。

最初に、キューで待っている時間、ランナーを準備する時間、依存関係を取得する時間、ビルドとテストの実行時間、成果物を転送する時間を分けて記録します。開発者が感じるフィードバック時間はこれらの合計ですが、改善策は区間ごとに異なります。待ち時間をターゲット分割で解決したり、不要テストを高性能VMで押し切ったりすると、費用だけが残ります。

改善の順序は、計測、実行対象の削減、タスク分割、キュー制御、実行基盤の選択、起動時間の短縮です。この順序なら、安価な計算資源を増やす前に、そもそも計算しなくてよい仕事を取り除けます。

PR単位の費用とP95フィードバック時間を基準にします

平均時間だけでは、混雑時の開発体験を評価できません。PR作成から最初の必須チェック完了までのP50とP95、キュー待ちのP95、成功時と失敗時のCPU分、再実行率、キャッシュ転送量を記録します。mainとNightlyは目的が異なるため、同じSLOへまとめません。

費用はVMの稼働時間だけでなく、キャッシュストレージ、成果物保存、ネットワーク転送、常時起動するwarm pool、Spot中断後の再実行も含めます。安いVMへ移した後に転送量と再実行が増えれば、請求額とフィードバック時間のどちらも悪化し得ます。

最適化の前後は、同じ変更集合と同じキャッシュ条件で比較します。warm cacheだけの数字を改善値として使わず、cold、incremental、混雑時、Spot中断時を分けます。目標は最速の一回ではなく、日常のP95とPRあたり総費用を安定して下げることです。

PR単位で追う最小メトリクス
feedback_time = queue + provisioning + setup + build_test + upload

compute_cost = sum(vcpu_minutes * runner_unit_price)
total_cost = compute_cost + cache_storage + network + warm_pool + retry

track:
  p50_feedback, p95_feedback, p95_queue
  vcpu_minutes_per_pr, retry_rate, cache_hit_rate
  cache_download_bytes, spot_interruption_rate

Bazelターゲットを、変更の無効化範囲に合わせて分けます

巨大な一つのtargetへ多数のサービス、生成処理、テストを集めると、小さな変更でも、その入力に依存する多数のactionが無効化されます。Bazelの並列実行とキャッシュはaction単位で働くため、互いに独立して変更されるコードと成果物は、依存関係を明示した小さなtargetへ分けます。細粒度の依存関係は、並列性とincremental buildの両方を生みます。

ただし、ファイル一つごとにtargetを作ればよいわけではありません。targetが細かすぎると、依存グラフの解析、メタデータ、リモートキャッシュへの多数の小さな通信が増えます。変更頻度、再利用境界、テスト責務がそろう単位をpackageとtargetの境界にし、分割前後でanalysis時間、action数、critical path、cache hitを比較します。

ランナー側でtargetを一つずつ順番に呼ぶと、分割の効果が失われます。変更影響から必要な複数ラベルを選び、一度のBazel呼び出しへ渡し、実行順序は依存グラフへ任せます。安定ブランチでは広いtarget集合を定期的に通し、日常経路の対象選択漏れを検出します。

  • 分割基準: 変更頻度、再利用境界、成果物、テスト責務が異なる箇所を分ける。
  • 避ける設計: 多数のサービスが巨大な共通targetへ依存し、変更のたびに下流全体を無効化する。
  • 評価指標: analysis時間、実行action数、critical path、remote cache hit、転送量。
  • 実行方法: 必要な複数targetを一度に渡し、外側のforループで直列化しない。

テスト影響分析は、全量テストを捨てずに高速経路を作ります

テスト影響分析では、変更されたファイル名だけで実行対象を決めません。変更targetからreverse dependencyをたどり、影響を受けるbuildとtestを選びます。動的ロード、生成コード、共通設定、データ契約のように静的な依存グラフへ現れにくい要素は、明示的な影響ルールまたは保守的なfallbackへ置きます。

選択テストだけを唯一の検証経路にすると、依存情報の欠落がそのまま偽陰性になります。PRでは高速な選択テストを実行し、mainまたはNightlyでは全量テストを実行して、選択されなかったテストの失敗を照合します。漏れが見つかったら、コードの修正だけでなく依存グラフまたは選択ルールも修正します。

TIAの評価では、削減したテスト数より、選択漏れ率とフィードバック時間を重視します。常に全量を実行する安全性と、変更に関係ないテストを毎回実行しない速度を、二つの経路で両立させます。

  • 変更ファイルの近さではなく、targetの依存関係から影響範囲を求める。
  • 動的依存、生成物、共通設定、データ契約には保守的なfallbackを設ける。
  • mainまたはNightlyの全量テストで、PRの選択漏れを継続的に監査する。
選択経路と網羅経路を分ける概念例
pull_request:
  affected_targets = reverse_deps(changed_targets)
  run(builds_and_tests(affected_targets))

main_or_nightly:
  run(all_required_builds_and_tests)
  compare(selection_log, full_suite_failures)
  repair_dependency_or_selection_rules()

ARMとSpot VMは、互換性と中断耐性でランナープールを分けます

ARM系CPUは、対象のコンパイラ、依存バイナリ、コンテナbase image、テストツールがarm64へ対応していれば、有力な選択肢です。単価だけで決めず、x86とarm64で同じ入力から期待する成果物とテスト結果が得られるか、処理時間と転送量を含むprice-performanceが改善するかを実測します。アーキテクチャをまたぐキャッシュは、toolchainとplatformをcache keyへ含めて混同を防ぎます。

Spot VMは、提供側が任意の時点で回収でき、容量が常に確保される保証もありません。そのため、短い単体テスト、分割可能なbuild、再試行しても副作用を起こさないジョブに使います。本番リリースの最終ゲート、migration、外部状態を書き換える処理は、通常ランナーへ残します。

Spotを使っても無制限に並列化できるわけではありません。quota、容量、キュー、キャッシュ帯域、外部サービスのrate limitを上限として扱います。Spotが不足した時は、待機、別zoneまたは別machine type、通常ランナーへの限定fallbackを選べるようにし、全ジョブが同時に再試行するretry stormを避けます。

移行は、同じジョブをx86とARMで一定期間比較し、成功率、P95、CPU分、成果物差分を確認してから広げます。Spotも中断を意図的に発生させ、再キュー、重複防止、途中成果物の扱いを検証します。

  • ARM候補: ネイティブ依存がarm64対応し、同一性または許容差を検証できるbuildとtest。
  • Spot候補: 短時間、再入可能、冪等、分割可能で、中断後に安全に再実行できる処理。
  • 通常ランナー: release gate、migration、外部write、厳しい完了時刻を持つ処理。
  • 共通上限: quota、capacity、queue、cache bandwidth、外部APIのrate limit。

Admission Queueで、PR、main、Nightlyの待ち行列を分けます

リソースが十分な時は、どのジョブもすぐ実行できます。Admission Queueが必要なのは、混雑時に何を先に通し、何を待たせるかを明示するためです。開発中のPR、安定ブランチ、Nightly、releaseを別のclassにし、priorityだけでなく予約枠と最大同時数を持たせます。

PRを常に最優先にすると、継続的にPRが届く環境でNightlyが永久に始まらない飢餓が起きます。一定時間待ったジョブの優先度を上げるaging、classごとの最低予約枠、未使用quotaの貸し借り、最大待ち時間を組み合わせます。ファストトラックは低優先ジョブを存在しないものにする仕組みではありません。

実行中ジョブを強制終了して高優先ジョブを入れるpreemptionは、失った計算量と再実行費用を増やします。まずは非preemptiveな優先キューで、空いた枠を高優先ジョブへ渡します。preemptionを使う場合は、中断可能なジョブだけを対象にし、回収できる進捗と失うCPU分を計測します。

  • 見る指標: class別のqueue P50・P95・最大値、実行数、キャンセル数、飢餓時間。
  • 調整手段: priority、予約枠、最大同時数、aging、quota borrowing。
  • preemptionは最後の手段とし、中断で失ったCPU分と再実行費用を記録する。
混雑時の入場制御を表す概念例
queues:
  pull_request:
    reserved_capacity: 60%
    max_concurrency: bounded
    aging: enabled
  main:
    reserved_capacity: 25%
    aging: enabled
  nightly:
    reserved_capacity: 15%
    borrow_unused_capacity: true

safety:
  max_queue_age: required
  preemption: interruptible_jobs_only

Fail-Fastは、止める失敗と集める失敗を分けます

すべての失敗で全ジョブを即時停止すると、最初のflaky testや一時的なネットワーク障害で診断情報が不足します。Fail-Fastに向くのは、コンパイル不能、依存解決不能、必須設定の欠落、確定したlint違反など、下流が成功しないことが明らかな失敗です。

独立したテストの失敗をまとめて把握したい場合は、同じstage内で失敗を収集し、後続の高コストstageだけを止めます。Bazelの--keep_going相当を使うかは、追加で得られる診断情報と消費するCPU分の比較で決めます。PRでは速い必須チェックを先に置き、長いテストやイメージ作成をその後へ依存させます。

キャンセル処理も検証対象です。親ジョブが失敗しても子プロセス、VM、コンテナ、リースが残れば費用は止まりません。キャンセル伝播、grace period、ログとテストレポートの保存、後片付け完了までをFail-Fastの完成条件にします。

  • 即停止: 下流の成功可能性がなく、再試行しても結果が変わらない前提失敗。
  • 収集継続: 独立テスト、flaky判定中の失敗、追加診断の価値が高い失敗。
  • 停止対象: 後続stage、子プロセス、VM、コンテナ、leaseまでキャンセルを伝播する。
  • 保存対象: 根本原因を再現できるログ、終了コード、テストレポート、選択target。

Pre-warmingは、起動時間をゼロではなく予算内へ近づけます

環境キャッシングでは、runner image、toolchain、依存パッケージ、頻出するbase imageを事前に用意します。ただし、すべてを常時暖めると、使われないVM、古いcache、storage、定期更新の費用が増えます。起動時間を完全にゼロにするのではなく、SLOを満たす最小のwarm poolを維持する設計にします。

暖機対象は利用頻度と取得時間から選びます。数GBのcacheを毎回ネットワークから復元する方がローカルbuildより遅い場合もあります。cache hit率だけでなく、restore時間、転送byte、展開CPU、eviction率を記録し、小さく再利用性の高い層を優先します。

需要には時間帯の偏りがあります。PR到着率とキュー長からwarm poolを増減し、夜間や休日は下限を下げます。toolchainやimageを更新した時は古いwarm instanceを段階的に交換し、異なる版が混ざらないよう起動時にdigestとtoolchain IDを検査します。

  • 事前配置候補: runner image、固定toolchain、頻出base image、再利用率の高い依存層。
  • 評価指標: provisioning時間、warm hit率、cache restore時間、転送量、idle費用。
  • 無効化: image digest、lockfile、toolchain ID、CPU architectureをcache keyへ含める。
  • 容量制御: 到着率とqueue長でwarm poolを増減し、常時最大数を維持しない。

改善は、無駄を減らしてから安い計算資源へ移します

複数の施策を同時に入れると、速度や費用が変わった理由を切り分けられません。最初にメトリクスを取り、次にtargetとテスト選択で総仕事量を減らします。その後にpriority queueで混雑を制御し、互換性を確認したジョブからARMとSpotへ移します。最後にFail-FastとPre-warmingを調整します。

各段階で、P50・P95フィードバック時間、queue時間、vCPU分、cache hit、転送量、再実行率を同じ方法で比較します。P50だけが改善しP95が悪化した場合、通常時は速くても混雑や中断に弱い設計です。費用が下がっても失敗検出が遅れた場合は、必須チェックの順序または予約枠を見直します。

展開は一部のtargetまたは一つのrunner poolから始め、従来経路へ戻せるようにします。ARMの結果差分、Spotの再試行増加、TIAの選択漏れ、queueの飢餓、warm poolのアイドル費用が基準を越えたら、自動拡大を止めて原因を確認します。

  • 第1段階: 区間時間、総CPU分、転送量、再実行、請求を計測する。
  • 第2段階: target分割とTIAで、実行しない仕事を増やす。
  • 第3段階: Admission Queueで、混雑時の優先度と公平性を制御する。
  • 第4段階: ARM・Spotを互換性と中断耐性のあるジョブへ段階導入する。
  • 第5段階: Fail-FastとPre-warmingを、診断価値と待機費用から調整する。

速く安く見えても、検証を外すと別の費用が増えます

細粒度targetは、依存が正確でなければキャッシュの再利用ではなく選択漏れを増やします。TIAも同様で、動的依存を捉えられないまま全量経路を外すと、障害の発見が本番へ移り、CIで節約した額を上回る復旧費用につながります。

Spotは中断のたびに安くなるわけではありません。長いジョブの終盤で中断し、最初から再実行すれば、通常VMより多くのCPU分を消費する場合があります。チェックポイントが高価なら、ジョブ自体を小さく分け、完了済みactionをcacheから再利用する方が単純です。

優先キューは、重要度の定義を誤ると組織的な飢餓を作ります。Pre-warmingは、需要予測が外れると待機費用を固定化します。どちらも設定値を一度決めて終わりにせず、queue age、idle率、再実行率、PR到着率から定期的に調整します。

  • 小規模で実行時間が短い場合、Bazel移行や専用queue基盤の運用費用が効果を上回ることがある。
  • ARM非対応のネイティブ依存やx86専用toolchainは、通常ランナーへ残す。
  • Spot容量が不足する時の待機とfallbackを決め、無制限retryをしない。
  • TIAの全量照合、queueのaging、warm poolのidle上限を安全弁として残す。

持ち帰る判断軸は、最大並列数より総仕事量です

CI/CD高速化とコスト最適化は、別々の施策ではありません。変更に関係ない仕事を実行せず、必要な仕事を依存関係に沿って小さく分け、重要なフィードバックを先に通せば、経過時間と総計算量を同時に減らせます。

ARMとSpot VMは、その後で単価とprice-performanceを改善する手段です。互換性、中断、quota、cache帯域を無視して並列数だけを増やしても、P95と再実行費用は下がりません。Fail-FastとPre-warmingも、停止条件と待機予算を定義した時に初めて効果を安定させられます。

最初に見る数字は『何台まで並べられるか』ではなく、『一つのPRへ何CPU分を使い、最初の有効な失敗を何分で返したか』です。この二つを同じダッシュボードで追うことが、速さと費用を同時に改善する出発点になります。

製品固有の挙動は公式資料で確認します

以下は2026年8月10日時点で確認した公式一次資料です。Bazelの推奨構成、remote cache、Spot VMの価格と中断条件、ARM環境、Kueueの公平な入場制御は更新される可能性があります。実際の導入前には、利用するversion、region、machine type、quota、料金を現行資料で再確認します。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ