本文へ移動

HPA のゼロスケールが v1.37 から既定有効に、CPU やメモリだけの HPA では使えない

v1.37 で HPA の minReplicas に 0 を書けるようになった。使えるメトリクスの条件、手動停止との区別、control plane を上げる途中の落とし穴を、queue consumer を持つチーム向けに整理する。

v1.37 の HPA は、minReplicas: 0 を feature gate の操作なしで受け付ける。HPAScaleToZero が Beta に上がり、kube-apiserver と kube-controller-manager の両方で既定有効になった。対象は object メトリクスか external メトリクスを持つ HPA だけで、CPU やメモリの resource メトリクスしか持たない HPA に 0 を書いても API サーバーが拒否する。

アドオンや外部コンポーネントで queue consumer や batch processor を 0 に落としているチームは、その役目が core に入ったことになる。GPU や専有 CPU を確保した Pod を待機させている構成では、最後の 1 Pod を消せる。control plane を段階的に上げている最中なら、先に落とし穴がある。両コンポーネントが揃う前に minReplicas: 0 の HPA を作ると workload が 0 のまま放置されうる。HTTP を受けるサービスは、この機能だけでは 0 にできない。

HPA 側は minReplicas を 0 にするだけ、ただしメトリクスに条件がある

2026年9月2日付の Kubernetes ブログに載っている例は、queue-worker という Deployment を Prometheus の queue_consumer_lag という external メトリクスで 0 から 10 replica の間で動かす。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: queue-worker
  annotations:
    kubernetes.io/description: "Scales queue-worker based on the number of queued tasks"
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: queue-worker
  minReplicas: 0
  maxReplicas: 10
  metrics:
  - type: External
    external:
      metric:
        name: queue_consumer_lag
        selector:
          matchLabels:
            name: worker_tasks
      target:
        type: Value
        value: "30"

target.type が Value で値が "30" なので、キューに溜まったタスク 30件ごとに 1 replica を要求する。キューが空なら 0 まで下げる。タスクが来れば、external メトリクスは Pod がなくても読めるので HPA が replica 数を計算し直し、maxReplicas の 10 で頭打ちになる。

resource メトリクスが使えない理由は単純で、CPU もメモリも動いている Pod から取る値だからだ。replica が 0 になった時点で測る対象がなくなり、戻すきっかけも消える。キューの長さは worker がいなくても存在するので、HPA は 0 の間も読み続けられる。API サーバーが minReplicas: 0 の HPA に object か external のメトリクスを最低一つ要求するのは、この制約の裏返しだ。

手動で 0 にした Deployment は HPA が起こさない

replica が 0 という状態には二つの意味がありうる。HPA が下げたのか、運用者が手で止めたのかだ。v1.36 で追加された ScaledToZero という status condition がこれを区別する。HPA が 1 以上から 0 へ下げたときに ScaledToZero=True を記録し、以降の reconcile はこの 0 を HPA の管理下として扱って object/external メトリクスを評価し続ける。戻したあとは ScaledToZero=False になり、reason は NotScaledToZero だ。

裏返すと、この condition なしで 0 になっている workload は「一時停止」のままだ。Deployment を手で 0 にすれば autoscaling が止まる挙動は以前から変わらず、HPA は自分が下げていないものを起こさない。Deployment は最低 1 replica で起動しておく必要がある。

状態は kubectl describe hpa queue-worker で見られる。adapter がメトリクスを返せなくなると ScalingActive=False になり、reason には FailedGetExternalMetric のような値が入る。この状態では 0 から自動で戻る手段がないので、メトリクスを復旧するか手動でスケールして capacity を取り戻す。

5分の安定化ウィンドウとコールドスタートは残る

0 まで下げられるようになっても、HPA の通常の挙動はそのまま乗る。scale down の安定化ウィンドウは既定で 5分あり、キューが一瞬空になっただけで全 worker が消えることを防ぐ。短くしたければ spec.behavior.scaleDown で調整する。

