[Kubernetes]ノードメンテナンスについて確認する

#はじめに
パッチ適用や障害時のメンテナンスなどKubernetesクラスタを構成するノードを停止する場面があると思います。
クラスタで稼働しているPod全てを停止していいなら悩むこともありませんが、実際には停止したいノードのみを停止させて、Podは稼働させた状態でサービスを継続することがほとんどだと思います。

今回はクラスタを構成するノードをメンテナンスする場面について確認したいと思います。

#cordon/uncordon（スケジューリング対象からの除外/追加）
kubectl cordonコマンドでノードを指定することで、そのノードをスケジューラーからのスケジューリング対象から外すことができます。
以下の環境でその動作を確認したいと思います。

##スケジューリング対象からの除外
replica数が4のDeploymentがデプロイされています。それぞれ2つのworkerノードにデプロイされています。

```
$ kubectl get pod -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx-dep-5589d85476-crlmg   1/1     Running   0          21s   192.168.69.235   k8s-worker02   <none>           <none>
nginx-dep-5589d85476-flmth   1/1     Running   0          21s   192.168.79.77    k8s-worker01   <none>           <none>
nginx-dep-5589d85476-fvkl4   1/1     Running   0          21s   192.168.79.76    k8s-worker01   <none>           <none>
nginx-dep-5589d85476-hw5k4   1/1     Running   0          21s   192.168.69.200   k8s-worker02   <none>           <none>
$ kubectl get node
NAME           STATUS   ROLES    AGE    VERSION
k8s-master     Ready    master   102d   v1.17.3
k8s-worker01   Ready    <none>   102d   v1.17.3
k8s-worker02   Ready    <none>   102d   v1.17.3
```

このうち、k8s-worker02をスケジューリング対象から外します。

```
$ kubectl cordon k8s-worker02
node/k8s-worker02 cordoned
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   102d   v1.17.3
k8s-worker01   Ready                      <none>   102d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   102d   v1.17.3
```
STATUSに「SchedulingDisabled」が追加されていますね。

この状態でreplica数を8にします。

```
$ kubectl apply -f nginx-dep.yaml
deployment.apps/nginx-dep configured
$ kubectl get pod -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP               NODE           NOMINATED NODE   READINESS GATES
nginx-dep-5589d85476-47gxt   1/1     Running   0          115s    192.168.79.119   k8s-worker01   <none>           <none>
nginx-dep-5589d85476-4hb62   1/1     Running   0          115s    192.168.79.66    k8s-worker01   <none>           <none>
nginx-dep-5589d85476-bgqbb   1/1     Running   0          115s    192.168.79.124   k8s-worker01   <none>           <none>
nginx-dep-5589d85476-crlmg   1/1     Running   0          3m16s   192.168.69.235   k8s-worker02   <none>           <none>
nginx-dep-5589d85476-flmth   1/1     Running   0          3m16s   192.168.79.77    k8s-worker01   <none>           <none>
nginx-dep-5589d85476-fvkl4   1/1     Running   0          3m16s   192.168.79.76    k8s-worker01   <none>           <none>
nginx-dep-5589d85476-hw5k4   1/1     Running   0          3m16s   192.168.69.200   k8s-worker02   <none>           <none>
nginx-dep-5589d85476-qtgh6   1/1     Running   0          115s    192.168.79.121   k8s-worker01   <none>           <none>
```

新しくデプロイされたPodはすべてworker01にデプロイされていますね。

##スケジューリング対象への追加
スケジューリング対象へ追加（元に戻す）には、kubectl uncordonコマンドでノードを指定します。

```
$ kubectl uncordon k8s-worker02
node/k8s-worker02 uncordoned
$ kubectl get node
NAME           STATUS   ROLES    AGE    VERSION
k8s-master     Ready    master   102d   v1.17.3
k8s-worker01   Ready    <none>   102d   v1.17.3
k8s-worker02   Ready    <none>   102d   v1.17.3
```

