GitHub Actions のランナー移行とは、ワークフローの runs-on ラベルを変更し、実行環境を macOS や Windows から Linux(Ubuntu 等)へ切り替えることで、実行時間枠の消費レートを最適化する運用手法です。

Private リポジトリで CI を回していると、macOS ランナーの課金負担は無視できない課題になります。GitHub Actions の公式料金表によると、Linux 2-core (x64) が 1 分あたり $0.006 であるのに対し、macOS 3-core / 4-core は 1 分あたり $0.062 に設定されており、分単価の比率は約 10.3 倍 に達します1。さらに課金だけでなく、macOS arm64 ランナー特有のインフラ容量制限によるキュー枯渇も深刻な問題です。筆者が運用する Private リポジトリの CI 実行ログ(run 39649785823)では、ジョブが 15 分 1 秒間キューで待たされた末に、The job was not acquired by Runner of type hosted even after multiple attempts というアノテーションを残してタイムアウト失敗しました。

通常時(run 39690862495)の所要時間は 1 分 19 秒(ジョブ実行時間は 1 分 17 秒)ですが、10 倍レート換算では 1 回のプッシュで約 13 分相当のランナー枠を消費します。自律型エージェントとの協業により 1 日 40 回前後のプッシュが発生する開発運用では、1 日で約 520 分相当の枠が消費され、ランナー不足で CI が赤(ビルド失敗)になる事態が頻発していました。

そこでコストを 1/10 に圧縮し、キュー枯渇を回避するため、ワークフローを ubuntu-latest へ移行することを決断しました。しかし、単にラベルを書き換えるだけでは済まず、3 つの実務トラップに直面することになりました。

Private リポジトリでの継続的な CI 運用において、macOS ランナーと Ubuntu ランナーには実行コストと安定供給の面で顕著な差が存在します。

比較軸macOS ランナー(arm64)Ubuntu ランナー(ubuntu-latest + キャッシュ)
実行単価(分単価)$0.062 / 分(基準の約 10.3 倍)$0.006 / 分(基準単価)
キュー供給の安定性容量制限による枯渇リスクあり(15 分タイムアウト実測)実測において容量制限による待機がなく即時実行
外部ツールの導入Homebrew 等で最新版を導入可能公式 apt はバージョンが古い場合あり(自前ビルド要)
初回セットアップ時間パッケージ導入のみ(全体 1 分強)初回ビルドで約 6 分半(Taskwarrior 3 実測)
キャッシュヒット時-ビルドステップ全スキップにより短縮見込み(※所要時間は未計測)

単価を 1/10 に抑え、キュー枯渇を回避できる一方で、Ubuntu では最新ツールを自前ビルドしてキャッシュする設計が必要となります。

なぜ Ubuntu 移行とキャッシュ設計を採用したのか:実務で直面した3つの罠

罠1: Ubuntu LTS の apt パッケージの古さとバイナリ不在

macOS 環境では Homebrew 経由で最新ツールを簡単に導入できていましたが、Ubuntu ランナーへ切り替えた瞬間に最初の壁にぶつかりました。テストの前提ツールとして要求される Taskwarrior 3 が、Ubuntu LTS の公式 apt リポジトリに存在しなかったのです。

Ubuntu 24.04 LTS (Noble) の公式パッケージを確認すると、バージョンは 2.6.2+dfsg-1build2 で止まっています2。次期 LTS である Ubuntu 26.04 (Resolute) のパッケージ情報を見ても 2.6.2+dfsg-3 であり、依然として 2 系のままです3。

さらに GitHub Releases の配布アセットを確認したところ、提供されているのはソースコードの tarball(task-3.5.0.tar.gz、約 947 KiB)のみで、Linux 向けのコンパイル済みバイナリは配布されていませんでした4。

そこで CI ワークフロー側でソースビルドを行い、その成果物を actions/cache@v65 でキャッシュ保存する方針を採りました。初回のみビルドを行い、2 回目以降はビルドを丸ごとスキップする構成です。

説明用コードとして、apt に存在せずバイナリ配布もないツールをソースから cmake ビルドし、actions/cache@v6 で版ごとにキャッシュ保存するワークフローのステップを示します。

