[Kubernetes]Podデプロイ時のスケジューリング制御方法を確認する。

#はじめに
今回はPodデプロイ時のスケジューリングの動作について確認したいと思います。Podをどのノードにデプロイするかを制御するためには、以下の方法があります。それぞれの設定方法と動作について確認していきます。

* nodeSelector
* Node Affinity
* node Anti-Affinity
* Inter-Pod Affinity
* Inter-Pod Anti-Affinity

#nodeSelector
nodeSelectorでデプロイするノードを制御するには、ノードに付与されているラベルを利用します。


##ラベルの確認と設定
まずは各ノードにデフォルトで設定されているラベルを確認します。

```
$ kubectl get nodes --show-labels
NAME           STATUS   ROLES    AGE    VERSION   LABELS
k8s-master     Ready    master   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-worker01   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker01,kubernetes.io/os=linux
k8s-worker02   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker02,kubernetes.io/os=linux
```
上記コマンドの他に、kubectl describeコマンドなどでも確認できます。

各Nodeにラベルを設定します。ここでは、「disktype」ラベルを設定して、k8s-worker01は「ssd」、k8s-worker02は「hdd」とします。
実際はどちらも同じディスクですけどね。検証ですので。

```
$ kubectl label node k8s-worker01 disktype=ssd
node/k8s-worker01 labeled
$ kubectl label node k8s-worker02 disktype=hdd
node/k8s-worker02 labeled
```

設定したラベルを確認します。

```
$ kubectl get nodes --show-labels
NAME           STATUS   ROLES    AGE    VERSION   LABELS
k8s-master     Ready    master   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-worker01   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker01,kubernetes.io/os=linux
k8s-worker02   Ready    <none>   104d   v1.17.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=hdd,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-worker02,kubernetes.io/os=linux
```

これだとちょっと見づらいので、disktypeラベルのみ確認します。

```
$ kubectl get node -L disktype
NAME           STATUS   ROLES    AGE    VERSION   DISKTYPE
k8s-master     Ready    master   104d   v1.17.3
k8s-worker01   Ready    <none>   104d   v1.17.3   ssd
k8s-worker02   Ready    <none>   104d   v1.17.3   hdd
```

##nodeSelecterの動作確認

該当のPodのマニフェストに、nodeSelectorのセクションを追加し、「disktype: ssd」のNodeにデプロイするようにします。今回はDeploymentにしています。

```nginx-dep.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-dep
spec:
  replicas: 4
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
        - name: nginx
          image: nginx:latest
      nodeSelector:
        disktype: ssd
```

applyして、PodがデプロイされているNodeを確認します。

```
$ kubectl apply -f nginx-dep.yaml
deployment.apps/nginx-dep created
$ kubectl get pod -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx-dep-8f97c98cd-b4z88   1/1     Running   0          21s   192.168.79.118   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-bfg46   1/1     Running   0          21s   192.168.79.110   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-jm22p   1/1     Running   0          21s   192.168.79.126   k8s-worker01   <none>           <none>
nginx-dep-8f97c98cd-vk8nj   1/1     Running   0          21s   192.168.79.117   k8s-worker01   <none>           <none>
```
4つのPodがすべてssdのNode（＝k8s-worker01）にデプロイされていますね。


#Node Affinity
Node AffinityもラベルでデプロイするNodeを制御しますが、nodeSelectorよりも細かな設定ができます。

##設定
spec.affinityフィールドに設定を記載します。必須のスケジューリングポリシー（requiredDuringSchedulingIgnoredDuringExecution）と優先的に考慮されるスケジューリングポリシー（preferredDuringSchedulingIgnoredDuringExecution）があり、必須のポリシーを満たしたNodeのうち、優先的なポリシーに基づいてデプロイされるNodeが決定します。

```nodeAffinity.yaml
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution: #必須のスケジューリングポリシー
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype #ラベル名
            operator: In #In、NotIn、Exists、DoesNotExist、Gt、Ltを指定可能
            values: #複数設定可能
            - hdd
      preferredDuringSchedulingIgnoredDuringExecution: #優先的に考慮されるスケジューリングポリシー
      - weight: 1 #1~100で指定。条件を満たすノードのWeight値を合計し、一番スコアが高いノードが優先される
        preference:
          matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - k8s-worker01
  containers:
  - name: nginx-na
    image: nginx:latest
```
今回は以下の図のように必須と優先的なスケジューリングポリシーを別ノードにするようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492530/2ae25b2a-c8e4-eb3d-c55a-ff65b7100c70.png)