STATUSの「SchedulingDisabled」がなくなりました。

cordon/uncodonだけですと、新しくデプロイされるPodはデプロイされなくなりますが、既にデプロイされているPodはそのままです。
例えば、今はまだ稼働させておくけど、近い将来停止予定のノードをスケジューリング対象から外す場合が考えられますね。

#drain(Podの退避)
kubectl cordonだとノードをスケジューリング対象から外すだけでPodはそのままです。そのため、すぐにノードを停止させたい場合には「kubectl drain」コマンドを使用します。kubectl drainコマンドによって、Podを別ノードに退避させることができます。

##Podの退避
以下の環境で確認します。

```
$ kubectl get pod -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx-dep-5589d85476-jj9gf   1/1     Running   0          25s   192.168.69.198   k8s-worker02   <none>           <none>
nginx-dep-5589d85476-s5xl6   1/1     Running   0          25s   192.168.79.69    k8s-worker01   <none>           <none>
```

kubectl drainコマンドで「k8s-worker02」ノードを指定します。

```
$ kubectl drain k8s-worker02
node/k8s-worker02 cordoned
evicting pod "nginx-dep-5589d85476-jj9gf"
pod/nginx-dep-5589d85476-jj9gf evicted
node/k8s-worker02 evicted
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   102d   v1.17.3
k8s-worker01   Ready                      <none>   102d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   102d   v1.17.3
```

kubectl drainコマンドを実行すると、cordonも実行されます。その次にk8s-worker02にあったPodがevicted（追い出された）とメッセージにあります。
Podの状態を確認します。


```
$ kubectl get pod -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP              NODE           NOMINATED NODE   READINESS GATES
nginx-dep-5589d85476-jt9pj   1/1     Running   0          4m2s    192.168.79.67   k8s-worker01   <none>           <none>
nginx-dep-5589d85476-s5xl6   1/1     Running   0          4m44s   192.168.79.69   k8s-worker01   <none>           <none>
```

k8s-worker02にあったPod（nginx-dep-5589d85476-jj9gf）は停止し、k8s-worker01で新たにPod（nginx-dep-5589d85476-jt9pj）がデプロイされたことがわかります。

別ターミナルでPodの状態変化を確認すると、以下のようになります。

```
$ kubectl get pod -w
NAME                         READY   STATUS    RESTARTS   AGE
nginx-dep-5589d85476-jj9gf   1/1     Running   0          31s
nginx-dep-5589d85476-s5xl6   1/1     Running   0          31s
nginx-dep-5589d85476-jj9gf   1/1     Terminating   0          42s
nginx-dep-5589d85476-jt9pj   0/1     Pending       0          0s
nginx-dep-5589d85476-jt9pj   0/1     Pending       0          0s
nginx-dep-5589d85476-jt9pj   0/1     ContainerCreating   0          0s
nginx-dep-5589d85476-jj9gf   0/1     Terminating         0          43s
nginx-dep-5589d85476-jt9pj   0/1     ContainerCreating   0          1s
nginx-dep-5589d85476-jj9gf   0/1     Terminating         0          44s
nginx-dep-5589d85476-jj9gf   0/1     Terminating         0          44s
nginx-dep-5589d85476-jt9pj   1/1     Running             0          7s
```

workerノードをスケジューリング対象に追加するには、同様にkubectl uncordonを実行します。

```
$ kubectl uncordon k8s-worker02
node/k8s-worker02 uncordoned
$ kubectl get node
NAME           STATUS   ROLES    AGE    VERSION
k8s-master     Ready    master   102d   v1.17.3
k8s-worker01   Ready    <none>   102d   v1.17.3
k8s-worker02   Ready    <none>   102d   v1.17.3
```

##drainの注意事項
Kubernetsにはライブマイグレーション機能はありません。そのため、kubectl drainコマンドでは、指定したノードにデプロイされているPodは停止されたのち、Deploymentのセルフヒーリング機能で別のノードにデプロイされました。
そのため、DeploymentやReplicaSetではなく、Podでデプロイされている場合は、evictedされません。

