Pod をAZ（アベイラビリティーゾーン）ごとに分散して配置させたい

AWS は堅牢であるものの、AZレベルの障害が発生しているため、複数のAZに分散して可用性を高めておきたいものです。
下記の図のように、各ゾーンごとにノードが配置されている場合、Podについてもそれぞれのゾーン毎に配置したいのですが、単純に `replicas` の数を増やしても、各ノードのリソースの空き状況に応じて配置されるだけなので、思ったように分散されないこともしばしばです。

![fig-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/bc4125f0-ee64-a0fb-f110-cb08f2b83007.png)

## PodAffinity / PodAntiAffinity について

Pod をどのノードに配置するかを指定する方法として、 ノードに付与されたラベルから配置先を指定する `nodeSelector` と、より柔軟な指定ができる `nodeAffinity / nodeAntiAffinity` があります。
これらはあくまでもノードに付与されたラベルから配置先を指定しますが、`PodAffinity / PodAntiAffinity` はノードのラベルではなく、すでに配置されたPodのラベルから配置先を指定することができます。

下記の図のように既に実行中のPodを避けて配置先を指定したい場合は `PodAntiAffinity` を指定します。

![fig-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/615436c2-932b-1293-5ebe-6c89ef319dd4.png)

また、指定できる条件として、必須条件の `requiredDuringSchedulingIgnoredDuringExecution` と、条件に満たなかった場合でも配置を試みる `preferredDuringSchedulingIgnoredDuringExecution` があります。

1AZで複数のノードを使用していたり、リソースの使用状況に応じてノード数やPod数を増減させるような、cluster autoscaler や Horizontal Pod Autoscaler (HPA) を使っている場合、必須条件である　`requiredDuringSchedulingIgnoredDuringExecution` で指定していると本来必要なPodが実行できないことになるため、`preferredDuringSchedulingIgnoredDuringExecution` を指定することになると思います。

## 動作確認

検証環境として以下のような2AZに1ノードずつ配置したEKSを構築しました。

![fig-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/066b059e-1a21-efd7-3802-200a5c96c0e8.png)

以下のようなテスト用 Deployment を `replicas: 1` で実行します。

```yaml:test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
spec:
  selector:
    matchLabels:
      app: podinfo
  replicas: 1
  template:
    metadata:
      labels:
        app: podinfo
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - podinfo
              topologyKey: "failure-domain.beta.kubernetes.io/zone"
      containers:
      - name: podinfo
        image: stefanprodan/podinfo		
```

```shell-session
$ kubectl apply -f test.yaml
```

下記のように `ap-northeast-1` にある `nod-1` に配置されました。

![fig-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/c488128c-e122-d8b0-98b7-d81bd78c69a8.png)

次にレプリカ数を `2` に増やしてみます。

```shell-session
$ kubectl scale deployment.v1.apps/podinfo --replicas=2
```

想定通り、既に配置している`ap-northeast-1a`を避けて`ap-northeast-1c` にある `node-2` に配置されました。

![fig-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/ccd9e977-2aef-80ad-a14d-1af1d1b6e0b3.png)


さらにレプリカ数を増やしてみます。

```shell-session
$ kubectl scale deployment.v1.app/podinfo --replicas=3
```

`preferredDuringSchedulingIgnoredDuringExecution` で設定しているので、既に同じAZで実行中のPodがあるものの、新しくPodが `node-2` で実行されました。

![fig-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/3bc12f2d-e89a-d856-22b9-f43b0d61a4da.png)

この状態で `ap-northeast-1d` に `node-3`という新しいノードを追加してみます。

![fig-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/1c04f9aa-4fe3-d0eb-3ef4-f6a48ef9347c.png)

新しいノードが追加されても、kubernetesは自動でPodのリバランスは行われないため、`node-2` で Podが2つ動いている状態は解消されません。

## Descheduler for Kubernetes でリバランスを行う

先記のように、PodAntiAffinity を使って Pod の配置先を指定したとしても、条件によって偏って配置されてしまったPodはリバランスされないため、手動でPodを停止させるなどしないと解消されません。

[descheduler](https://github.com/kubernetes-sigs/descheduler) を使うと CronJob として実行し、偏って配置されたPodを停止して正しいノード先に再配置されるようになります。

descheduler では PodAntiAffinity に違反したPodを停止する以外にも以下のようなポリシーを設定することもできます。

* 同じノードに重複してPodが配置されている場合に重複したPodを停止させて別ノードに配置されるようにする。
* 使用率が低いノードがある場合、他のノードのPodを停止して、このノードに配置されるようにする。

それ以外は [Policy and Strategies](https://github.com/kubernetes-sigs/descheduler#policy-and-strategies) を参照してください。
ただし、 descheduler は、あくまでもPodを停止する事しかしないため、Podの再配置については Kuberenetes が決定するため、思った通りの挙動にならないこともあります。

では、Helm でインストールできるので検証用EKSにインストールしてみます。

```shell-session
$ helm upgrade --install --namespace kube-system descheduler descheduler/descheduler
```

デフォルトでは `*/2 * * * *` なので、毎2分ごとに実行されます。
しばらく待つと descheduler が実行されて下記のようにPodが正しく再配置されました。

![fig-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/6037601f-3002-6a68-4b4a-9318d07939bc.png)

## まとめ

PodAntiAffinity と descheduler を使うことで、AZ（アベイラビリティーゾーン）ごとにPodを分散して配置することができるようになりました。
descheduler を使うと、意図しないタイミングでPodが停止することになるので、Pod disruption budgets や コンテナーの Lifecycle Hook の PreStop の設定を行なっておく必要もあるかと思います。