###operatorフィールドについて
以下の6つを指定できます。

|operator|概要
|:---|:---
|In|keyで指定したラベルが指定したvaluesのいづれか一つ以上に一致する。
|NotIn|keyで指定したラベルが指定したvaluesのいづれにも一致しない。
|Exists|keyで指定したラベルが存在する。valuesは指定しません。
|DoesNotExist|keyで指定したラベルが存在しない。valuesは指定しません。
|Gt|keyで指定したラベルの値（values）よりも大きい。valuesには整数値を指定します。
|Lt|keyで指定したラベルの値（values）よりも小さい。valuesには整数値を指定します。

##Node Affinityの動作確認

applyして動作を確認します。

```
$ kubectl apply -f nodeAffinity.yaml
pod/with-node-affinity created
$ kubectl get pod -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
with-node-affinity   1/1     Running   0          11s   192.168.69.210   k8s-worker02   <none>           <none>
```

想定通りk8s-worker02にデプロイされていますね。
requiredDuringSchedulingIgnoredDuringExecutionは満たさないといけませんが、preferredDuringSchedulingIgnoredDuringExecutionは必ずしも満たす必要はありません。

なお、requiredDuringSchedulingIgnoredDuringExecutionを満たすNodeがない場合、preferredDuringSchedulingIgnoredDuringExecutionを満たすNodeがあってもスケジューリングされません。

Podを削除して、k8s-worker02をスケジューリング対象から外してから、再度applyしてみます。

```
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   105d   v1.17.3
k8s-worker01   Ready                      <none>   105d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   105d   v1.17.3
$ kubectl apply -f nodeAffinity.yaml
pod/with-node-affinity created
$ kubectl get pod
NAME                 READY   STATUS    RESTARTS   AGE
with-node-affinity   0/1     Pending   0          5s
$ kubectl describe pod with-node-affinity
Name:         with-node-affinity
・・・
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  66s (x2 over 66s)  default-scheduler  0/3 nodes are available: 1 node(s) were unschedulable, 2 node(s) didn't match node selector.
```

スケジューリングされず、Pendingで止まってますね。

#Node Anti-Affinity
Node Anti-Affinityは、Node Affinityの条件（operator）を否定形にします。
以下の例では、「disktypeがhdd"ではない"Node」（＝k8s-worker01）にデプロイされます。

```nodeAntiAffinity.yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-anti-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: NotIn
            values:
            - hdd
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - k8s-worker01
  containers:
  - name: nginx-na
    image: nginx:latest
```

```
$ kubectl apply -f nodeAntiAffinity.yaml
pod/node-anti-affinity created
$ kubectl get pod -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
node-anti-affinity   1/1     Running   0          31s   192.168.79.116   k8s-worker01   <none>           <none>
```

#Inter-Pod Affinity
Inter-Pod Affinityは既にデプロイされているPodのラベルをキーにして、新規にデプロイされるPodのスケジューリングを決定します。
例えば、あるサービスを構成するPodは同じノードにデプロイすることで、レイテンシーを極力小さくするなどの利用方法が考えられますね。

##設定
ここでは、既に以下のPodがデプロイされています。

```
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
redis-prd                    1/1     Running   0          20s   192.168.79.111   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   0          20s   192.168.69.225   k8s-worker02   <none>           <none>            stg
```

Production環境に新たにnginxをデプロイします。このとき、既にデプロイされているredisと同じノードにデプロイさせたいと思います。

マニフェストはNode Affinityと似ていますが、topologyKeyが追加となっています。ここではhostnameを指定していますので、prdラベルを持つPodと同じhostnameのNodeにデプロイされます。

```interPodAffinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-prd
spec:
  selector:
    matchLabels:
      env: prd
  replicas: 3
  template:
    metadata:
      labels:
        env: prd
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: env
                operator: In
                values:
                - prd
            topologyKey: "kubernetes.io/hostname"　#スケジューリング対象の範囲を指定
      containers:
      - name: nginx-prd
        image: nginx:latest
```

##Inter-Pod Affinityの動作確認

このマニフェストをapplyします。

```
$ kubectl apply -f interPodAffinity.yaml
deployment.apps/nginx-prd created
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
nginx-prd-7d498d98ff-djzvz   1/1     Running   0          16s   192.168.79.120   k8s-worker01   <none>           <none>            prd
nginx-prd-7d498d98ff-gtbzj   1/1     Running   0          16s   192.168.79.122   k8s-worker01   <none>           <none>            prd
nginx-prd-7d498d98ff-vtmk4   1/1     Running   0          16s   192.168.79.127   k8s-worker01   <none>           <none>            prd
redis-prd                    1/1     Running   0          10m   192.168.79.111   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   0          10m   192.168.69.225   k8s-worker02   <none>           <none>            stg
```