以下の環境で確認します。

```
$ kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          14s   192.168.69.255   k8s-worker02   <none>           <none>
$ kubectl get deployments.apps
No resources found in default namespace.
```

k8s-worker02をdrainで除外します。

```
$ kubectl drain k8s-worker02
node/k8s-worker02 cordoned
node/k8s-worker02 drained
```

cordonされてノードはスケジューリング対象からは除外されますが、Podはそのまま残っています。

```
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   102d   v1.17.3
k8s-worker01   Ready                      <none>   102d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   102d   v1.17.3
$ kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          96s   192.168.69.255   k8s-worker02   <none>           <none>
```
なお、kubectl drainコマンドに--forceオプションを付与して実行すると、Podは削除されます。

```
$ kubectl drain k8s-worker02 --force
node/k8s-worker02 cordoned
WARNING: deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: default/nginx
evicting pod "nginx"
pod/nginx evicted
node/k8s-worker02 evicted
$ kubectl get pod
No resources found in default namespace.
$ kubectl get node
NAME           STATUS                     ROLES    AGE    VERSION
k8s-master     Ready                      master   102d   v1.17.3
k8s-worker01   Ready                      <none>   102d   v1.17.3
k8s-worker02   Ready,SchedulingDisabled   <none>   102d   v1.17.3
```

1PodでもDeploymentでデプロイすることで、バージョン管理ができるメリットがありますが、このノードのメンテナンスを考えてもDeploymentにしておいた方がよいですね。

#PodDisruptionBudget
PodDisruptionBudget（PDB）はkubectl drainでPodを停止できる最大数を設定するリソースです。設定のパラメータは以下のうち、どちらかを指定します。

* minAvailable：最小のPod起動数、またはパーセンテージで指定
* maxUnavailable：最大のPod停止数、またはパーセンテージで指定

また、対象となるPodをラベルで指定します。

```pdb.yaml
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: pdb
spec:
#  minAvailable: 1
  maxUnavailable: 1
  selector:
    matchLabels:
      app: app1
```


```
$ kubectl apply -f pdb.yaml
poddisruptionbudget.policy/pdb created
$ kubectl get poddisruptionbudgets.policy
NAME   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
pdb    N/A             1                 1                     12s
```

##確認
以下の環境で確認します。
nginxとredisがそれぞれ8Podずつデプロイされています。nginxのラベルが「app1」ですので、PDBの制御対象はnginxになります。

```
$ kubectl get pod -o wide -L app
NAME                         READY   STATUS    RESTARTS   AGE     IP               NODE           NOMINATED NODE   READINESS GATES   APP
nginx-dep-5589d85476-crvmc   1/1     Running   0          8m      192.168.79.88    k8s-worker01   <none>           <none>            app1
nginx-dep-5589d85476-qcmlv   1/1     Running   0          8m      192.168.69.211   k8s-worker02   <none>           <none>            app1
nginx-dep-5589d85476-rn9tc   1/1     Running   0          8m      192.168.79.91    k8s-worker01   <none>           <none>            app1
nginx-dep-5589d85476-rxs7f   1/1     Running   0          8m      192.168.79.83    k8s-worker01   <none>           <none>            app1
nginx-dep-5589d85476-s6tr4   1/1     Running   0          8m      192.168.69.204   k8s-worker02   <none>           <none>            app1
nginx-dep-5589d85476-vs9bv   1/1     Running   0          8m      192.168.69.212   k8s-worker02   <none>           <none>            app1
nginx-dep-5589d85476-xzwr5   1/1     Running   0          8m      192.168.69.203   k8s-worker02   <none>           <none>            app1
nginx-dep-5589d85476-z9g8r   1/1     Running   0          8m      192.168.79.86    k8s-worker01   <none>           <none>            app1
redis-dep-79fd57c7b9-dwzh9   1/1     Running   0          8m27s   192.168.79.75    k8s-worker01   <none>           <none>            app2
redis-dep-79fd57c7b9-jsrbg   1/1     Running   0          8m27s   192.168.79.85    k8s-worker01   <none>           <none>            app2
redis-dep-79fd57c7b9-kg228   1/1     Running   0          8m27s   192.168.69.209   k8s-worker02   <none>           <none>            app2
redis-dep-79fd57c7b9-l2xlg   1/1     Running   0          8m27s   192.168.69.206   k8s-worker02   <none>           <none>            app2
redis-dep-79fd57c7b9-pfwmj   1/1     Running   0          8m27s   192.168.79.68    k8s-worker01   <none>           <none>            app2
redis-dep-79fd57c7b9-qkxwt   1/1     Running   0          8m27s   192.168.69.215   k8s-worker02   <none>           <none>            app2
redis-dep-79fd57c7b9-vqw55   1/1     Running   0          8m27s   192.168.79.89    k8s-worker01   <none>           <none>            app2
redis-dep-79fd57c7b9-wwcj2   1/1     Running   0          8m27s   192.168.69.207   k8s-worker02   <none>           <none>            app2
```

