EKS FargateでPodのAZ分散

## 背景

EKS FargateはEKSクラスタにおいて、NodeをEC2インスタンスとして用意することなく、起動されたPodにFargateインスタンスを割り当てて起動するものです。

このような事情から、Fargateで起動されるように設定されたPodは、ほかのPodと異なり、fargate-schedulerと呼ばれるschedulerに任されます。結果として、PodがどのようなNodeに割り当てられるかはfargate-schedulerと、背後で連携しているであろうFargate自体のコントロールプレーン(AWSサービスそのもの)に依存します。この時、fargate-schedulerは割り当てられたNodeがPodのaffinityやtopology spread constraintを満たすかどうか確認しません。結果として、Podのこれらの設定値は無視されます。

ところで、EKS上で稼働するサービスの信頼性を向上するために、サービスを複数のAvailabilityZone(以下、AZ)に分散すること、AZ分散は欠かせません。「AZ障害に対してEKSであれば障害になったAZで稼働しているPodは正常なAZに再作成される(フェールオーバーする)」などというナイーブな想像が働きますが、現実はそう簡単ではありません。そもそも「AZの障害」といっても多岐にわたり、場合によってはAZに障害があり、特定AZのPodにユーザーがアクセスできない場合でも、それをEKSコントロールプレーンが正しく検知できるとは限りません[^1]。その場合、当然PodはほかのAZに再作成されることはありません。また、仮にPodがほかのAZに再作成されるとしても、障害になったAZにすべてのPodが存在すれば、障害の検知からPod再作成までの時間をゼロにすることはできません[^2]。したがって、あらかじめ複数のAZでPodを起動しておき、障害時の影響を最小限に抑えるAZ分散は必須といえるでしょう。

[^1]: 本題ではないので深入りしませんが、パブリックサブネットに存在しているLBからはアクセスできない(ユーザーはPodにアクセスできない)ものの、同じAZにあるEKSコントロールプレーンのENIとPodは疎通できる場合などが考えられます。
[^2]: 利用されている方には既知だと思いますが、Fargateで稼働するよう設定されたPodを作成してから、実際にFargateによるNodeが作成されてPodがスケジュールされるまでに40～50秒かかります。

Podの稼働場所を複数のAZに分散させるには、Podの稼働状況などによっていくつか方法がありますが、Podの数が少ない場合[^3]はpod anti affinityを利用する方法も一般的でしょう。しかし、そのような場合に、Fargateを利用していると、affinityが無視されてしまうのは困りものです。どのくらい影響があるのかというと、(東京リージョンのAZの数[^4]である)3つPodを起動して3~5回ほどrolling updateすると1回は複数のPodが1つのAZに起動されるはずです。[^5]

[^3]: Podの数が十分に多い場合、fargate-schedulerは`topologySpreadConstraints`を無視しますが、Nodeはある程度複数のAZに分散されるので、それほど今回の問題の影響を受けません。
[^4]: 4AZある場合があるのは有名ですが、最近のたいていのアカウントでは3AZだと思われます。
[^5]: この割合を低いとみるか高いとみるかは運用によると思いますが、仮に一度AZが偏った状態になると、なんらかの変更をしない限り、AZが偏っている状況が続くことも勘案して評価するべきと考えます。

この動作仕様はEKS Fargateユーザーの間では知られており、aws/containers-roadmapにもいくつか関連するissueが上がっています。

- [[EKS/Fargate] [request]: Support for pod anti-affinity on fargate #1324](https://github.com/aws/containers-roadmap/issues/1324)
- [[EKS/Fargate] [request]: Schedule evenly pod replicas across AZs #824](https://github.com/aws/containers-roadmap/issues/824)

Fargateを特定のAZに配置するには、PodをどのようなFargateを割り当てるかを決める、FargateProfileというリソースがEKSにあるので、それを利用します。FargatePrfileではFargateを起動するVPC Subnetと、どのPodがそのFargateProfileにマッチするnamespaceやラベルセレクターが指定できます。それぞれのAZのためのFargateProfileを作成して、AZの分だけDeploymentリソースを作成してその管理下のPodがそれぞれのFargateProfileにマッチするようにラベルセレクターを設定すれば複数のAZにPodを分散起動する状態は確実にできます。

この方法だとAZの数だけDeploymentを作成します。それに伴って、1アプリケーション1 Deploymentを前提とする仕組みと併用できなくなります。具体的にいうと、私の場合はargo-rolloutsと組み合わせることができず、困りました。

## deschedulerの利用

fargate-schedulerは別にわざと1つのAZに偏らせようとしているわけではありません。筆者による観察では、単純に毎回1/3の確率でランダムにAZを割り当てているようです。[^6]

[^6]: ごく短い時間においては、偏りがあるように感じられます。おそらく、FargateインスタンスのAZごとの使用量に偏りがあるとそれを解消するような制御が入っているのではないでしょうか。ただ、再起動を繰り返して全体として平均するとおおよそ1/3で均等に割り当てられるようです。

従って、DeploymentがPodをAZの数だけ[^7]Podを起動するなら、同じAZで起動してしまったPodのうち1つを残して残りをevictすることでいつかはAZ分散した状態になります。

[^7]: AZの数以下であれば同じですが、ここでは話を簡単にするためAZの数とちょうど同じとしておきます。

その作業を自動化するためにdeschedulerを利用します。deschedulerはkubernetesコミュニティのsig-schedulingで開発されているカスタムコントローラーの一種で、特定の条件のPodをevictします。この機能として、pod anti-affinityに違反している状態のPodをevictする設定もあります。[^8]

[^8]: 以前のバージョンでは「podAntiAffinityをおなじNodeのPod間でしかチェックしない」という振る舞い(バグ？)でしたが、v0.28.0で筆者のpull requestがマージされて修正されました。

利用する際の制限として、いわゆる「ソフトなaffinity」は利用できなくなります。その性質上、ソフトな制約は無視させるか、強制させるかしか選べません。

## まとめ

EKS FargateでのPodのAZ分散の必要性と、そのためのワークアラウンドとしてのdeschedulerを利用する方法について書きました。

実際に動作を確認するための設定などを今後加筆したいと思っています。