ClusterAutoscalerを使う場合EKSのNodeGroupの分け方に気をつける

ClusterAutoscaler（以下CA）を採用した際にEKSのNodeGroupの分け方について気をつけないといけないことがあるので記事にします。

それは **NodeGroupはZoneごとに分けたほうがいい** ということです。

## Zoneごとに分けたほうがいい理由
### 公式が推奨している
まずAWS公式でそのような記載があります。

>複数のノードグループを構成し、各グループで 1 つのアベイラビリティーゾーンにスコープし、--balance-similar-node-groups 機能を有効化することをお勧めします。ノードグループを 1 つだけ作成する場合には、そのノードグループを複数のアベイラビリティーゾーンにまたがるようにスコープします。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/autoscaling.html#:~:text=%E8%A4%87%E6%95%B0%E3%81%AE%E3%82%A2%E3%83%99%E3%82%A4%E3%83%A9%E3%83%93%E3%83%AA%E3%83%86%E3%82%A3%E3%83%BC%E3%82%BE%E3%83%BC%E3%83%B3%E3%81%AB%E3%81%BE%E3%81%9F%E3%81%8C%E3%82%8B%E3%83%8E%E3%83%BC%E3%83%89%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

スポットインスタンスを使わないかつ小規模な環境であればNodeGroupは1つでいいと思うので、Zoneごとに分けなくてもよさそうです。
スポットインスタンスを使う場合は停止リスクを考えインスタンスタイプの選択肢を増やさないといけないため、複数のNodeGroupが必要になってきます。

### うまくPodのスケジューリングができない場合がある
Podの[Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)を使って、ZoneごとにPodを分散配置したい場合に問題が起こることがあります。
それが`Pod.spec.topologySpreadConstraints.whenUnsatisfiable` が `DoNotSchedule` の場合です。

CAはPodのスケジューリングが可能なNodeを探索するとき
NodeGroupから[テンプレートノード](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.25.0/cluster-autoscaler/FAQ.md#:~:text=Cluster%20Autoscaler%20creates%20template%20nodes%20for%20each%20of%20the%20node%20groups%20and%20checks%20if%20any%20of%20the%20unschedulable%20pods%20would%20fit%20on%20a%20new%20node.)というものを選択します。ここで得られたノードの情報がそのNodeGroupのサーバーのスペックと見なされます。

このノードの情報の中にZoneも含まれます。

たとえばNodeGroupが1つあったとします。このNodeGroupはa/c/d3つのZoneにまたがっているとします。
NodeGroupに2つのNodeがいたとして、それぞれaとc zoneに配置されているとします。
このテンプレートノードを取得する際にc zoneのノードが選択された場合、そのNodeGroupはc zoneのインスタンスが配置されていると判定されます。

ここでPodの数が1つ増え、d zoneのNodeにPodをスケジューリングしないといけなくなったときに
CAにはd zoneのNodeを生成してほしいわけですが、d zoneに属するNodeGroupは（CAの中では）存在しないので
スケールアウトされず、Podもスケジューリングされない状況となります。

## ZoneごとにNodeGroupを分けるときの注意点
2つ注意点があります。
* NodeGroupが多くなるとスケールアウトに時間がかかる場合がある
* 単純にZoneごとにNodeGroupを分けてもうまくバランシングしてくれない

### NodeGroupが多くなるとスケールアウトに時間がかかる場合がある
AWS公式が言及しています。
>ノードグループの数を減らして、大規模なクラスタでの Cluster Autoscaler のパフォーマンスを向上させることができます。個々のチームまたはアプリケーションベースでノードグループを構造化する場合には、この作業は難しいかもしれません。これは Kubernetes API で完全にサポートされていますが、スケーラビリティに影響を与え得る、Cluster Autoscaler のアンチパターンとして見なされます。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/autoscaling.html#:~:text=%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%97%E3%81%BE%E3%81%99%E3%80%82-,%E3%83%8E%E3%83%BC%E3%83%89%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E6%95%B0%E3%82%92%E6%B8%9B%E3%82%89%E3%81%99,-%E3%83%8E%E3%83%BC%E3%83%89%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE

これはNodeGroupの数が増えることによって、スケールアウトする際にどのNodeGroupを選択するかという計算が増えることによるCAのパフォーマンス劣化によるものだと考えられます。
とはいえ極端に多くなければそこまで影響は出ないと思いますが、いずれにせよスケールアウトの時間の検証が必要です。

### 単純にZoneごとにNodeGroupを分けてもうまくバランシングしてくれない
`--balance-similar-node-groups` というオプションをCAに付与すると、スケールアウトする際に選択されたNodeGroupと似ているNodeGroupを探し、それらのNodeGroupで起動するノードをバランシングしてくれます。

この **似ているNodeGroup** というのがクセモノで、デフォルトだと以下のような定義をされています。

* 各リソース（CPU・Memoryなど）のCapacityが一緒
* 割当可能(Allocatable)なリソースの差が5％以内
* 空きリソース（AllocatableからDaemonSetやkube-proxyが使用するリソースを引いたもの）が5％以内
* 同じLabelを持っている（zone/hostname除く)

https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/balance_similar.md#similar-node-groups

NodeGroupでノードを起動すると様々なLabelが付与されています。
Labelには `instanceType` の情報が含まれるため、似ているNodeGroupと判定されないことがあります。

たとえば以下のようなインスタンスタイプで起動するNodeGroupがZoneごとにあるとします。
これらのインスタンスタイプはCPU/Memoryは同じです。

* c5.large
* c6i.large
* c6a.large

3つのNodeGroupはそれぞれ1つずつノードが起動されており以下のような状況になっているとします。

zone | 起動しているノードのinstanceType
-- | --
a | c5.large
c | c6i.large
d | c5.large

CAは各NodeGroupでテンプレートノードを取得するという話をしました。
このテンプレートノードの情報を元にすると、**CAはa/d zoneのNodeGroupが似ているNodeGroup**と判定します。
この状態で3台追加でスケールアウトしないといけなったときに、CAは以下のようにスケールアウトするNodeGroupを選択します。

* どのNodeGroupをスケールアウトさせるかを計算し、a zoneのNodeGroupを選択する。
* a zoneのNodeGroupと似ているNodeGroupを探す。d zoneのNodeGroupが対象になる。
* a/d zoneのNodeGroupをバランスよくスケールアウトさせる

ということで、c zoneのNodeGroupはスケールアウトされず、アンバランスなノードの配置になってしまいます。

---

このようなことを回避するために CAの1.25から追加された `--balancing-label` が使えます。

https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.25.0/cluster-autoscaler/FAQ.md#:~:text=%22%22-,balancing%2Dlabel,-Define%20a%20node

これは似ているNodeGroupを判定するためのラベルを任意で指定できます。
この設定がある場合、上記で記載した似ているNodeGroupを探すためのデフォルトの動作はしなくなります。

各NodeGroupに以下のようなラベルを持たせ、`--balancing-label` で指定すれば各Zoneバランス良くNodeが配置されます。

* example.com/cpu: 2
* example.com/memory: 4