KuredでKubernetesのNodeアップデートを自動で行う

# はじめに
* Kubernetesクラスターを運用するうえでは様々な要素を考えないといけないですが、その一つに**Nodeのアップデート**があります。Kubernetesが動作するサーバーそのもののセキュリティを担保するのは、定期的なサーバー・OSのアップデートが大切で、特にオンプレ環境などで運用者がNodeアップデートを実行する必要がある場合、アップデートに向けた戦略を考えることが重要になります。

* Nodeのアップデートを行う場合、以下のようなことを考える必要があります。
  * アップデート情報の検知
  * クラスター上に動くPodの一時的な退避
  * アップデートの実行

これらを踏まえたアップデート運用方法を考える手間を省くことができるかもしれないのが、Kuredというツールです。

# Kuredとは
[Kured](https://github.com/weaveworks/kured) (KUbernetes REboot Daemon)はWeaveworks社が開発しているツールで、大まかに以下のような特徴を備えています。

* デフォルトでは```/var/run/reboot-required```ファイルが存在するか否かを検知し、存在する場合は対象のNodeを再起動する(```/var/run/reboot-required```については[こちらから](https://linux-audit.com/check-required-reboot-on-debian-ubuntu-systems/))。
* 再起動する際は一度に1つのNodeずつしか行わなず、再起動前後でcordon/uncordon・drainすることで、事前にPodの退避を行ったうえでNodeの再起動を行う。

# Kuredを使ってみる
今回は以下のような環境で実行しました。なお、Kuredの対応バージョンはgithubに記載されており、まだkubernetes ver.1.15には対応していないように見えますが、一応は動作することが確認できました（ただしKuredのすべての機能が正しく動作するかは保証できないため、そこは十分検証したうえで本番利用してください）。

## 検証環境
* Kubernetesバージョン：1.15
* 構築環境：virtualbox（デプロイではvagrantを利用）
* 構築方法：[kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/)
* 1master + 2 node
  * ```kube-master01```: 172.16.33.10
  * ```kube-worker01```: 172.16.33.20
  * ```kube-worker02```: 172.16.33.30

## Kuredのデプロイ
それではKuredをデプロイします。デプロイ方法は[githubページ](https://github.com/weaveworks/kured)に書かれており、```kubectl```コマンドでYAMLファイルをデプロイして利用します。また[Helmチャートも公開されており](https://github.com/helm/charts/tree/master/stable/kured)、そちらを利用することもできます。

```console
[root@kube-master01 ~]# kubectl apply -f https://github.com/weaveworks/kured/releases/download/1.2.0/kured-1.2.0-dockerhub.yaml
clusterrole.rbac.authorization.k8s.io/kured created
clusterrolebinding.rbac.authorization.k8s.io/kured created
role.rbac.authorization.k8s.io/kured created
rolebinding.rbac.authorization.k8s.io/kured created
serviceaccount/kured created
daemonset.apps/kured created
[root@kube-master01 ~]#
```

デプロイ後は以下の通り、```kube-system```Namespaceに```kured```Podがデプロイされています。

```console
[root@kube-master01 ~]# kubectl get pods -n kube-system -o wide
NAME                                       READY   STATUS    RESTARTS   AGE     IP                NODE            NOMINATED NODE   READINESS GATES
calico-kube-controllers-7bd78b474d-sftn7   1/1     Running   2          7d4h    192.168.237.73    kube-master01   <none>           <none>
calico-node-ksl4b                          1/1     Running   2          7d4h    172.16.33.30      kube-worker02   <none>           <none>
calico-node-mqpss                          1/1     Running   5          7d4h    172.16.33.20      kube-worker01   <none>           <none>
calico-node-wfws8                          1/1     Running   2          7d4h    172.16.33.10      kube-master01   <none>           <none>
coredns-5c98db65d4-4j67d                   1/1     Running   2          7d4h    192.168.237.72    kube-master01   <none>           <none>
coredns-5c98db65d4-zsqkt                   1/1     Running   2          7d4h    192.168.237.71    kube-master01   <none>           <none>
etcd-kube-master01                         1/1     Running   2          7d4h    172.16.33.10      kube-master01   <none>           <none>
kube-apiserver-kube-master01               1/1     Running   3          7d4h    172.16.33.10      kube-master01   <none>           <none>
kube-controller-manager-kube-master01      1/1     Running   2          7d4h    172.16.33.10      kube-master01   <none>           <none>
kube-proxy-mzfm4                           1/1     Running   5          7d4h    172.16.33.20      kube-worker01   <none>           <none>
kube-proxy-t8sld                           1/1     Running   2          7d4h    172.16.33.30      kube-worker02   <none>           <none>
kube-proxy-zsrgb                           1/1     Running   2          7d4h    172.16.33.10      kube-master01   <none>           <none>
kube-scheduler-kube-master01               1/1     Running   2          7d4h    172.16.33.10      kube-master01   <none>           <none>
★ kured-5qvrc                                1/1     Running   0          5m41s   192.168.247.239   kube-worker02   <none>           <none>
★ kured-9s9m9                                1/1     Running   0          5m54s   192.168.132.32    kube-worker01   <none>           <none>
★ kured-n82kt                                1/1     Running   0          5m43s   192.168.237.76    kube-master01   <none>           <none>
[root@kube-master01 ~]#
```

```kubectl logs```でKuredのPodが出力したログを確認すると、以下のように出力されます。Kuredのバージョン情報や再起動のトリガーとなるファイル、検知周期などが出力されています。

```console
[root@kube-master01 ~]# kubectl logs -n kube-system kured-9s9m9
time="2019-08-10T14:34:02Z" level=info msg="Kubernetes Reboot Daemon: 1.2.0"
time="2019-08-10T14:34:02Z" level=info msg="Node ID: kube-worker01"
time="2019-08-10T14:34:02Z" level=info msg="Lock Annotation: kube-system/kured:weave.works/kured-node-lock"
time="2019-08-10T14:34:02Z" level=info msg="Reboot Sentinel: /var/run/reboot-required every 1h0m0s"
time="2019-08-10T14:34:02Z" level=info msg="Blocking Pod Selectors: []"
[root@kube-master01 ~]#
```

今回は事前に以下のDeploymentファイルをデプロイしておき、サーバーのアップデート時にどのような挙動をするか見てみます。

```sample-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 4
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: nginx-container
        image: nginx:1.12
        ports:
        - containerPort: 80
```

```console
[root@kube-master01 ~]# kubectl apply -f sample-deployment.yaml
deployment.apps/sample-deployment created
[root@kube-master01 ~]# kubectl get deployment
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
sample-deployment   4/4     4            4           12s
[root@kube-master01 ~]# kubectl get rs
NAME                           DESIRED   CURRENT   READY   AGE
sample-deployment-7bb5fc6bc6   4         4         4       16s
[root@kube-master01 ~]# kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE            NOMINATED NODE   READINESS GATES
sample-deployment-7bb5fc6bc6-8m9fj   1/1     Running   0          22s   192.168.247.241   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-bx566   1/1     Running   0          22s   192.168.247.240   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-c7vvn   1/1     Running   0          22s   192.168.132.34    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-g8jjn   1/1     Running   0          22s   192.168.132.33    kube-worker01   <none>           <none>
[root@kube-master01 ~]#

```

テスト用Podをデプロイしたので、Kuredのテストを行います。ここでは```kube-worker01```に```/var/run/reboot-required```ファイルを配置することで、Kuredによる再起動を誘導します。

```console
[root@kube-worker01 ~]# touch  /var/run/reboot-required
[root@kube-worker01 ~]# ls -l  /var/run/reboot-required
-rw-r--r-- 1 root root 0 Aug 10 14:41 /var/run/reboot-required
[root@kube-worker01 ~]#
```

デフォルトの設定では１時間ごとに```/var/run/reboot-required```ファイルがないか確認しますので、しばらくしてから```Kured```のPodのログを確認すると、再起動したのがわかります。ここでは周辺のリソースも含めて見ています。

```console
# NodeのCordon/Uncordon、Drainの検知
[root@kube-master01 ~]# kubectl get nodes -o wide -w
NAME            STATUS   ROLES    AGE    VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION               CONTAINER-RUNTIME
kube-master01   Ready    master   7d5h   v1.15.1   172.16.33.10   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-worker01   Ready    <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-worker02   Ready    <none>   7d5h   v1.15.1   172.16.33.30   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2

## kube-worker01がスケジューリングから外れたのち、再びスケジュール対象となっているのがわかります
kube-worker01   Ready    <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready,SchedulingDisabled   <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready,SchedulingDisabled   <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-master01   Ready                      master   7d5h   v1.15.1   172.16.33.10   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-worker02   Ready                      <none>   7d5h   v1.15.1   172.16.33.30   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready,SchedulingDisabled   <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready,SchedulingDisabled   <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-master01   Ready                      master   7d5h   v1.15.1   172.16.33.10   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-worker02   Ready                      <none>   7d5h   v1.15.1   172.16.33.30   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
kube-worker01   Ready,SchedulingDisabled   <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready                      <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2
★ kube-worker01   Ready                      <none>   7d5h   v1.15.1   172.16.33.20   <none>        CentOS Linux 7 (Core)   3.10.0-957.27.2.el7.x86_64   docker://17.3.2


# Kured Podのログ確認
[root@kube-master01 ~]# kubectl logs -n kube-system kured-9s9m9
time="2019-08-10T15:49:11Z" level=info msg="Kubernetes Reboot Daemon: 1.2.0"
time="2019-08-10T15:49:11Z" level=info msg="Node ID: kube-worker01"
time="2019-08-10T15:49:11Z" level=info msg="Lock Annotation: kube-system/kured:weave.works/kured-node-lock"
time="2019-08-10T15:49:11Z" level=info msg="Reboot Sentinel: /var/run/reboot-required every 1h0m0s"
time="2019-08-10T15:49:11Z" level=info msg="Blocking Pod Selectors: []"
★ time="2019-08-10T15:49:11Z" level=info msg="Holding lock"
★ time="2019-08-10T15:49:11Z" level=info msg="Uncordoning node kube-worker01"
★ time="2019-08-10T15:49:11Z" level=info msg="node/kube-worker01 uncordoned" cmd=/usr/bin/kubectl std=out
★ time="2019-08-10T15:49:11Z" level=info msg="Releasing lock"
[root@kube-master01 ~]#


# /var/run/reboot-requiredは削除される
[root@kube-worker01 ~]# ls -l /var/run/reboot-required
ls: cannot access /var/run/reboot-required: No such file or directory
[root@kube-worker01 ~]#

```

またテスト用PodをWatchすると、別のNodeに新たなPodが作成されることがわかります。

```console
# テスト用Podのwatch
[root@kube-master01 ~]# kubectl get pods -o wide -w
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE            NOMINATED NODE   READINESS GATES
sample-deployment-7bb5fc6bc6-8m9fj   1/1     Running   0          63m   192.168.247.241   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-bx566   1/1     Running   0          63m   192.168.247.240   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-c7vvn   1/1     Running   0          63m   192.168.132.34    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-g8jjn   1/1     Running   0          63m   192.168.132.33    kube-worker01   <none>           <none>


## kube-worker01上のPodが削除され、kube-worker02に新たなPodが作成される
★ sample-deployment-7bb5fc6bc6-c7vvn   1/1     Terminating   0          69m   192.168.132.34    kube-worker01   <none>           <none>
★ sample-deployment-7bb5fc6bc6-g8jjn   1/1     Terminating   0          69m   192.168.132.33    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-2b7ld   0/1     Pending       0          0s    <none>            <none>          <none>           <none>
★ sample-deployment-7bb5fc6bc6-2b7ld   0/1     Pending       0          0s    <none>            kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-vb5bg   0/1     Pending       0          0s    <none>            <none>          <none>           <none>
★ sample-deployment-7bb5fc6bc6-vb5bg   0/1     Pending       0          0s    <none>            kube-worker02   <none>           <none>
★ sample-deployment-7bb5fc6bc6-2b7ld   0/1     ContainerCreating   0          0s    <none>            kube-worker02   <none>           <none>
★ sample-deployment-7bb5fc6bc6-vb5bg   0/1     ContainerCreating   0          0s    <none>            kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-g8jjn   0/1     Terminating         0          69m   192.168.132.33    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-c7vvn   0/1     Terminating         0          69m   192.168.132.34    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-vb5bg   0/1     ContainerCreating   0          0s    <none>            kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-2b7ld   0/1     ContainerCreating   0          1s    <none>            kube-worker02   <none>           <none>
★ sample-deployment-7bb5fc6bc6-2b7ld   1/1     Running             0          1s    192.168.247.243   kube-worker02   <none>           <none>
★ sample-deployment-7bb5fc6bc6-vb5bg   1/1     Running             0          1s    192.168.247.242   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-c7vvn   0/1     Terminating         0          69m   192.168.132.34    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-c7vvn   0/1     Terminating         0          69m   192.168.132.34    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-g8jjn   0/1     Terminating         0          69m   192.168.132.33    kube-worker01   <none>           <none>
sample-deployment-7bb5fc6bc6-g8jjn   0/1     Terminating         0          69m   192.168.132.33    kube-worker01   <none>           <none>

^C[root@kube-master01 ~]#
[root@kube-master01 ~]# kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE     IP                NODE            NOMINATED NODE   READINESS GATES
★ sample-deployment-7bb5fc6bc6-2b7ld   1/1     Running   0          4m45s   192.168.247.243   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-8m9fj   1/1     Running   0          74m     192.168.247.241   kube-worker02   <none>           <none>
sample-deployment-7bb5fc6bc6-bx566   1/1     Running   0          74m     192.168.247.240   kube-worker02   <none>           <none>
★ sample-deployment-7bb5fc6bc6-vb5bg   1/1     Running   0          4m45s   192.168.247.242   kube-worker02   <none>           <none>
[root@kube-master01 ~]#


# kube-system NamespaceのPodをwatch
[root@kube-master01 ~]# kubectl get pods -n kube-system -o wide -w
NAME                                       READY   STATUS    RESTARTS   AGE    IP                NODE            NOMINATED NODE   READINESS GATES
calico-kube-controllers-7bd78b474d-sftn7   1/1     Running   2          7d5h   192.168.237.73    kube-master01   <none>           <none>
calico-node-ksl4b                          1/1     Running   2          7d5h   172.16.33.30      kube-worker02   <none>           <none>
calico-node-mqpss                          1/1     Running   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-wfws8                          1/1     Running   2          7d5h   172.16.33.10      kube-master01   <none>           <none>
coredns-5c98db65d4-4j67d                   1/1     Running   2          7d5h   192.168.237.72    kube-master01   <none>           <none>
coredns-5c98db65d4-zsqkt                   1/1     Running   2          7d5h   192.168.237.71    kube-master01   <none>           <none>
etcd-kube-master01                         1/1     Running   2          7d5h   172.16.33.10      kube-master01   <none>           <none>
kube-apiserver-kube-master01               1/1     Running   3          7d5h   172.16.33.10      kube-master01   <none>           <none>
kube-controller-manager-kube-master01      1/1     Running   2          7d5h   172.16.33.10      kube-master01   <none>           <none>
kube-proxy-mzfm4                           1/1     Running   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
kube-proxy-t8sld                           1/1     Running   2          7d5h   172.16.33.30      kube-worker02   <none>           <none>
kube-proxy-zsrgb                           1/1     Running   2          7d5h   172.16.33.10      kube-master01   <none>           <none>
kube-scheduler-kube-master01               1/1     Running   2          7d5h   172.16.33.10      kube-master01   <none>           <none>
kured-5qvrc                                1/1     Running   0          72m    192.168.247.239   kube-worker02   <none>           <none>
kured-9s9m9                                1/1     Running   0          72m    192.168.132.32    kube-worker01   <none>           <none>
kured-n82kt                                1/1     Running   0          72m    192.168.237.76    kube-master01   <none>           <none>

## kube-worker01上のPodが一時的に利用不可となったのち、再び利用可能となる
kube-proxy-mzfm4                           0/1     Error     5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
kured-9s9m9                                0/1     Error     0          74m    <none>            kube-worker01   <none>           <none>
kured-9s9m9                                0/1     Error     0          74m    <none>            kube-worker01   <none>           <none>
kured-9s9m9                                1/1     Running   1          74m    192.168.132.35    kube-worker01   <none>           <none>
calico-node-mqpss                          0/1     Completed   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
kube-proxy-mzfm4                           1/1     Running     6          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-mqpss                          0/1     Completed   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-mqpss                          0/1     Completed   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-mqpss                          0/1     Completed   5          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-mqpss                          0/1     Running     6          7d5h   172.16.33.20      kube-worker01   <none>           <none>
calico-node-mqpss                          1/1     Running     6          7d5h   172.16.33.20      kube-worker01   <none>           <none>
kured-9s9m9                                0/1     Error       1          74m    192.168.132.35    kube-worker01   <none>           <none>
kured-9s9m9                                0/1     CrashLoopBackOff   1          75m    192.168.132.35    kube-worker01   <none>           <none>
kured-9s9m9                                1/1     Running            2          75m    192.168.132.35    kube-worker01   <none>           <none>

^C[root@kube-master01 ~]#
```


## その他オプション
Kuredには他にも利用できるオプションが存在します。特定のラベルが付与されたPodが存在する場合に再起動を阻止したり、PrometheusやSlackとの連携も可能です。

* ```--blocking-pod-selector```: 特定のラベルを指定し、それが付与されたPodが存在するNodeは再起動されない
* ```--prometheus-url```: Prometheusを利用する際に指定するURL
* ```--slack-hook-url```: 再起動の通知をslackから通知するためのWebhook URL

ここではslackの通知機能を試してみます。Kuredをデプロイ済みの場合は、```kubectl edit```等でコンフィグを編集します。

```console
[root@kube-master01 ~]# kubectl get ds -n kube-system
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
calico-node   3         3         3       3            3           beta.kubernetes.io/os=linux   7d3h
kube-proxy    3         3         3       3            3           beta.kubernetes.io/os=linux   7d3h
kured         3         3         3       3            3           <none>                        30m
[root@kube-master01 ~]# kubectl edit ds kured -n kube-system

(中略)

  containers:
  - command:
    - /usr/bin/kured
    - --slack-hook-url=<slackのWebhook URLを指定>

(中略)


daemonset.extensions/kured edited
[root@kube-master01 ~]#
```

上記の設定を入れておくと、再起動の際、以下のようにSlackに通知が飛んできます。

![kured-01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177048/92ecae45-8f26-43cc-b709-e6dcf013180d.jpeg)

# まとめ
* Kuredを使うことでKubernetesノードのアップデートを安全に行うことができます。アップデートは脆弱性への対策として必須であり、それをスムーズに実行できることは、Kubernetes環境のセキュリティを高めるうえで重要となります。
* ただし、Nodeアップデートを行う際には、一時的に別のNodeにPodが移動するため、アップデートするNode以外のNodeに対する負荷が増加します。そのため、負荷増加を見越したクラスター設計が必要となります（これはNodeアップデートに限らず、Kubernetesを本番環境で利用するうえでは、Nodeがダウンしたときに備えた設計が必要です）。
* また、Node上のリソースのアップデート方法を検討する必要もあります。今回は特に指定をせずにDeploymentを使用したため```RollingUpdate```が実行されましたが、例えばPod単体でデプロイしている場合、Nodeアップデートに合わせて削除されてしまいます。そのためリソースの種類やアップデート戦略によっては、考え直す必要があるかもしれません。

## 参考ドキュメント
* [しくみがわかるKubernetes Azureで動かしながら学ぶコンセプトと実践知識](https://www.amazon.co.jp/dp/4798157848)

* [Kubernetes完全ガイド (impress top gear)](https://www.amazon.co.jp/dp/4295004804/)

* [Check for a required reboot on Debian and Ubuntu systems](https://linux-audit.com/check-required-reboot-on-debian-ubuntu-systems/)

* [Kured](https://github.com/weaveworks/kured)

* [SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)