引用元: プロジェクトA(非公開)の .github/workflows/test.yml(抜粋)

      - id: task-cache
        uses: actions/cache@v6
        with:
          path: ~/.local/taskwarrior
          key: taskwarrior-${{ runner.os }}-${{ runner.arch }}-${{ env.TASKWARRIOR_VERSION }}
      - if: steps.task-cache.outputs.cache-hit != 'true'
        run: |
          sudo apt-get update && sudo apt-get install -y uuid-dev
          curl -fsSL "https://github.com/GothenburgBitFactory/taskwarrior/releases/download/v${TASKWARRIOR_VERSION}/task-${TASKWARRIOR_VERSION}.tar.gz" | tar xz
          cmake -S "task-${TASKWARRIOR_VERSION}" -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$HOME/.local/taskwarrior"
          cmake --build build -j"$(nproc)"
          cmake --install build
      - run: echo "$HOME/.local/taskwarrior/bin" >> "$GITHUB_PATH"
      - run: task --version

このステップでは、~/.local/taskwarrior をキャッシュ対象とし、キャッシュミス時(cache-hit != 'true')のみビルドステップを実行してバイナリを $GITHUB_PATH に通します。必要な依存パッケージ(uuid-dev)のみを apt で最小限インストールしています。

罠2: MSRV の思い込みと一次ソース(Cargo.toml)の実測乖離

次に直面したのが、言語ツールチェーンのバージョン要件(MSRV: Minimum Supported Rust Version)をめぐる罠でした。

当初、ワークフローのコメントには「ランナー標準の cmake と rust(MSRV 1.88)を使い」と書かれていました。しかし、実際にジョブを動かすとコンパイルエラーが発生して停止してしまったのです。

この「1.88」という数値について、書き手自身にも記憶がありませんでした。当時エージェントと協働してワークフローを組んでいたため、エージェントが推測で記述した可能性が高いと考えられます。

誰かがどこかに書いた誤りや勘違いが AI によって広まってしまう構造がある現代において、Web の二次解説や AI の提示を鵜呑みにせず、一次ソースで確かめる姿勢が不可欠です。

そこで配布 tarball 内の一次ソースコード(src/taskchampion-cpp/Cargo.toml)を直接開いて確認したところ、6 行目に次の記述がありました6。

rust-version = "1.91.1"   # MSRV

実際の要件は 1.88 ではなく 1.91.1 だったのです。GitHub Actions の Ubuntu ランナーにプリインストールされている Rust のバージョンがこの要件を満たしていなかったため、ビルドが失敗していました。

一次ソースの実測で判明した MSRV 1.91.1 を満たすため、ビルド直前に rustup で stable へ引き上げる修正を加えました。

引用元: プロジェクトA(非公開)の .github/workflows/test.yml(抜粋)

      - if: steps.task-cache.outputs.cache-hit != 'true'
        run: |
          rustup update stable && rustup default stable
          sudo apt-get update && sudo apt-get install -y uuid-dev
          curl -fsSL "https://github.com/GothenburgBitFactory/taskwarrior/releases/download/v${TASKWARRIOR_VERSION}/task-${TASKWARRIOR_VERSION}.tar.gz" | tar xz
          cmake -S "task-${TASKWARRIOR_VERSION}" -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$HOME/.local/taskwarrior"
          cmake --build build -j"$(nproc)"
          cmake --install build

修正後のプッシュで実行された run 39736993347 のログ出力は以下の通りです。

✓ test in 6m24s (ID 119684902669)
- "The ubuntu-latest label will migrate to Ubuntu 26 beginning October 19, 2026. For more information, see https://github.com/actions/runner-images/issues/14748"

所要時間 6 分 24 秒でジョブは無事に成功し、初回ビルドとキャッシュ保存が完了しました。ランナー標準の Rust ツールチェーンの版数に依存せず、ビルド直前に rustup update stable && rustup default stable を実行してバージョン要件を確実に満たす設計が重要になります。

罠3: キャッシュキーにおける ${{ env.ImageOS }} の空振りと $GITHUB_ENV 転写

ビルドが成功して安心したのも束の間、成功時のログに表示された 1 行のアノテーションが第 3 の罠の引き金となりました。

The ubuntu-latest label will migrate to Ubuntu 26 beginning October 19, 2026. For more information, see https://github.com/actions/runner-images/issues/14748

GitHub Actions の公式 Issue #14748 によると、ubuntu-latest は 2026 年 10 月 19 日から Ubuntu 26.04 への移行が順次開始され、11 月 19 日までに完了する予定とアナウンスされていました7。

