kubernetesでatコマンドを実装する
kubernetesには、Horizontal Pod Autoscaler(HPA)と呼ばれるオブジェクトがある。
HPAは、特定のDeployment配下のPodの平均CPU使用率などの値によってDeploymentのPodの数(=replicas)を増減させてくれる仕組みだ。
基本的にkubernetesではこれによってトラフィックに応じたオートスケーリングを実現するのが一般的だ。
しかし、実際の運用の中では事前にトラフィックの急増が予測できたりする。
単純に仕事終わりの時間帯や、TVなどのメディアで取り上げられることがわかっていたり、特定の製品の発売日を迎たりする場合だ。
このような場合、事前にPodの数を増やしておくことでよりスムーズに大量のトラフィックに対応することができる。
このような時間ベースの変更のやり方として、kubernetesのCronJobを使う方法が考えられる。
Cronjobを使うとスケジュールに従って Job
を作成してくれる。JobはPodを作るので、Podの中でkubectl scale
相当のことを実施すれば事前に
Podの数を変更しておくことができる。
だが、このようなやり方ができるのはスケールイン/アウトが定期的である場合に限られる。 CronJobのスケジュールは通常のcronコマンドと同じで、分、時、日、週、月を指定して、特定のタイミングでの実行を繰り返すことになる。 このため、仕事終わりの時間帯に合わせてスケールアウトをするのには向いているが、製品の発売日やメディア露出 のためのスケールイン/アウトを実施するのには向いていない。
CronJob以外にも時間ベースでkubernetesをスケールさせてくれるための仕組みはあるが、いずれもcron準拠で定期的なものだった。
https://github.com/LiliC/kube-start-stop
https://github.com/amelbakry/kube-schedule-scaler
このような不定期なスケールイン/アウトを実現するため、1つポッドを動かし続けて、その中で 特定の時刻が来たらスケールイン/アウトを実行させていた。 しかし、これは障害耐性のことを考えるとあまり賢い方法ではなかった。スケールイン/アウトを実施するポッド が1つなので、該当の時間にポッドが落ちていた場合には何も動かない。だからと言ってポッドの数を増やして冗長化すると どのポッドがスケールを決めるのかという問題が発生する。(Leader Electionが必要になる。)
kubernetes上のAT
というわけで、atコマンドをkubernetesで作ってみることにした。
atコマンドとは以下のように特定の時間に1回だけ特定のコマンドを実行してくれるコマンドだ。
echo hello > /tmp/sample | at 11:50
同じように特定の時間に1回だけ、Jobを作成するATというオブジェクトをkubernetes上に作れば、不定期なスケールイン/アウトに対応することができる。 kubernetesでは、独自にCustom Resource Definition(CRD) とそれの制御をおこなうPod(コントローラー)を作ることで独自のリソースをクラスタ上に定義できてしまう(すごい)。 フレームワーク(kubebuilder)を使って作ればLeader Electionもばっちりやってくれる。また、CRDにはほかにも以下のような利点がある。
- kubectlからオブジェクトが見えるのでチーム内の誰でもスケジュールをメンテナンスできる。
- 入力値のバリデーションを行える。
- ほかのリソースと同様に定義をyamlにできるので、Infrastructure as a code的にGood
kubebuilderを使うためには覚えることが結構あるが、 ATを作るうえで考えるべきことは以下の2つに集約されると思う。
- ATオブジェクトはどのようなデータ構造を持つか?
- ATオブジェクトのControl Loopはどのようなものになるか?
ATのデータ構造
トップレベルのATのデータ構造は以下のようになっている。
type AT struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ATSpec `json:"spec,omitempty"`
Status ATStatus `json:"status,omitempty"`
}
実はここまではテンプレで、ATは各種メタデータとSpec
(ATを定義するための情報)とStatus
(Control Loopやユーザーのための状態)を持っている。
(別にATに限らずここまではほとんどのケースで同じになるはず)
type ATSpec struct {
// JobTemplate that is run by AT
JobTemplate batchv1beta1.JobTemplateSpec `json:"jobTemplate"`
// Time which we run this job
Schedule *metav1.Time `json:"schedule"`
}
Spec
に関しては上のようにしてみた。
JobTemplate
は通常のkubernetesのJobを作る際のものと同じで通常Jobを作るときには以下のように指定しているものになる。
apiVersion: batch/v1
kind: Job
metadata:
name: hello
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- echo "hello world"
restartPolicy: Never
Schedule
に関しては、cronだと5 4 * * *
のような形式で指定するところだが、特定の時刻に1回だけ実行
するものなので*metav1.Time
で時刻を指定してもらうようにした。(yamlからATを作る際には、RFC3339の形式で指定できるようになる。)
type ATStatus struct {
// Status of AT
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
// AT Status
// +optional
ATScheduleStatus ATScheduleStatus `json:"status,omitempty"`
}
Status
に関しては上のようにした。LastScheduleTime
にはCronJobのLastScheduleTime
にならって、Jobを作成した時間をControl Loopで入れることにする。
ATScheduleStatus
にはPending
(まだJobを実行する時間じゃない),Succeeded
(Job成功)、Stale
(もうJobを実行させていい時間を過ぎてしまった)
のような状態を持たせる。(実はStale
が未実装)
ATのControl Loop
Control Loopを実装するには
Reconcilerインターフェイス
のReconcile
メソッドを実装することになる。
Reconcile
の定義は以下のような感じで、引数がRequest
で戻り値がResult
とerror
だ。
type Reconciler interface {
// Reconciler performs a full reconciliation for the object referred to by the Request.
// The Controller will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
Reconcile(Request) (Result, error)
}
Request
は特定のオブジェクトとネームスペースの組み合わせだ。
例えば、defalutネームスペースにsampleというATを作った場合には、defalutとsampleの組み合わせをもらえる。
Result
は次にReconcileが呼び出されるまでの時間を指定するものになっている。
まとめると、Reconcile
では、Control Loopの対象となるオブジェクトの名前を受け取るので、それに対する制御を行い
次にReconcile
が必要になる時間を返却することになる。
ATのReconcileでは以下のようなことを行った。
- ATが子供のJobをもっておらず、まだ実行のタイミングでない場合、実行のタイミングで
Reconcile
がまた呼び出されるようにResult
を返却 - ATが子供のJobをもっておらず、実行のタイミングであった場合、子供のJobを作成。
- ATが子供のJobを持っていた場合、Jobの状態に従ってATの状態(
ATStatus
)を変更する。
実際のコードは以下。
https://github.com/sato-s/k8s-at/blob/master/controllers/at_controller.go#L47-L108
ATを作ってみる
以下のようにATの定義を用意して実際にATを作成してみた。(CRDとコントローラーのインストールはgithubを参照)
apiVersion: batch.my.domain/v1
kind: AT
metadata:
name: at-sample
spec:
schedule: "2020-03-21T13:53:55Z"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- sleep 30; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
上をkubectl create -f
で作ると以下のようにPending状態のATが作成される。
$ kubectl get at
NAME STATUS LASTSCHEDULETIME SCHDULE
at-sample Pending 2020-03-21T13:53:55Z
schedule
に指定した時刻になると、以下のようにJobTemplate
で指定されたJobを作成してくれる。
$ kubectl get job
NAME COMPLETIONS DURATION AGE
at-sample-1584798835 1/1 34s 13h
Jobが未完了の場合、以下のようにATのステータスはRunningになる。
$ kubectl get at
NAME STATUS LASTSCHEDULETIME SCHDULE
at-sample Running 2020-03-21T13:53:55Z 2020-03-21T13:53:55Z
Jobの完了後、ATのステータスもSucceededになった。
$ kubectl get at
NAME STATUS LASTSCHEDULETIME SCHDULE
at-sample Succeeded 2020-03-21T13:53:55Z 2020-03-21T13:53:55Z
感想
kubebuilderを使って、CRDとコントローラーを作るのは正直結構苦労したが、それでもここまで簡単に機能を拡張できるのは さすがkubernetesだと思う。CRDはAWS S3のような外部のオブジェクトをkubernetes上のオブジェクトに マッピングしたりなど応用範囲が広いので覚えておいて損がなさそう。 S3も今回のATもそうだが、kubernetes上に存在しないオブジェクトをCRDで作るようにしておくと、関係する資産をすべてkubernetes用のyaml の中に記載できるので、Infrastructure as a Codeのためにもよさそう。