GitHub ActionsでElectronアプリのリリースを自動化する

やろうやろうと思っていたけど,できていなかったので,いよいよ自動化した. ここにおけるポイントは,ほぼMacOSをどうするかという話に集約すると思うんだけど,一応Windows/Linuxでもちょっと変更しなきゃいけないことはあったので,書いておく.

続きを読む

自分のためだけに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ができると最高なのだが,まだ実装できていない.

Kubernetes 1.20ではownerReferenceのnamespaceに気を使え

qiita.com

ここの話です.

github.com

Issueの内容を熟知している方には余計なお世話です.

いやーこんなの自分には関係ないだろうと思ってたら見事に当たりました.

問題になるケース

問題になるケースはおそらく以下の2パターン.

  1. OwnerとChildが別のnamespaceに所属するパターン
  2. namespaceに所属するOwnerがcluster scopeなChildからOwnerReferenceを張られているパターン

ちなみにこれは作成時にエラーになるわけではない.あくまでGCコントローラの挙動の変更なので,リソースが削除される挙動が変更になっている.

私の場合2に該当していた.

CRDを定義して,カスタムリソースをapplyするとAdmissionWebhookのConfigurationと,それに合わせたWebhookのPodとServiceを作るようなコントローラを作っていた. このとき,CRDのscopeをNamespacedにしていたんだけど,NamespacedなカスタムリソースがCluster scopeのValidating/MutatingWebhookConfigurationをChildに持つ場合,がっつり上記に該当してGCされなくなる. 結果として,カスタムリソースを削除してもWebhookConfigurationが削除されないという状態になった.

対応

私の場合の話なので,2に該当する場合なのだけれど,OwnerがNamespacedである必要がないのであれば,OwnerをCluster scopeにしてしまえば良い.Cluster scopeなOwnerがCluster scopeなChildを持つことは特に問題ない.また,Cluster scopeなOwnerがNamespace scopeのChildを持つことも,特に問題はない.GCコントローラはちゃんとOwnerが消えたときにChildも消してくれる.

今回で言うなら,Validating/MutatingWebhookConfigurationのOwnerなので,別にCluster scopeで良くて,Cluster scopeに変更することで対応できた.合わせて作るPodとServiceのnamespaceをどこにするかは悩みどころではあったが.Cluster scopeなカスタムリソースもそれらのPodやServiceのOwnerになれるので,特に問題はなかった.

1についても同様なのだけれど,OwnerがCluster scopeで良いのであれば,Cluster scopeにすることで正しいOwnerReferenceになる.

問題はOwnerがNamespace scopeである必要がある場合なのだが,それってどんなケースなのか想像できないのでちょっと対応策も思いつかない…….

気づいた経緯

そもそもこの話は知っていたんだけど,完全に無関係だろうと思っていた.ので完全に油断していたんだけど,先日書いたkind local registryによりE2Eがそれなりの頻度でCIで走るようになったおかげで気づいた.

h3poteto.hatenablog.com

Kubernetes 1.20に上げたところでテストが落ちて,AdmissionWebhookが消えなくなっていたので,そこで問題が発覚した.

E2Eテスト便利や.

ちなみに記事でも紹介されてたけど kubectl-check-ownerrefefrences を使えば一発でわかるんで,これも便利だったよ.

github.com