もし Ubuntu 24 環境でビルドしたバイナリ(libuuid 等の共有ライブラリに依存)を、移行後の Ubuntu 26 ランナーがキャッシュからそのまま引き当ててしまうと、共有ライブラリの ABI 不整合によって実行時にサイレントクラッシュするリスクが生じます。

CI 実行ログにプラットフォーム側から直接届く事前シグナルは非常に便利であり、環境更新によるサイレントな破壊を先回りで防ぐための貴重な一次情報として見逃さずに活かすべきです。

そこで、OS のメジャーバージョン差分をキャッシュキーに組み込み、ランナーが切り替わった際には自動的にキャッシュミスさせて再ビルドが走るように設計することにしました。ランナーイメージ構築スクリプト(configure-environment.sh)を確認すると、イメージ構築時に /etc/environment へ ImageOS という環境変数が設定されていることがわかります8。

この変数を利用するため、キャッシュキーを次のように記述しました。

key: taskwarrior-${{ runner.os }}-${{ runner.arch }}-${{ env.ImageOS || 'unknown' }}-${{ env.TASKWARRIOR_VERSION }}

ところが、実際に生成されたキャッシュキーを確認すると、期待した ubuntu24 ではなく unknown にフォールバックし、taskwarrior-Linux-X64-unknown-3.5.0 と評価されてしまっていたのです。

原因は GitHub Actions の式構文における コンテキストスコープ仕様 にありました。GitHub Actions の公式ドキュメントには次のように明記されています9。

“The env context contains variables that have been set in a workflow, job, or step. It does not contain variables inherited by the runner process.”

つまり、ワークフローの式(${{ ... }})内で参照できる env コンテキストには、ワークフローファイル自身で定義された変数や $GITHUB_ENV に明示的に書き込まれた変数が入り、ホストランナープロセスの OS 環境変数は含まれません。そのため、${{ env.ImageOS }} は常に空文字列と判定され、フォールバックの 'unknown' が採用されていたのです。

解決策は、シェルステップ内で OS 環境変数 $ImageOS を読み取り、公式の手順に従って $GITHUB_ENV へ転写することです10。

引用元: プロジェクトA(非公開)の .github/workflows/test.yml(抜粋)

      - run: echo "IMAGE_OS=${ImageOS:-unknown}" >> "$GITHUB_ENV"
      - id: task-cache
        uses: actions/cache@v6
        with:
          path: ~/.local/taskwarrior
          key: taskwarrior-${{ runner.os }}-${{ runner.arch }}-${{ env.IMAGE_OS }}-${{ env.TASKWARRIOR_VERSION }}

修正後のプッシュで実行された run 39768918905 のステップログ出力は以下の通りです。

IMAGE_OS: ubuntu24
Cache saved with key: taskwarrior-Linux-X64-ubuntu24-3.5.0

シェルステップ内で $ImageOS を取得して $GITHUB_ENV に追記したことで、後続ステップの式構文から ${{ env.IMAGE_OS }} として正しく ubuntu24 が参照され、意図通りのキャッシュキーが生成・保存されました。

この修正コミットは直前の試行からわずか 51 秒後に行われており、書き手自身にも手動でデバッグした記憶がありません。エージェントが自律検証によって仕様の食い違いを即座に検知し、修正したと推測されます。人間が細部の仕様をすべて記憶していなくても、仕組みによって自律的に誤りに気づき正しく修正できることこそが、協業基盤が健全に機能している証拠といえます。

メリットの裏にあるトレードオフと適用限界

macOS ランナーから Ubuntu ランナーへの移行と、ソースビルド+actions/cache@v6 構成の導入には明確なメリットがありますが、同時に運用上のトレードオフと適用限界が存在します。

第 1 に、初回ビルド時間のオーバーヘッドです。Ubuntu ランナーの実行単価は 1/10 に抑えられますが、キャッシュが存在しない初回実行時には、Taskwarrior 3 の cmake ビルドに約 6 分半(実測 6 分 24 秒)を要します。2 回目以降はキャッシュヒットによりビルド全体がスキップされるため大幅な時間短縮が見込めますが(※キャッシュヒット時の所要時間自体は未計測)、ブランチの新設やキャッシュ失効時には初回ビルドの待ち時間が発生します。

第 2 に、GitHub Actions のキャッシュ失効ポリシーと容量制限です。公式仕様により、7 日間アクセスされなかったキャッシュエントリは自動的に退役(削除)されます11。また、リポジトリ全体でのキャッシュサイズ上限は 10 GB に制限されています。プッシュ頻度が極端に低いリポジトリや、膨大なキャッシュを消費する大規模プロジェクトでは、キャッシュミスが頻発して再ビルドのコストが嵩む可能性があります。