worker02をdrainします。

```
$ kubectl drain k8s-worker02
node/k8s-worker02 cordoned
evicting pod "nginx-dep-5589d85476-vs9bv"
evicting pod "nginx-dep-5589d85476-qcmlv"
evicting pod "nginx-dep-5589d85476-s6tr4"
evicting pod "redis-dep-79fd57c7b9-kg228"
evicting pod "nginx-dep-5589d85476-xzwr5"
evicting pod "redis-dep-79fd57c7b9-l2xlg"
evicting pod "redis-dep-79fd57c7b9-qkxwt"
evicting pod "redis-dep-79fd57c7b9-wwcj2"
error when evicting pod "nginx-dep-5589d85476-s6tr4" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pod "nginx-dep-5589d85476-vs9bv" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/redis-dep-79fd57c7b9-wwcj2 evicted
evicting pod "nginx-dep-5589d85476-s6tr4"
error when evicting pod "nginx-dep-5589d85476-s6tr4" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-vs9bv"
error when evicting pod "nginx-dep-5589d85476-vs9bv" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/redis-dep-79fd57c7b9-kg228 evicted
pod/redis-dep-79fd57c7b9-qkxwt evicted
pod/nginx-dep-5589d85476-qcmlv evicted
pod/redis-dep-79fd57c7b9-l2xlg evicted
evicting pod "nginx-dep-5589d85476-s6tr4"
evicting pod "nginx-dep-5589d85476-vs9bv"
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-vs9bv" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/nginx-dep-5589d85476-s6tr4 evicted
evicting pod "nginx-dep-5589d85476-vs9bv"
error when evicting pod "nginx-dep-5589d85476-vs9bv" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-vs9bv"
error when evicting pod "nginx-dep-5589d85476-vs9bv" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-vs9bv"
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-xzwr5"
error when evicting pod "nginx-dep-5589d85476-xzwr5" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod "nginx-dep-5589d85476-xzwr5"
pod/nginx-dep-5589d85476-vs9bv evicted
pod/nginx-dep-5589d85476-xzwr5 evicted
node/k8s-worker02 evicted
```

nginxもredisと同様にすべての対象Pod（4つ）が最初にevicting されていますが、PDBで1つずつに制御されていますね。
でも、「error」が表示されるのはちょっと違和感があります。この機能はまだベータ版ですので、今後変わるのかも知れません。

#まとめ
今回はworkerノードメンテナンス時のクラスタ、Podの操作について確認しました。
KubernetsはHypervisor型の仮想化機能と違って、ライブマイグレーション機能がありません。マイクロアーキテクチャの思想から考えて今後も実装されないと思いますので、Podそのものを停止させないのではなく、Podは止まるものとして、保守時を含めてサービスをいかに止めないかを考える必要がありますね。