自分のためだけにcustom controllerを書く

ふだんkopsで自分用のKubernetesクラスタを立ち上げているんだけど,個人用途なのでAutoScaleはしてほしくないのでClusterAutoscalerは入れていない.だけど,その状態でも可用性はできる限り下げたくない. というわけで,ちょっと自分でノードを管理するカスタムコントローラを書いた.

こういう,あんまり需要がないけど自分のところだけでいろいろカスタマイズしたいときに,適当なコントローラを自作すると手動作業が減らせるので大変良い.

個人用クラスタについて

kopsを使ってAWS上に構築している.複数台構成で

  • control-plane * 3
  • node * 3

くらいで運用している.nodeの数は乗せるものによって増やしたり減らしたり.

ちなみに全部SpotInstanceで運用している. Spotだと不意にTerminateされることが多いんだけど,大抵の場合はすぐに次のインスタンスが上がってきて,事なきを得る. たまに特定のFlavorの値段が高騰したりすると,新しいインスタンスをe立ち上げられなくなるんだけど,これに関してはAutoScalingGroupのLaunchTemplateとMixedInstancePolicyの組み合わせを使うことで,複数のFlavorを利用するとこができる. 例えば,t3.largeの値段が高騰したとしても,MixedInstancePolicyで

  • t3.large
  • c5.large
  • m5.large

とか入れていれば,この3つの中で安いSpotInstanceを探してきて立ち上げてくれる. たしかにSpotPriceはたまに高騰するんだけど,複数Flavorを用意しておけばそれらがすべて高騰することはあまりない.

実現したいこと

ノードの可用性を下げたくないが,不要なインスタンスは消したい

kopsで作成している時点で,基本的にAutoScalingGroupでインスタンスを立ち上げている.そのため特定のnodeが突然ししたとしても,新しいnodeは自動的に補充される.

ただし,特定のAvailabilityZoneのSpotが足らなくなったり,価格が高騰したりした場合,そのAZのインスタンスは起動できなくなる. 特に最近のkopsはAZごとにAutoScalingGroupを作成している.

  • masters.ap-northeast-1a.hogehoge
  • masters.ap-northeast-1c.hogehoge
  • masters.ap-northeast-1d.hogehoge
  • nodes.ap-northeast-1a.hogehoge
  • nodes.ap-northeast-1c.hogehoge
  • nodes.ap-northeast-1d.hogehoge

こうなっていると,特定のASGだけひたすらインスタンスが起動できないような状態に陥ることがある.

このときに,可用性を下げたくないので,生きているASGで代替のインスタンスを一時的に起動しておいてほしい.

という要望はありつつ,不要なインスタンスまで起動していてほしくない.なので,例えば上記のトラブルが解消されたら元のインスンタンスに戻してほしいのである.

一定期間でノードをリフレッシュしたい

かつて,kopsのAutoScalingGroupでSpotInstanceを使うときに,LaunchTemplateではなくLaunchConfigurationしか使えない時代があった.このときにSpotInstanceを立ち上げると,Spotの期限がデフォルトの7dayになってしまっていた. これは編集不可能で,仕方なく7日おきにインスタンスを殺すようなLambdaを書いていた.

ただ,このタイミングでどうしてもインスタンスが死ぬと障害になってしまうので,Lambdaではなく内部のコントローラでこれを行いたかった.

最近のkopsはLaunchTemplateが使えて,Spotの期限も無期限にできるので,特にこのような問題はない. ただ,上記のようなことをやっていたため,どうしてもあまり長くインスタンスを生かしておきたくなかった.というわけで,週に一度くらいはnodeのローリングアップデートをしたかった.

これらを一つのcontroller-managerで実現したい

少し考えてみて,これらを一つのcontroller-managerで実現したほうがよいと思った.というのも,不要なインスタンスを消す機能と,ローリングアップデートを同時に動かすのは結構難しい.ローリングアップデート時にインスタンスを突然殺す分には特に問題ない.ただ,それをやるとnode数がたらなくなり,配置できないPodが生じる可能性はある.そのため,予備のnodeを予め起動しておいてから,古いnodeのローリングアップデートをしたい.それをやろうと思うと,予備のnodeが不要なnodeと判定されないためにも,これらの動作を排他的に制御できるのが望ましく,そのために同じcontroller-managerで実現するほうが楽だと判断した.

やることが複数なので,複数CRDで複数controllerという構成になるものを,controller-managerで束ねるという形にしたいと思う.

できたもの

github.com

まだだいぶ荒削りだがとりあえず動くものはできた.

今回はcontroller-runtimeを使ってしまっている.controllerとしてそこまで複雑なことをするわけではないので.

全体的な構成

node-manager
├── aws-node-manager-master
│   ├── replenisher-master
│   └── refresher-master
└── aws-node-manager-worker
    ├── replenisher-worker
    └── refresher-worker

aws-node-managerがやること

現在のKubernetesのnodeを取得してきて,これがAWS上のどのAutoScalingGroupに属しているのか,instanceIDが何なのか,ということを埋めている.これをCRDのStatusに埋めることで,下位のcontrollerがこれらの情報を利用する.

また,CustomResourceの定義に応じてmaster, nodeのrefresherとreplenisherのCRを作成する.

refresherがやること

CRに指定されたスケジュールに応じて以下のようなステップでローリングアップデートを行う.

  1. 次のスケジュール日程を決定する
  2. スケジュールが来るまで待つ
  3. スケジュールが来たら,surplusNodes分だけ予備のnodeを追加する
  4. nodeがreadyになるのを待つ
  5. 全nodeがreadyになったら一番古いnodeを1台消す
  6. ASGによりinstanceが自動的に補充されnodeとしてクラスタに参加するのを待つ
  7. 全nodeがrefresh開始時より新しくなるまで5~の流れを繰り返す
  8. 全nodeが新しくなったら補充したsurplusNodes分のnodeを削除する
  9. 次のスケジュール日程を決定する

replenisherがやること

  1. refresh中の場合は処理をすべてスキップする
  2. desiredで指定されたnode数とreadyなnode数を比較する
  3. nodeが不足している場合はASGのdesiredを上げてinstanceを追加で起動させる
  4. このとき,desiredと実際に起動されてるinstance数が異なるASGは,spotの都合でinstanceが起動できていない可能性がある.そのため,instanceの追加対象ASGから除外して考える.あくまで正常にinstanceを起動できているASGだけを対象にdesiredを上げる
  5. nodeが過剰にある場合は,適当なASGのdesiredを下げる
  6. また,起動後一定時間経ってもクラスタに参加できていないinstanceがある場合は,ASGからdetach & terminateする

kopsでは,cluster specにてetcdの数と,etcdを起動するinstance groupを指定している.これの定義以上のインスタンスを起動しても,masterとしてはクラスタに参加することができない(etcdの定義が被ってしまう)ため,そういったインスタンスはいつまで待っていても無駄である.そのため,これも削除するようにしている.

まとめ

実際,LaunchTemplateとMixedInstancePolicyが使えるようになってから,replenisherの仕事はほぼなくなった.複数のflavorを指定しているので,特定のASGでインスタンスが起動できなくなるという事態がそもそも発生しにくくなっている.そのため,普段はほとんどrefresherの仕事を眺めるだけになっている.

Spotは中断の2分前に通知を出すことができる.

docs.aws.amazon.com

本当はこれを拾って,対象nodeのdrainができると良いのだけど,まだそこまで実装できていない. また,refreshにおいても,インスタンスを殺す前にdrainができると最高なのだが,まだ実装できていない.