第 3 に、事前ビルド済み Docker コンテナイメージ配布との比較です。「なぜ Docker Hub や GitHub Packages にビルド済みコンテナイメージを用意して container: で動かさないのか」という代替案も考えられます。しかし Private リポジトリにおいてコンテナ運用を採用すると、レジストリ認証用シークレットの管理、ベース OS の脆弱性パッチ適用、コンテナイメージを自動更新するための別ワークフローの維持など、運用オーバーヘッドが急増します。単一の依存ツールを解決する目的であれば、ワークフロー内で完結する actions/cache 構成の方が、長期的な保守コストを格段に低く抑えられます。

第 4 に、OS メジャー更新時の挙動です。今回のようにキャッシュキーに OS バージョン(ubuntu24)を組み込む設計は、ランナーが Ubuntu 26 に切り替わった際に ABI 不整合のバイナリを読み込まず、クリーンに再ビルドを行わせるための安全装置として機能します。一方で、移行直後の最初のビルドジョブでは必ずキャッシュミスが発生し、約 6 分半のビルド時間が加算される点は事前に運用計画として織り込んでおく必要があります。

前提が変われば結論はどう変わるか

本記事で採用した設計は、現在の環境制約に基づいた最適解です。したがって、将来的に前提条件が変化すれば、選択すべき構成も変わります。

まず、Ubuntu の公式 apt リポジトリや信頼できる PPA で Taskwarrior 3 系が標準提供されるようになれば、自前での cmake ビルドやキャッシュ設計は一切不要になり、sudo apt-get install の 1 行で完結します。ただし、次期 LTS である Ubuntu 26.04 (Resolute) のパッケージ情報でも依然として 2.6 系が維持されている現状を踏まえると、短期間で apt 化される可能性は低いと考えられます。

次に、GitHub Releases で Linux 向けのコンパイル済みバイナリが配布されるようになった場合です。バイナリ配布が開始されれば、重いコンパイル処理を行わずに直接 curl して展開するだけの軽量なステップへ移行できます。

また、GitHub Actions のランナー仕様が拡充され、ホストランナーの OS バージョンが runner.os_version のようなコンテキストで直接公開されれば、$GITHUB_ENV への明示的な転写ステップを挟む必要はなくなります。

前提技術が進化すれば個別のワークフロー記述は変化しますが、今回の実践から得られた 2 つのコア教訓は色褪せません。

1 つ目は、AI や Web 検索が提示する「それらしい設定記法」を鵜呑みにせず、公式ドキュメントの仕様定義や配布ソースの一次実測値で確かめる姿勢 です。MSRV の乖離や式構文のコンテキストスコープは、二次解説をなぞるだけでは決して見抜けない落とし穴でした。

2 つ目は、CI 実行ログのアノテーションのように、プラットフォーム側から直接届く事前シグナルを見逃さずに先回りで活かす設計思想 です。予告された環境更新を事前に察知し、キャッシュキーに OS 識別子を組み込んでおいたことで、将来の予期せぬ環境破壊を未然に防ぐ堅牢なパイプラインを構築できました。

よくある質問(FAQ)

Q. actions/cache の key に ImageOS を直接指定しても unknown になるのはなぜですか?

A. GitHub Actions の式構文における env コンテキストにはホストランナープロセスの環境変数が含まれないためです。${{ env.ImageOS }} は常に空文字列と評価されます。ホスト環境変数をキーに含めるには、事前ステップで echo "IMAGE_OS=${ImageOS:-unknown}" >> "$GITHUB_ENV" を実行してコンテキストに明示的に転写する必要があります。

Q. GitHub Actions で macOS ランナーから Ubuntu ランナーへ移行するとどれくらいコストを削減できますか?

A. 実行分数あたりの単価が約 1/10(約 10.3 倍の単価差)に削減されます。GitHub 公式の料金表では、Linux 2-core が $0.006/分であるのに対し、macOS 3-core / 4-core は $0.062/分です。さらに、macOS arm64 ランナー特有の容量制限によるキュー待ちタイムアウト失敗を回避し、CI 実行の安定供給が得られます。

Q. Ubuntu LTS の apt にない最新ツールを CI で高速に動かすにはどうすればよいですか?

