Kubernetes × Actions Runner Controller:EphemeralRunner の自己回復を実現する PR #4059 徹底解説
1. 背景 ― なぜ本 PR が必要だったのか?
症状 | これまでの実装 |
---|---|
Spot インスタンス は時間が来たら終了し、EphemeralRunnerはそれを障害判定してしまう。 | EphemeralRunner の status.failures に 1 回ごとに失敗が蓄積。 |
Spotインスタンスを利用している状況下で minRunners ≥ 1 にしていると Spotインスタンスの中断に必ず遭遇してしまう。 |
5 回 失敗(中断)すると Phase=Failed で Runner CR が残留。 |
Workflow 側からは「Runner が見つからない」状態になりジョブが詰まる。 | Issue #2721 で約2年前から報告 |
2. これまでの制限と課題
-
失敗上限 5 回 という定数自体は 以前から 存在
-
上限到達後は
markAsFailed()
で Phase を変えるだけ だったため、- EphemeralRunnerSet が「Desired=1 だけど既にオブジェクトがある」と判断
- 新しい EphemeralRunner を生成できず “ゾンビ化” していた
3. PR #4059 で何が変わったのか?
変更点 | 役割 |
---|---|
status.failures の 型変更 map[string]bool → map[string]metav1.Time
|
失敗時刻を記録し 最新失敗を時系列で把握 |
failedRunnerBackoff 配列導入 |
0 s → 5 s → 10 s → 20 s → 40 s → 80 s の 指数バックオフ (GitHub) |
LastFailure() ヘルパー追加 |
Status から 直近失敗時刻 を取得 (GitHub) |
再キュー制御 | 失敗回数 n ≤ 5 なら RequeueAfter(backoff[n]) 相当の処理 (GitHub) |
上限超えの対処を変更 |
Phase 変更 ではなく EphemeralRunner CR を即 Delete() 。EphemeralRunnerSet が新品を再生成可 (GitHub) |
💡 ポイント
上限 5 回 という定数そのものは変わっていません
「上限到達後にどうするか」 が Phase=Failed → オブジェクト削除 に変わり、
EphemeralRunner の 自己回復 が可能になりました
4. 実際の Spot × minRunners シナリオでの動作
- 6 回目で CR 削除 → 再生成 するため “ゾンビ” は残らない
5. コード解説
1. 失敗を記録する Status フィールド
type EphemeralRunnerStatus struct {
// …既存フィールド
Failures map[string]metav1.Time `json:"failures,omitempty"`
}
func (s *EphemeralRunnerStatus) LastFailure() *metav1.Time {
// map を走査して最新タイムスタンプを返すヘルパー
}
- キー: Podの.metadata.uid
- 値: その失敗が起きた時刻
2. 指数的バックオフ & 再キュー
var failedRunnerBackoff = []time.Duration{
0, 5 * time.Second, 10 * time.Second,
20 * time.Second, 40 * time.Second, 80 * time.Second,
}
const maxFailures = 5
if len(er.Status.Failures) <= maxFailures {
delay := failedRunnerBackoff[len(er.Status.Failures)]
return ctrl.Result{RequeueAfter: delay}, nil
}
- 0 秒 → 5 s → 10 s … と再試行
- 6 回目(
len > maxFailures
)に到達すると次のセクションへ
3. 6 回目で即削除 → EphemeralRunnerSet が再作成
// 最終フェーズ
if len(er.Status.Failures) > maxFailures {
log.Info("Maximum failures reached – deleting EphemeralRunner")
if err := r.Delete(ctx, er); err != nil { ... }
return ctrl.Result{}, nil
}
- オブジェクト自体を Delete するので Controller の再試行は終了
- 上位リソース EphemeralRunnerSet が Desired 数を満たすために新しい EphemeralRunner を生成 → Pod → ジョブ再開
動作フローまとめ
6. まとめ
PR #4059 は Spot × minRunners 環境だと特に頻繁に遭遇する 「5 回中断すると詰む」 という長年の課題(Issue #2721)を解決しました
- 失敗を時系列で管理し、指数バックオフで再試行
- 上限超過時はEphemeralRunner CR を削除して EphemeralRunnerSet に再作成させる
- これにより フルマネージドに近い自己回復 が実現
- とはいえ ジョブが実行中にSpotの中断に遭遇して実行が阻害されるリスク は残っているので、Spotを利用する場合はその点にも注意する必要があります