3つのPod全てがredis-prdと同じNodeにデプロイされていますね。

なお、今回はrequiredDuringSchedulingIgnoredDuringExecutionのみを指定しましたが、Node Affinityと同様にpreferredDuringSchedulingIgnoredDuringExecutionも指定することができます。

#Inter-Pod Anti-affinity
Inter-Pod Anti-affinityは、特定のPodがデプロイされていないノード上に新たなPodをスケジューリングするポリシーです。

##設定
Node Anti-Affinityとは異なり、operatorを否定形にするのではなく、spec.affinityフィールドにpodAntiAffinityをマニフェストで指定できます。
以下の例は、inter-Pod Affinityのマニフェストの「podAffinity」を「podAntiAffinity」に変更しています。


```interPodAntiAffinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-prd
spec:
  selector:
    matchLabels:
      env: prd
  replicas: 3
  template:
    metadata:
      labels:
        env: prd
    spec:
      affinity:
        podAntiAffinity: #podAffinityから変更
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: env
                operator: In
                values:
                - prd
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: nginx-prd
        image: nginx:latest
```

##Inter-Pod Anti-affinityの動作確認

このマニフェストをapplyして動作を確認します。初期状態として、redisのみがデプロイされています。

```
$ kubectl get pod -o wide -L env
NAME        READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
redis-prd   1/1     Running   1          22h   192.168.79.121   k8s-worker01   <none>           <none>            prd
redis-stg   1/1     Running   1          22h   192.168.69.229   k8s-worker02   <none>           <none>            stg
$ kubectl apply -f interPodAntiAffinity.yaml
deployment.apps/nginx-prd created
$ kubectl get pod -o wide -L env
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES   ENV
nginx-prd-7df77bb75f-69gcw   0/1     Pending   0          54s   <none>           <none>         <none>           <none>            prd
nginx-prd-7df77bb75f-7gvq6   0/1     Pending   0          54s   <none>           <none>         <none>           <none>            prd
nginx-prd-7df77bb75f-xqj5v   1/1     Running   0          54s   192.168.69.221   k8s-worker02   <none>           <none>            prd
redis-prd                    1/1     Running   1          22h   192.168.79.121   k8s-worker01   <none>           <none>            prd
redis-stg                    1/1     Running   1          22h   192.168.69.229   k8s-worker02   <none>           <none>            stg
```
replica数3のうち、1つだけPodがRunningになって、残りの2つはPendingで止まってますね。
詳細を確認します。

```
$ kubectl describe replicasets.apps nginx-prd-7df77bb75f
Name:           nginx-prd-7df77bb75f
・・・
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  84s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-xqj5v
  Normal  SuccessfulCreate  83s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-69gcw
  Normal  SuccessfulCreate  83s   replicaset-controller  Created pod: nginx-prd-7df77bb75f-7gvq6
$ kubectl describe pod nginx-prd-7df77bb75f-69gcw
Name:           nginx-prd-7df77bb75f-69gcw
・・・
Events:
  Type     Reason            Age                 From               Message
  ----     ------            ----                ----               -------
  Warning  FailedScheduling  35s (x2 over 101s)  default-scheduler  0/3 nodes are available: 1 node(s) had taints that the pod didn't tolerate, 2 node(s) didn't match pod affinity/anti-affinity.
```
ReplicaSetまでは正常ですが、PodのデプロイでFailedSchedulingになっています。
あまり考えずに、「podAffinity」を「podAntiAffinity」に変更しましたが、よくよく考えると「prdラベルのPodがないノードにprdラベルのPodをデプロイする」という設定なので、1つ目のnginx-prdがデプロイされた時点で、全てのノード（2ノード）にprdラベルのPodが存在することになりますね。なので、2つ目、3つ目のPodがPendingで止まるのは正常な動作です。

#まとめ
今回は5つのスケジューリングの制御方法について確認しました。環境や要件に応じて、色々な制御ができそうですね。
また、Node Affinity/Node Anti-Affinity/Inter-Pod Affinity/Inter-Pod Anti-Affinityの4つは組み合わせて利用することもできますので、さらに細かく制御することができます。
まあ、細かくすればするほど設計が大変になりそうではありますが。。。