k8s での Pod のスケジューリングについて確認した時のメモ(NodeAffinity)

メモ。
以下の続きです。

[k8s での Pod のスケジューリングについて確認した時のメモ(nodeSelector まで)](https://qiita.com/toshihirock/items/0f38aea8d069d434d81d)

# まとめ

+ nodeSelector と比較し、Node Affinity/Node Anti-Affinity によってより柔軟な設定ができる
+ 必須条件と優先条件を指定出来る
+ 必須条件を満たさない場合、Pod は Pending となり、スケジューリングされず起動しない
+ 優先条件は満たさない場合でも他の Node のリソースなどがあればスケジューリングされ、起動しようとする
+ オペレーターの種類は複数あり、これによって柔軟な設定ができる
+ 必須条件と優先条件は併記する事が出来る

# 環境

以下の記事と同じとなります。

[k8s での Pod のスケジューリングについて確認した時のメモ(nodeSelector まで)](https://qiita.com/toshihirock/items/0f38aea8d069d434d81d)

# Affinity と Anti-Affinity 

[Affinity と Anti-Affinity](https://kubernetes.io/ja/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity)

nodeSelector はとてもシンプルなスケジューリング方法だが、Affinity/Anti-Affinity を使うことでより柔軟な設定が可能

1. 様々な指定方法がある (“AND条件”に限らない)
1. 必須条件ではなく優先条件を指定でき、条件を満たさない場合でもPodをスケジュールさせることができる
1. Node自体のラベルではなく、Node(または他のトポロジカルドメイン)上で稼働している他のPodのラベルに対して条件を指定することができ、そのPodと同じ、または異なるドメインで稼働させることができる

3点目については Inter-Pod Affinity/Anti-Affinity に対するメリットであり、この記事では記載しない

# Node Affinity

[Node Affinity](https://kubernetes.io/ja/docs/concepts/configuration/assign-pod-node/#node-affinity)

機能としては Node のラベルによって Pod がどの Node にスケジュールされるかを制限する機能であり、nodeSelector と同じ。
ただし、より柔軟な設定が出来る

以下の2種類の指定がある。

+ requiredDuringSchedulingIgnoredDuringExecution:「requiredDuringScheduling」とある通り、スケジューリングの際の **必須** 条件
+ preferredDuringSchedulingIgnoredDuringExecution:「preferredDuringScheduling」とある通り、スケジューリングの際の **優先** 条件

なお、「IgnoredDuringExecution」とある通り、実行中については条件は無視される。これは nodeSelector の機能と同様であり、Node のラベルが変更され、Pod がその条件を満たさなくなった場合でも Pod はその Node で稼働し続けるということを意味する。

# 検証

## 検証に利用する Node

以下の通り2つの Node が配置された環境を利用する。
デフォルトで付与されてるラベルは以下の通り

```bash
$k get nodes -o json|jq ".items[]|.metadata.labels"
{
  "alpha.eksctl.io/cluster-name": "test-cluster",
  "alpha.eksctl.io/instance-id": "i-0b26392bc266a5999",
  "alpha.eksctl.io/nodegroup-name": "standard-workers",
  "beta.kubernetes.io/arch": "amd64",
  "beta.kubernetes.io/instance-type": "t3.medium",
  "beta.kubernetes.io/os": "linux",
  "disktype": "ssd",
  "failure-domain.beta.kubernetes.io/region": "ap-northeast-1",
  "failure-domain.beta.kubernetes.io/zone": "ap-northeast-1c",
  "kubernetes.io/arch": "amd64",
  "kubernetes.io/hostname": "ip-192-168-44-10.ap-northeast-1.compute.internal",
  "kubernetes.io/os": "linux"
}
{
  "alpha.eksctl.io/cluster-name": "test-cluster",
  "alpha.eksctl.io/instance-id": "i-02b72bf1fd5120455",
  "alpha.eksctl.io/nodegroup-name": "standard-workers",
  "beta.kubernetes.io/arch": "amd64",
  "beta.kubernetes.io/instance-type": "t3.medium",
  "beta.kubernetes.io/os": "linux",
  "failure-domain.beta.kubernetes.io/region": "ap-northeast-1",
  "failure-domain.beta.kubernetes.io/zone": "ap-northeast-1d",
  "kubernetes.io/arch": "amd64",
  "kubernetes.io/hostname": "ip-192-168-65-145.ap-northeast-1.compute.internal",
  "kubernetes.io/os": "linux"
}

```

上記の通り、Node は ap-northeast-1c 及び ap-northeast-1d に起動されている。

## requiredDuringSchedulingIgnoredDuringExecution 

### 基本的な使い方

以下のようなマニュフェストを書いて特定 AZ の Node への配置を必須とする。

```node-affinity.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: failure-domain.beta.kubernetes.io/zone
            operator: In
            values:
            - ap-northeast-1a
            - ap-northeast-1c
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0
```

```bash
$k apply -f node-affinity.yaml

# ap-northeast-1c の Node 上に起動
$k describe pods |grep Node
Node:         ip-192-168-44-10.ap-northeast-1.compute.internal/192.168.44.10
Node-Selectors:  <none>
```

無事想定通りの動きとなった。

### 条件を満たさない場合の動き

必須条件が満たされない時の動きも見る。
先程のマニュフェストを使いしつつ、事前に対象 Node は drain してスケジューリング出来ないようにしておく。

```bash
# Daemon set が動いているので --ignore-daemonsets を付与して drain
$k drain ip-192-168-44-10.ap-northeast-1.compute.internal --ignore-daemonsets
node/ip-192-168-44-10.ap-northeast-1.compute.internal already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/aws-node-4c74w, kube-system/kube-proxy-msjts
node/ip-192-168-44-10.ap-northeast-1.compute.internal drained

# 確認
$k get nodes
NAME                                                STATUS                     ROLES    AGE   VERSION
ip-192-168-44-10.ap-northeast-1.compute.internal    Ready,SchedulingDisabled   <none>   13d   v1.14.9-eks-1f0ca9
ip-192-168-65-145.ap-northeast-1.compute.internal   Ready                      <
none>   13d   v1.14.9-eks-1f0ca9

# Pod の起動
$k apply -f node-affinity.yaml
pod/with-node-affinity created

# Pending　状態になっている
$k get pods
NAME                 READY   STATUS    RESTARTS   AGE
with-node-affinity   0/1     Pending   0          43s

# 詳細を確認
$k describe pods with-node-affinity
・・・

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  54s   default-scheduler  0/2 nodes are available: 1
 node(s) didn't match node selector, 1 node(s) were unschedulable.
```

2つ Node があるが一方は node selector にマッチせず、もう一方は unschedulable なのでスケジューラーが配置できない状態であるというのが分かった。

つまり、必須要件の場合、名前の通り **必須** な条件なので Node がある場合でも要件に合致する Node がなければ配置しない。

先程 drain した Node は ready に戻しておく

```bash
$k uncordon ip-192-168-44-10.ap-northeast-1.compute.internal
node/ip-192-168-44-10.ap-northeast-1.compute.internal uncordoned

$k get nodes
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-44-10.ap-northeast-1.compute.internal    Ready    <none>   13d   v1.14.9-eks-1f0ca9
ip-192-168-65-145.ap-northeast-1.compute.internal   Ready    <none>   13d   v1.1
4.9-eks-1f0ca9
```

## preferredDuringSchedulingIgnoredDuringExecution 

### 基本的な使い方

先ほどのマニュフェストを少し変えて検証。
条件を **必須** から **優先** に変える

```node-affinity.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      -  weight: 1
         preference:
           matchExpressions:
             - key: failure-domain.beta.kubernetes.io/zone
               operator: In
               values:
                 - ap-northeast-1a
                 - ap-northeast-1c
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0
```

主な変更点は以下

+ 「requiredDuringSchedulingIgnoredDuringExecution」を「preferredDuringSchedulingIgnoredDuringExecution」に変更
+ preferredDuringSchedulingIgnoredDuringExecution には List を設定しつつ、preference を設定
+ weight は必須なようなので追加

これで検証

```bash
$k apply -f node-affinity.yaml
pod/with-node-affinity created

$k get pods
NAME                 READY   STATUS    RESTARTS   AGE
with-node-affinity   1/1     Running   0          2m58s

#　優先条件として指定された ap-northeast-1c  への起動を確認
$k describe pods |grep Node
Node:         ip-192-168-44-10.ap-northeast-1.compute.internal/192.168.44.10
Node-Selectors:  <none>
```

### 条件を満たさない場合の動き

同じように優先条件に指定された内容が満たされない場合の挙動を確認する。

```bash
# drain する
$k drain ip-192-168-44-10.ap-northeast-1.compute.internal --ignore-daemonsets
node/ip-192-168-44-10.ap-northeast-1.compute.internal already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/aws-node-4c74w, kube-system/kube-proxy-msjts
node/ip-192-168-44-10.ap-northeast-1.compute.internal drained

# SchedulingDisabled　になっていることを確認
$k get nodes
NAME                                                STATUS                     ROLES    AGE   VERSION
ip-192-168-44-10.ap-northeast-1.compute.internal    Ready,SchedulingDisabled   <none>   13d   v1.14.9-eks-1f0ca9
ip-192-168-65-145.ap-northeast-1.compute.internal   Ready                      <none>   13d   v1.14.9-eks-1f0ca9

# Pod 作成
$k apply -f node-affinity.yaml
pod/with-node-affinity created

# Pod は Running
$k get pods
NAME                 READY   STATUS    RESTARTS   AGE
with-node-affinity   1/1     Running   0          28s

# 詳細を確認
$k describe pods with-node-affinity
・・・
Events:
  Type    Reason     Age   From
       Message
  ----    ------     ----  ----
       -------
  Normal  Scheduled  64s   default-scheduler
       Successfully assigned default/with-node-affinity to ip-192-168-65-145.ap-
northeast-1.compute.internal
  Normal  Pulling    64s   kubelet, ip-192-168-65-145.ap-northeast-1.compute.internal  Pulling image "k8s.gcr.io/pause:2.0"
  Normal  Pulled     61s   kubelet, ip-192-168-65-145.ap-northeast-1.compute.internal  Successfully pulled image "k8s.gcr.io/pause:2.0"
  Normal  Created    61s   kubelet, ip-192-168-65-145.ap-northeast-1.compute.internal  Created container with-node-affinity
  Normal  Started    61s   kubelet, ip-192-168-65-145.ap-northeast-1.compute.internal  Started container with-node-affinity
```

特に詳細な Events の記載はないがスケジューラのアサインが出来たことは確認出来た。
また、こちらは **必須** ではなく、 **優先** なので条件が満たされない場合でも他の Node のリソースに余裕があればスケジューラーは Pod の起動を実行するということだと思われる。

# その他

## オペレーター(operator)

今回の検証では `In` のみ利用したが他にも以下がある

+ In
+ NotIn
+ Exists
+ DoesNotExist
+ Gt
+ Lt

`NotIn` や `DoesNotExist` などを利用することで Node-AntiAffinity を実現する。（xxx という条件ではない Node へ Pod の起動をするというような条件の場合）

## requiredDuringSchedulingIgnoredDuringExecution と requiredDuringSchedulingIgnoredDuringExecution の併用

必須条件「requiredDuringSchedulingIgnoredDuringExecution」 と 優先条件「
  requiredDuringSchedulingIgnoredDuringExecution」 の併用も可能。

その為、以下のような要件の作成も可能

+ OS が Linux であることを必須条件とする。(beta.kubernetes.io/os =　linux)。加えて可能な限り ap-northeast-1c に配置して欲しい(failure-domain.beta.kubernetes.io/zone = ap-northeast-1c)

nodeSelector と比較すると難しい条件の指定も出来る。

[k8s での Pod のスケジューリングについて確認した時のメモ(nodeSelector まで)](https://qiita.com/toshihirock/items/0f38aea8d069d434d81d)