A. 初回実行時にソースコードをビルドして成果物を actions/cache に保存し、2 回目以降はビルドをスキップする構成にします。初回ビルドに数分のオーバーヘッドが発生しますが、キャッシュヒット時は重いコンパイル処理をスキップでき、Private リポジトリでのコンテナイメージ管理のような追加の運用コストを負わずに済みます。

まとめ

macOS ランナーから Ubuntu ランナーへの移行とキャッシュ設計において、現場で確認すべき判断軸を整理します。

  • macOS ランナーから Ubuntu ランナーへの移行は、分単価を約 1/10 に圧縮し、キュー待ちタイムアウトを回避する強力な手段である。
  • Ubuntu LTS の apt や GitHub Releases に依存バイナリがないツールは、初回ソースビルドと actions/cache@v6 の組み合わせで最小限の運用コストで高速化できる。
  • ツールの MSRV やバージョン要件は、ドキュメントや Web・AI の二次解説を過信せず、配布ソース(Cargo.toml 等)の一次実測値から特定しなければならない。
  • GitHub Actions の式における env コンテキストにはランナーの OS 環境変数は入らないため、${{ env.ImageOS }} をキャッシュキーに使うには $GITHUB_ENV への明示的な転写が不可欠である。
  • CI ログに届くプラットフォーム移行のアノテーションは、将来のサイレントな環境破壊を防ぐ貴重な一次シグナルとして先回りで活用すべきである。

CI/CD パイプラインの最適化、GitHub Actions のコスト削減やランナー移行設計、自律型 AI エージェントの運用基盤構築について、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. About billing for GitHub Actions GitHub, Inc. 公式ドキュメント(2026年11月確認)。Linux 2-core ($0.006/分) と macOS 3/4-core ($0.062/分) の料金単価比率(約 10.3 倍)は料金表セクション。 ↩

  2. Ubuntu packages: taskwarrior (noble) Canonical Ltd. 公式パッケージ情報(2026年11月確認)。Ubuntu 24.04 LTS (noble) における公式パッケージバージョンが 2.6.2+dfsg-1build2 であることはパッケージ詳細ページ。 ↩

  3. Ubuntu packages: taskwarrior (resolute) Canonical Ltd. 公式パッケージ情報(2026年11月確認)。Ubuntu 26.04 (resolute) における公式パッケージバージョンが 2.6.2+dfsg-3 であることはパッケージ詳細ページ。 ↩

  4. Taskwarrior Release v3.5.0 GothenburgBitFactory リリース情報(2026年11月確認)。リリース v3.5.0 の存在確認(アセットがソース tarball のみで Linux バイナリが非配布である点は一次検証ログに依拠)。 ↩

  5. actions/cache README GitHub, Inc. 公式リポジトリ README(2026年11月確認)。キャッシュキーにおけるコンテキスト参照や前段ステップ出力をキーに組み込む仕様は「Creating a cache key」セクション。 ↩

  6. Taskwarrior v3.5.0 Cargo.toml GothenburgBitFactory ソースコード(2026年11月確認)。Taskwarrior 3.5.0 の MSRV が 1.91.1 であることは 6 行目の rust-version = "1.91.1"。 ↩

  7. runner-images Issue #14748 GitHub, Inc. 公式 Issue(2026年10月)。ubuntu-latest の Ubuntu 26.04 への移行が 2026年10月19日開始、11月19日完了予定である移行アナウンスは Issue 本文。 ↩

  8. runner-images configure-environment.sh GitHub, Inc. 公式リポジトリ(2026年11月確認)。ImageOS がランナーイメージ構築時に /etc/environment に設定される OS 環境変数である実装は set_etc_environment_variable "ImageOS" の定義箇所。 ↩

  9. Contexts reference (env context) GitHub, Inc. 公式ドキュメント(2026年11月確認)。env コンテキストがワークフロー・ジョブ・ステップで設定された変数を含み、ランナープロセスが継承した環境変数を含まない仕様は「env context」セクション。 ↩

  10. Workflow commands for GitHub Actions (Setting an environment variable) GitHub, Inc. 公式ドキュメント(2026年11月確認)。$GITHUB_ENV への書き込みによって後続ステップに環境変数を引き渡す手順は「Setting an environment variable」セクション。 ↩

  11. Caching dependencies to speed up workflows GitHub, Inc. 公式ドキュメント(2026年11月確認)。キャッシュの 7 日間非アクセス失効ポリシーおよびリポジトリあたり 10 GB の制限は「Usage limits and eviction policy」セクション。 ↩