Scheduled Scaling する Kubernetes Custom Controller を作った
この記事は 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 は既に存在しており、
- amelbakry/kube-schedule-scaler: Kubernetes Controller which provides schedule scaling to Kubernetes deployments and other custom resources in the cluster
- lilic/kube-start-stop: Schedule Scaling of Kubernetes Resources
などを使用すると同様のことが実現できるはずです。
しかし GitOps を実践している場合は Git に置いてある manifest を更新しなければならず、Cluster の中から API を使用して更新をするだけでは不十分です。このモチベーションに対応できる Custom Controller は (当時) 観測範囲見つかりませんでした。ので、GitOps でも使用できる Scheduled Scaling を実現できる Custom Controller を自作しました。
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
/minReplicas
を Schedule
の 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 がとても素晴らしいのでぜひそちらを参照してみてください。
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 はここにあります:
なので HPA を使用したまま replicas を 0 にすることは現状できません。では、どうするかというと Scale to Zero する Scheduled Scaling の場合は generate した HPA を削除して直接 Deployment なりの replicas を Scale API を使用して 0 にし、Scheduled Scaling 終了後に HPA を再 generate して戻すみたいな実装は思いつくのですが複雑化もするしあんまり綺麗じゃないな、と思っています。何かいいアイデアがある方は教えていただけると嬉しいです。(このブログはこれが言いたかった)
その他機能追加要望や Bug の報告などは GitHub Issue にて報告していただければ反応します。
Discussion