GitHub Actions Runnerの「飽和」を殺す:KEDAによるゼロレイテンシ・オートスケーリングの深淵
GitHub Actionsのセルフホストランナーを管理している諸君、君たちはまだ「静的なランナープール」という遺物と戦っているのか?
「朝のプルリクエスト殺到によるビルド待ち」「夜間の無駄なアイドルコスト」「スケールアップの遅延が引き起こすコンテキストスイッチ」。これらはすべて、設計の怠慢だ。GitHubが提供する`actions-runner-controller` (ARC) をただ導入するだけでは足りない。真のDevOpsエンジニアは、KEDA(Kubernetes Event-driven Autoscaling)を調教し、Kubernetesの深部を制御することで、コストとパフォーマンスの極限を叩き出す。
今日は、理論ではなく「現場で勝つための」設計思想とハックを伝授する。
—
1. 従来の「静的ランナー」という名の負債
多くの現場が犯す最大の過ちは、EC2やVMにランナーを常駐させることだ。
- アイドルコスト: 夜間や週末も稼働するランナーは、AWSの請求書に刻まれる無駄な墓標だ。
- スケーラビリティの硬直: 突発的な大規模ビルドが発生した際、既存のプールが枯渇すれば、ビルドは待ち行列(Pending)に放り込まれる。
- ノードの汚染: 実行環境が永続化されると、一時ファイルやキャッシュが蓄積し、`dirty environment`による再現不能なビルドエラーを誘発する。
これらを解決するための最適解が、「Kubernetes上のEphemeral Runner(使い捨てランナー)」である。
—
2. KEDAによるイベント駆動の真髄
ARCとKEDAを組み合わせることで、GitHubのWebhookやAPIを監視し、ジョブが投入された瞬間にPodを起動、完了と同時に消滅させるアーキテクチャを構築する。
アーキテクチャの核心
- RunnerDeployment: リポジトリや組織レベルでのランナーの定義。
- KEDA ScaledObject: GitHub APIをポーリングし、`queued_or_in_progress`のジョブ数を監視。
- Ephemeral Pods: ビルドごとにPodを生成。終了時は`SIGTERM`を受け取り、キャッシュを焼き払って消える。
構成の要:`ScaledObject`のチューニング
デフォルト設定ではスケールアウトが遅い。これを極限まで詰める。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: github-runner-scaler
spec:
scaleTargetRef:
name: arc-runner-deployment
pollingInterval: 5 # APIポーリング間隔を5秒まで絞る(GitHub API制限に注意)
cooldownPeriod: 300 # スケールインの猶予は短めに設定し、コストを削る
triggers:
- type: github-runner
metadata:
githubRunnerScaleTarget: “arc-runner-deployment”
runnerScope: “organization”
organization: “your-org-name”
# 特定のラベルのジョブのみを追跡する(重要!)
labels: “self-hosted,linux,x64”
—
3. パフォーマンスを極限まで引き上げる「ハック」
A. コンテナイメージのプリフェッチ (Warm-up)
Podが起動してからDocker Pullが走っていては遅い。`DaemonSet`を使い、ノード上にベースイメージを予め展開しておく「イメージ・ウォーミング」が必須だ。
ノード上の全イメージを管理するDaemonSetのイメージ
常に最新のランナーイメージをキャッシュしておく
docker pull ghcr.io/actions/actions-runner:latest
B. PVC(Persistent Volume)によるキャッシュ活用
ランナーが使い捨てであっても、`node_modules`や`maven`のレポジトリキャッシュまで消してはビルド時間が延びる。`Local PV`または`EFS CSI`をマウントし、`$RUNNER_TEMP/_github_workflow`を永続化せよ。
C. Pod Anti-Affinityによるリソース競合の排除
同じノードに複数の重いビルドが重なると、CPUの奪い合いでビルド時間が不安定になる。
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: “actions-runner-controller”
operator: In
values: [“true”]
topologyKey: “kubernetes.io/hostname”
—
4. 伝説的エンジニアの「最終兵器」:API直叩きオートスケーリング
KEDAの標準トリガーで足りない複雑なロジック(例:特定の開発者のジョブだけ優先したい、スポットインスタンスの空き状況に応じてランナー数を動的に制限したい)が必要な場合は、自作コントローラーをGoで書け。
GitHubの `CheckRuns` や `WorkflowJobs` API を定期的に叩き、Kubernetesの `Custom Resource` を直接操作するスクリプトをサイドカーとして動かすのだ。
// 擬似コード: GitHub APIを監視し、特定の優先度ジョブがあれば即座にスケールさせる
func ReconcileRunnerCount(ctx context.Context) {
jobs := githubClient.GetQueuedJobs(org, repo)
if len(jobs) > threshold {
k8sClient.ScaleDeployment(“arc-runner”, len(jobs))
}
}
—
5. 最後に:運用の哲学
このアーキテクチャの最大のメリットは、「エンジニアがインフラを意識しなくなること」だ。開発者は `runs-on: self-hosted` と書くだけで、無限に湧き出す計算資源を手に入れる。
- コスト: ゼロビルド時はほぼゼロ円(FargateやSpotノードを使用)。
- 速度: Podの起動時間は数十秒。
- 信頼性: 毎回クリーンな環境で実行されるため、ゴミ溜めのようなレガシーCI環境から脱却できる。
これこそが、DevOpsの到達点だ。ツールに振り回されるな。K8sとGitHub APIを掌の上で転がし、パイプラインを「動く芸術」へと昇華させろ。
何かあればまた聞け。君たちのパイプラインが、世界最速になるその日まで。