Scheduled Scaling する Kubernetes Custom Controller を作った

11 min read読了の目安(約10000字

この記事は Kubernetes Advent Calendar 2020 の 22 日目の記事です。

はじめに

Kubernetes には Horizontal Pod Autoscaler (以下 HPA) という Pod を水平にスケールさせるためのリソースが存在します。HPA は非常に便利で Node のスケールを行う Cluster Autoscaler と組み合わせて運用しているところは多いのでは無いでしょうか。

ですがこの HPA も万能ではありません。Pod や Node のスケールには多少なりとも時間がかかってしまうため、いわゆるスパイクアクセスなどの突発的なアクセスには対応することが難しいです。この場合どうするかというと特定の時間にスパイクアクセスが発生することが分かっているのあればいつもより余分にリソースを用意しておく、のような対応をすることが多いのでは無いかと思います。HPA でいうとスパイクに備えて minReplicas を定常時よりも大きい値にしておくようなイメージです。この対応のことを以下 Scheduled Scaling と呼称します。

しかし上記の minReplicas を予め増やしておくという対応にも問題点があります。それはコスト的な問題があるためスパイクが発生する時刻の直前に Cluster に適用したいが、そのためには何らかの仕組みを作り込まないと行けないという点です。実現する仕組み自体は単純で cron で HPA の minReplicas を更新するような仕組みがあればよいです。雑に実装すると CronJob なりで API を叩いて更新するような仕組みが思いつきます。

実は似たようなことを実現する Kubernetes の Custom Controller は既に存在しており、

などを使用すると同様のことが実現できるはずです。

しかし GitOps を実践している場合は Git に置いてある manifest を更新しなければならず、Cluster の中から API を使用して更新をするだけでは不十分です。このモチベーションに対応できる Custom Controller は (当時) 観測範囲見つかりませんでした。ので、GitOps でも使用できる Scheduled Scaling を実現できる Custom Controller を自作しました。

scheduled-pod-autoscaler

https://github.com/d-kuro/scheduled-pod-autoscaler

scheduled-pod-autoscaler は 2 つの Custom Resource から構成されています。

親子関係は次のようになっています:

$ kubectl tree scheduledpodautoscaler nginx
NAMESPACE  NAME                             READY  REASON  AGE
default    ScheduledPodAutoscaler/nginx     -              6m5s
default    ├─HorizontalPodAutoscaler/nginx  -              6m5s
default    ├─Schedule/test-1                -              6m4s
default    ├─Schedule/test-2                -              6m4s
default    └─Schedule/test-3                -              6m4s

ScheduledPodAutoscaler

ScheduledPodAutoscaler は HPA をラップする Custom Resouce です。ScheduledPodAutoscaler の Controller はこのリソースから HPA を generate します。

ここで定義した HPA の spec は Scheduled Scaling が行われてない時に使用されます。

なぜ HPA をラップする必要があるのかというと、scheduled-pod-autoscaler は特定の時間において、HPA の maxReplicas/minReplicas を変更するというアプローチで Scheduled Scaling を実現しています。Git に HPA の manifest が管理されている場合、 Controller の中から Kubernetes API を使用して HPA の spec を変更しただけだと Git 上にある manifest は変更されていないので GitOps では不都合になります。HPA をラップするリソースを用いることで HPA の manifest 自体は Git 上では管理されなくなり、 API 経由で HPA の spec を変更しても問題ないという理屈です。

ラップしている HPA の spec は v2beta2 の API のものが使用できます。

apiVersion: autoscaling.d-kuro.github.io/v1
kind: ScheduledPodAutoscaler
metadata:
  name: nginx
spec:
  horizontalPodAutoscalerSpec:
    scaleTargetRef:
      apiVersion: apps/v1
      kind: Deployment
      name: nginx
    minReplicas: 3
    maxReplicas: 10
    metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
$ kubectl get spa # You can use spa as a short name of scheduledpodautoscaler.
NAME    MINPODS   MAXPODS   STATUS      AGE
nginx   3         10        Available   6m52s

Schedule

Schedule は Scheduled Scaling を定義するための Custom Resouce です。親の ScheduledPodAutoscaler に対して複数の Schedule を定義することができます。

ScheduledPodAutoscaler の Controller は Schedule を参照し、Scheduled Scaling の時間になると generate した HPA の maxReplicas/minReplicasSchedule の spec に基づいて書き換えます。この際に注意として、HPA の replicas が変更されたあと実際に Pod が起動するまでには時間がかかります。なのでスパイクが発生する時刻に Scheduled Scaling が行われるようにするのではなく、少し余裕を持たせ前もって Scheduled Scaling が行われるようにすると良いです。

複数の Schedule で時間範囲が競合している場合は、それぞれの Schedule で定義してある maxReplicas/minReplicas の最大値が使用されます。

$ kubectl get schedule -o wide
NAME     REFERENCE   TYPE      STARTTIME          ENDTIME            STARTDAYOFWEEK   ENDDAYOFWEEK   MINPODS   MAXPODS   STATUS      AGE
test-1   nginx       Weekly    20:10              20:15              Saturday         Saturday       1         1         Available   4m49s
test-2   nginx       Daily     20:20              20:25                                              2         2         Available   4m49s
test-3   nginx       OneShot   2020-10-31T20:30   2020-10-31T20:35          

Schedule は 3 つの Schedule Type をサポートしています。

type: Weekly

時刻を HH:mm のフォーマットで記述し、曜日を指定します。

apiVersion: autoscaling.d-kuro.github.io/v1
kind: Schedule
metadata:
  name: nginx-push-notification
spec:
  scaleTargetRef:
    apiVersion: autoscaling.d-kuro.github.io/v1
    kind: ScheduledPodAutoscaler
    name: nginx
  minReplicas: 10
  maxReplicas: 20
  type: Weekly
  startDayOfWeek: Monday
  startTime: "11:50"
  endDayOfWeek: Wednesday
  endTime: "13:00"
  timeZone: Asia/Tokyo

type: Daily

時刻を HH:mm のフォーマットで記述します。

apiVersion: autoscaling.d-kuro.github.io/v1
kind: Schedule
metadata:
  name: nginx-push-notification
spec:
  scaleTargetRef:
    apiVersion: autoscaling.d-kuro.github.io/v1
    kind: ScheduledPodAutoscaler
    name: nginx
  minReplicas: 10
  maxReplicas: 20
  type: Daily
  startTime: "11:50"
  endTime: "13:00"
  timeZone: Asia/Tokyo

type: OneShot

時刻を yyyy-MM-ddTHH:mm のフォーマットで記述します。

apiVersion: autoscaling.d-kuro.github.io/v1
kind: Schedule
metadata:
  name: nginx-push-notification
spec:
  scaleTargetRef:
    apiVersion: autoscaling.d-kuro.github.io/v1
    kind: ScheduledPodAutoscaler
    name: nginx
  minReplicas: 10
  maxReplicas: 20
  type: OneShot
  startTime: "2020-09-01T10:00"
  endTime: "2020-09-10T19:00"
  timeZone: Asia/Tokyo

Install

以下のコマンドでインストールできます。

# Kubernetes v1.16+
$ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/install.yaml

# Kubernetes < v1.16
$ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/legacy/install.yaml

その他細かい API spec や export している metrics などは README を参照してください。

ちょっとした内部の話

scheduled-pod-autoscaler は kubebuilder を使用して作成しています。

kubebuilder を用いた Custom Controller の作成方法は @zoetrope さんの つくって学ぶKubebuilder がとても素晴らしいのでぜひそちらを参照してみてください。

https://zoetrope.github.io/kubebuilder-training/

Controller

scheduled-pod-autoscaler で動いている Controller は ScheduledPodAutoscaler の Controller と Schedule の Controller の 2 つが存在します。基本的にメインのロジックはすべて ScheduledPodAutoscaler の Controller 側に書かれています

では Schedule の Controller が何をやっているのかと言うと、.spec.scaleTargetRef で指定した親の ScheduledPodAutoscaler との親子関係を表現するために ownerReference を付与しています。ここで付与した ownerReferenceインデックスを付与しており、ScheduledPodAutoscaler の Controller の中で子の Schedule をまとめて List する際に使用しています。個人的にここの処理が気になっていて、generate されるリソースでも無いのにownerReference を後付で付与するのはどうなんだとか、ownerReference を付けると親の ScheduledPodAutoscaler を消した時に子の Schedule もガベージコレクションされるのでそこもイケてないなーとか思っています。誰かこうしたほうがいいよとかあれば教えて貰えると喜びます。

CRD の API Version

インストールの例を見るとわかりますが Kubernetes の version 別に 2 種類のインストール方法を提供しています。

# Kubernetes v1.16+
$ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/install.yaml

# Kubernetes < v1.16
$ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/legacy/install.yaml

なぜ 2 種類提供しているかと言うと Kubernetes v1.16 で CRD が GA しており、API version が変わっているからです。

# Kubernetes v1.16+
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
...

# Kubernetes < v1.16
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
...

CRD などの manifest は controller-gen というツールで generate するのですが、そのバージョンによって generate される manifest の API version が変わります。なので 2 種類のバージョンを使用して manifest を generate しています。

Kubernetes のアップストリームの version も約 3 ヶ月ごとに更新されるかつ、各種クラウドプロバイダでサポートしている version にズレがあったりするのでこの辺は Custom Controller なりを書く際にどこの version までサポートするのかということに気を使わないといけなかったりで大変だな、となっています。(v1.16 より下のバージョンのサポートを切りたい)

今後について

基本的にもともとやりたかったことは実装できたので頻繁に機能追加していく、ということはあまり考えていないのですが強いて言えば Scale to Zero のサポートができたらいいなと考えています。Scale to Zero をサポートできると夜間などの特定時間に replicas を 0 にしてコストを節約するなどの運用が可能になります。

Scale to Zero ができると嬉しいのはそうなのですが、なぜ尻込みしているのかというと現在の
scheduled-pod-autoscaler の仕組みは generate した HPA の maxReplicas/minReplicas を制御することで Scheduled Scaling を実現しています。実は HPA 自体の Scale to Zero のサポートがまだ alpha で Feature Gates を使用して有効にしないと使用できないからです。

HPA の Scale to Zero の KEP はここにあります:

https://github.com/kubernetes/enhancements/pull/2022

なので HPA を使用したまま replicas を 0 にすることは現状できません。では、どうするかというと Scale to Zero する Scheduled Scaling の場合は generate した HPA を削除して直接 Deployment なりの replicas を Scale API を使用して 0 にし、Scheduled Scaling 終了後に HPA を再 generate して戻すみたいな実装は思いつくのですが複雑化もするしあんまり綺麗じゃないな、と思っています。何かいいアイデアがある方は教えていただけると嬉しいです。(このブログはこれが言いたかった)

その他機能追加要望や Bug の報告などは GitHub Issue にて報告していただければ反応します。