KubernetesでStatefulSetのRolling Updateを試してみた


# はじめに

Kubernetes 1.7でStatefulSetのRolling Updateがbetaとして[機能](http://qiita.com/tkusumi/items/817cf85917efd24c7a4f#statefulset-%E3%81%AE%E8%87%AA%E5%8B%95%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88)が入りました。

PodやNodeのライフサイクルに影響されずにPodから出たファイルのログなどをFluentdなどで収集されるまで置いておきたい用途があり、PersistentVolumeを使うためStatefulSetでvolumeClaimTemplatesを利用しようとしていました。ただ、[この問題](https://github.com/kubernetes/kubernetes/issues/41015)のためか、`kubectl apply` によるimage更新などがうまくいってませんでした。
今回手元のユースケースで、image更新によるStatefulsetの `kubectl apply` が1.6までだと成功しなかった更新が成功することを確認しました。

# 準備

## Kubernetes Cluster

なにはともあれ、kubernetes 1.7 の環境を準備します。今回は GKE で試しています。主題ではないので詳細は省略します。

## StatefulSet

StatefulSetも続いて準備します。簡単にnginxのimageを動かすことにします。

```yaml:nginx-1.12.1-statefulset.yml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: nginx-stateful
spec:
  replicas: 3
  serviceName: nginx
  selector:
    matchLabels:
      app: nginx
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
  podManagementPolicy: Parallel
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.12.1-alpine
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          hostPort: 80
        volumeMounts:
        - mountPath: "/var/log/nginx"
          name: log
        lifecycle:
          preStop:
            exec:
              command: ["kill", "-QUIT", "1"]
        livenessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
    metadata:
      name: nginx
      labels:
        app: nginx
  volumeClaimTemplates:
  - metadata:
      name: log
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 100Gi
```

`updateStrategy` の部分が1.7で追加された記述です。ここに`type: RollingUpdate`と書くことでRolling Updateができるようになります。また、`partition` というパラメータも追加されました。指定することで一部Podから入れ替えたりでカナリアリリースなどで利用できるようです。詳細は[こちら](https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#staging-an-update)をご覧ください。

今回`/var/log/nginx`が真っ新なDiskに付け変わるので、元のimageにある`/dev/stdout`や`/dev/stderr`へのsymlinkは無くなります。また、ログ出力時にディレクトリが存在しないと問題になる場合は、`initContainers`の処理などでディレクトリを作成しておく必要があります。

PersistentVolumeは、Dynamic Provisioningに任せることにして今回明示的には作成してません。`pd-standard`で作成されるため、`pd-ssd`が必要であれば、StorageClassなどの設定が必要です。また、Dynamic Provisioningに頼らない場合にはPersistentDiskの作成も必要です。Dynamic Provisioningで作成されたPersistentDiskは、PersistentVolumeClaimが消えるとPersistentDiskも消されるので注意が必要です。手元だとStatefulSetを消しても、PersistentVolumeClaimは消えなかったので明示的に消さない限りは大丈夫かも知れません。このあたりは別途調べてみたいと思います。



# 実行

StatefulSetを作成してみましょう。`kubectl apply`で作成します。

```shell-session
$ kubectl apply -f nginx-1.12.1-statefulset.yml
```

作成された内容も確認してみます。

```shell-session
$ kubectl get statefulset
NAME             DESIRED   CURRENT   AGE
nginx-stateful   3         3         31s
$ kubectl get pvc
NAME                   STATUS    VOLUME                                     CAPACITY   ACCESSMODES   STORAGECLASS   AGE
log-nginx-stateful-0   Bound     pvc-9d21a174-700d-11e7-9d7f-42010a92004d   100Gi      RWO           standard       48s
log-nginx-stateful-1   Bound     pvc-9d28003b-700d-11e7-9d7f-42010a92004d   100Gi      RWO           standard       48s
log-nginx-stateful-2   Bound     pvc-9d2c1e8d-700d-11e7-9d7f-42010a92004d   100Gi      RWO           standard       48s
$ kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                          STORAGECLASS   REASON    AGE
pvc-9d21a174-700d-11e7-9d7f-42010a92004d   100Gi      RWO           Delete          Bound     default/log-nginx-stateful-0   standard                 57s
pvc-9d28003b-700d-11e7-9d7f-42010a92004d   100Gi      RWO           Delete          Bound     default/log-nginx-stateful-1   standard                 57s
pvc-9d2c1e8d-700d-11e7-9d7f-42010a92004d   100Gi      RWO           Delete          Bound     default/log-nginx-stateful-2   standard                 57s
$ kubectl get po
NAME               READY     STATUS    RESTARTS   AGE
nginx-stateful-0   1/1       Running   0          1m
nginx-stateful-1   1/1       Running   0          1m
nginx-stateful-2   1/1       Running   0          1m
```

アクセスログも確認しておきます

```shell-session
$ kubectl exec   nginx-stateful-0 head /var/log/nginx/access.log
10.44.4.1 - - [24/Jul/2017:01:16:40 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:40 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:50 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:50 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:00 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:00 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:10 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:10 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:20 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:20 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
```

# image更新

続いてimageの更新をします。nginxのimageを1.13.3-alpineにしてみます。

```shell-session
$ kubectl patch statefulset nginx-stateful -p'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:1.13.3-alpine"}]}}}}'
statefulset "nginx-stateful" patched
```

もしくは、上記ymlのimageの部分を書き換えた下記ymlを流します。

```yaml:nginx-1.13.3-statefulset.yml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: nginx-stateful
spec:
  replicas: 3
  serviceName: nginx
  selector:
    matchLabels:
      app: nginx
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
  podManagementPolicy: Parallel
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.13.3-alpine
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          hostPort: 80
        volumeMounts:
        - mountPath: "/var/log/nginx"
          name: log
        lifecycle:
          preStop:
            exec:
              command: ["kill", "-QUIT", "1"]
        livenessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
    metadata:
      name: nginx
      labels:
        app: nginx
  volumeClaimTemplates:
  - metadata:
      name: log
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 100Gi
```

```shell-session
$ kubectl apply -f nginx-1.13.3-statefulset.yml
statefulset "nginx-stateful" configured
```

```shell-session
$ kubectl rollout status statefulset/nginx-stateful
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 1 pods at revision nginx-stateful-46265502...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 2 pods at revision nginx-stateful-46265502...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
statefulset rolling update complete 3 pods at revision nginx-stateful-46265502...
$ kubectl get po
NAME               READY     STATUS    RESTARTS   AGE
nginx-stateful-0   1/1       Running   0          39s
nginx-stateful-1   1/1       Running   0          1m
nginx-stateful-2   1/1       Running   0          1m
```

1podずつ更新されていることが確認できました。

アクセスログも残っていることを確認してみます。

```
$ kubectl exec   nginx-stateful-0 head /var/log/nginx/access.log
10.44.4.1 - - [24/Jul/2017:01:16:40 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:40 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:50 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:16:50 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:00 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:00 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:10 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:10 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:20 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
10.44.4.1 - - [24/Jul/2017:01:17:20 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
```

Pod更新前のログが残っていますね。


# まとめ

Kubetnetes 1.7でStatefulSetのRolling Updateができました。`kubectl apply`によるStatefulSetの更新もできるようになり、CI/CDの中でも使いやすくなったのでないかと思います。