上げる側にはコールドスタートがある。HPA がメトリクスの変化を観測し、Pod をスケジュールし、アプリケーションが起動するまでの時間だ。仕事が durable なキューに溜まって待てるなら問題にならない。Kubernetes の Service は Ready な Pod がない間リクエストをバッファしないので、HTTP のようなリクエスト駆動の workload には別のバッファ層が要る。ブログはその層に何を使うかまでは示していない。

節約が大きいのは、Pod ごとに専有 CPU や GPU のような高価なリソースを確保している場合だ。最後の 1 Pod が待機しているだけで GPU を握り続ける構成なら、0 に落とす意味がある。

メトリクスの経路は HPA を作る前に叩いて確かめる

external メトリクスを HPA に見せるには metrics adapter が要る。ブログは Prometheus Adapter を一例として挙げ、externalRules で系列を公開する設定を載せている。

externalRules:
- seriesQuery: '{__name__="queue_consumer_lag",name!=""}'
  metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (name)
  resources:
    overrides:
      namespace:
        resource: namespace

adapter の導入手順や discovery の規則は監視基盤ごとに違う。HPA を作る前に、External Metrics API から値が返るかを直接確認する。

kubectl get --raw \
  '/apis/external.metrics.k8s.io/v1beta1/namespaces/default/queue_consumer_lag?labelSelector=name%3Dworker_tasks'

worker_tasks の現在値が返らなければ、メトリクスが取れない HPA は 0 から戻れないので、先にメトリクスの経路を直す。

control plane を段階的に上げるなら、HPA の作成は最後に回す

feature gate HPAScaleToZero は kube-apiserver と kube-controller-manager の両方に関わる。API サーバーが minReplicas: 0 を受け付け、controller manager が condition ベースのスケーリングを実行する。役割が分かれているので、片方だけ新しい状態が危ない。

version skew のあるアップグレード中は、両コンポーネントがこの機能を持ち、かつ有効になってから minReplicas: 0 の HPA を作る。gate が無効な controller manager は replica 0 を手動停止として扱うため、API サーバーが受理した HPA の workload が 0 のまま戻らないことがありうる。v1.37 の control plane では Storage Version Migration も既定有効で GA になっているので、gate の既定値が変わる項目はまとめて確認しておくと手戻りが少ない。

逆方向にも手順がある。gate を無効にする前、または condition ベースの実装がないバージョンへ戻す前に、次の二つを済ませる。

  • 該当する HPA の minReplicas を 1 以上に変える
  • 現在 0 になっている workload を最低 1 replica まで手で上げる

無効化後の controller manager は 0 を手動停止として扱うので、飛ばすと起こす主体がいなくなる。

v1.16 の Alpha から condition ベースの再設計を経て Beta へ

最初の Alpha 実装は v1.16 に入った。v1.36 で ScaledToZero condition と、自動の scale down と手動停止を区別する controller の挙動が加わり、v1.37 で external メトリクスから 0 へ下げて戻す integration test と e2e test を足した上で既定有効になった。GA は Beta での運用フィードバックを集めてから判断するとされている。機能のオーナーは SIG Autoscaling で、設計は KEP-2021 にある。

試すなら順番はこれで足りる

  • control plane が v1.37 に揃っているか確認する。skew 中は minReplicas: 0 の HPA を作らない
  • External Metrics API を kubectl get --raw で叩き、対象メトリクスの値が返ることを確かめる
  • Deployment を 1 replica 以上で起動してから HPA を適用する
  • kubectl describe hpa で ScaledToZero と ScalingActive の遷移を見る
  • ロールバック手順に「minReplicas を 1 以上へ戻し、0 の workload を手で上げる」を入れる

HTTP を受けるサービスをこの機能だけで 0 にするのは、Service がリクエストを保持しない以上、別のバッファ層なしには成立しない。バッファ層を持たないなら、対象は queue consumer と batch processor に絞るのが素直だ。

出典 種別について