Kubernetes の StatefulSet について検証した時のメモ

ステートフルなコンテナを管理するための StatefulSet の挙動について検証して確認した時のメモ。

[StatefulSets](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)

[Example: Deploying Cassandra with Stateful Sets](https://kubernetes.io/docs/tutorials/stateful-application/cassandra/)


# 環境

```bash
 kubectl version
Client Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.3", GitCommit:"2bba0127d85d5a46ab4b778548be28623b32d0b0", GitTreeState:"clean", BuildDate:"2018-05-28T20:03:09Z", GoVersion:"go1.9.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11+", GitVersion:"v1.11.5-eks-6bad6d", GitCommit:"6bad6d9c768dc0864dab48a11653aa53b5a47043", GitTreeState:"clean", BuildDate:"2018-12-06T23:13:14Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
```

# チュートリアル [Example: Deploying Cassandra with Stateful Sets](https://kubernetes.io/docs/tutorials/stateful-application/cassandra/) を試す

## Creating a Cassandra Headless Service

まずは Service を作成する。

```cassandra-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandra
spec:
  clusterIP: None
  ports:
  - port: 9042
  selector:
    app: cassandra
```


```bash
# Service を作成
$kubectl create -f https://k8s.io/examples/application/cassandra/cassandra-service.yaml
service "cassandra" created

service "cassandra" created

# 作成を確認
$kubectl get svc cassandra
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
cassandra   ClusterIP   None         <none>        9042/TCP   40s
```

## Using a StatefulSet to Create a Cassandra Ring/Validating The Cassandra StatefulSet


```cassandra-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  labels:
    app: cassandra
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      terminationGracePeriodSeconds: 1800
      containers:
      - name: cassandra
        image: gcr.io/google-samples/cassandra:v13
        imagePullPolicy: Always
        ports:
        - containerPort: 7000
          name: intra-node
        - containerPort: 7001
          name: tls-intra-node
        - containerPort: 7199
          name: jmx
        - containerPort: 9042
          name: cql
        resources:
          limits:
            cpu: "500m"
            memory: 1Gi
          requests:
            cpu: "500m"
            memory: 1Gi
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
        lifecycle:
          preStop:
            exec:
              command: 
              - /bin/sh
              - -c
              - nodetool drain
        env:
          - name: MAX_HEAP_SIZE
            value: 512M
          - name: HEAP_NEWSIZE
            value: 100M
          - name: CASSANDRA_SEEDS
            value: "cassandra-0.cassandra.default.svc.cluster.local"
          - name: CASSANDRA_CLUSTER_NAME
            value: "K8Demo"
          - name: CASSANDRA_DC
            value: "DC1-K8Demo"
          - name: CASSANDRA_RACK
            value: "Rack1-K8Demo"
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
        readinessProbe:
          exec:
            command:
            - /bin/bash
            - -c
            - /ready-probe.sh
          initialDelaySeconds: 15
          timeoutSeconds: 5
        # These volume mounts are persistent. They are like inline claims,
        # but not exactly because the names need to match exactly one of
        # the stateful pod volumes.
        volumeMounts:
        - name: cassandra-data
          mountPath: /cassandra_data
  # These are converted to volume claims by the controller
  # and mounted at the paths mentioned above.
  # do not use these in production until ssd GCEPersistentDisk or other ssd pd
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: fast
      resources:
        requests:
          storage: 1Gi
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
  type: pd-ssd
```

StorgeClass これは動かないかも。
とはいえやってみる

```bash
# マニフェストファイル適用.statefull set を storageclass を作成
$kubectl create -f https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml
statefulset.apps "cassandra" created
storageclass.storage.k8s.io "fast" created

# 一つのみ起動
$kubectl get statefulset cassandra
NAME        DESIRED   CURRENT   AGE
cassandra   3         1         3m


# Pod は pending になっている
$ kubectl get pods -l="app=cassandra"
NAME          READY     STATUS    RESTARTS   AGE
cassandra-0   0/1       Pending   0          4m

# やはり Volume の箇所でエラー
$ kubectl describe pod cassandra-0
(一部略)

Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  1s (x94 over 5m)  default-scheduler  pod has unbound PersistentVolumeClaims (repeated 3 times)
  
# pvc(PersistentVolumeClaim) を確認.エラー
$kubectl describe pvc cassandra-data-cassandra-0
Name:          cassandra-data-cassandra-0
Namespace:     default
StorageClass:  fast
Status:        Pending
Volume:
Labels:        app=cassandra
Annotations:   volume.beta.kubernetes.io/storage-provisioner=k8s.io/minikube-hostpath
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
Events:
  Type    Reason                Age                From                         Message
  ----    ------                ----               ----                         -------
  Normal  ExternalProvisioning  57s (x26 over 6m)  persistentvolume-controller  waiting for a volume to be created, either by external provisioner "k8s.io/minikube-hostpath" or manually created by system administrator
```

やはり Volume の作成が失敗。
一度削除して再作成。

```bash
# 一旦削除
$ kubectl delete -f https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml

statefulset.apps "cassandra" deleted
storageclass.storage.k8s.io "fast" deleted

```

マニュフェストファイルで定義されている StorageClass だが EKS ではデフォルトで EBS を使う StorageClass が作成されていた。

```bash
$kubectl get storageclass gp2
NAME            PROVISIONER             AGE
gp2 (default)   kubernetes.io/aws-ebs   6d
```

その為、これを利用するようにマニュフェストファイルを変更。
ストレージクラスの定義を削除し、利用するストレージクラスを既にある gp2 にする

```bash
$diff cassandra-statefulset.yaml cassandra-statefulset.yaml.1
89c89
<       storageClassName: gp2
---
>       storageClassName: fast
92a93,100
> ---
> kind: StorageClass
> apiVersion: storage.k8s.io/v1
> metadata:
>   name: fast
> provisioner: k8s.io/minikube-hostpath
> parameters:
>   type: pd-ssd
```

再実行。

```bash
$kubectl apply -f cassandra-statefulset.yaml
```

む。。。同じエラー。。。
どうやら先程作成に失敗した pvc が残っているとこれを利用するのか同じエラーになる模様。
以下によって削除。

```bash
$ kubectl delete pvc cassandra-data-cassandra-0
```

その後、再度 apply したら成功。

```bash
# 要求した3つの Pod が起動している
$kubectl get statefulset
NAME        DESIRED   CURRENT   AGE
cassandra   3         3         4m

# 3つの Pod が作成されている
$ kubectl get pods -l="app=cassandra"
NAME          READY     STATUS    RESTARTS   AGE
cassandra-0   1/1       Running   0          5m
cassandra-1   1/1       Running   0          4m
cassandra-2   1/1       Running   0          3m


# pvc も3つ起動
$ kubectl get pvc
NAME                         STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
cassandra-data-cassandra-0   Bound     pvc-9c0a9803-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            6m
cassandra-data-cassandra-1   Bound     pvc-bd510f3a-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            5m
cassandra-data-cassandra-2   Bound     pvc-ee1121f4-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            4m

```

## Modifying the Cassandra StatefulSet

作成した StatefulSet を変更する。
replicas　フィールドを「3」から「4」にする。

```bash
# edit コマンドで編集.通常であればマニュフェストファイルを変更する
$kubectl edit statefulset cassandra
statefulset.apps "cassandra" edited

# 変更によって Pod 数が 3 から 4 になった
$ kubectl get statefulset
NAME        DESIRED   CURRENT   AGE
cassandra   4         4         12m

# 新しい Pod cassandra-3 が出来た
$ kubectl get pods -l="app=cassandra"
NAME          READY     STATUS    RESTARTS   AGE
cassandra-0   1/1       Running   0          12m
cassandra-1   1/1       Running   0          11m
cassandra-2   1/1       Running   0          10m
cassandra-3   0/1       Running   0          43s

```

上記のように StatefulSet では Pod 名は「(StatefulSet名)-N」という形式になり、追加すると N+1 の Pod 名が作成される。

試しにスケールインしてみる。

```bash
# replica 数を 4 から 3 にする
$$kubectl edit statefulset cassandra
statefulset.apps "cassandra" edited

# 一番新しい Pod が削除される
$ kubectl get pods -l="app=cassandra"
NAME          READY     STATUS    RESTARTS   AGE
cassandra-0   1/1       Running   0          16m
cassandra-1   1/1       Running   0          15m
cassandra-2   1/1       Running   0          14m

```

なお、上記場合でも pvc は削除されていないので必要なければ削除する。

```bash
$kubectl get pvc
NAME                         STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
cassandra-data-cassandra-0   Bound     pvc-9c0a9803-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            22m
cassandra-data-cassandra-1   Bound     pvc-bd510f3a-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            21m
cassandra-data-cassandra-2   Bound     pvc-ee1121f4-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            19m
cassandra-data-cassandra-3   Bound     pvc-49d749e9-0966-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            10m

$kubectl delete pvc cassandra-data-cassandra-3
persistentvolumeclaim "cassandra-data-cassandra-3" deleted
```



# Pod が死んだ時の挙動を確認する

WorkerNode が死んだときなど予期せぬタイミングで Pod が死んだ時の挙動を確認する。
検証のため、予め削除予定の Pod でマウントしている Volume にテスト用ファイルを作成し、その後 Pod を削除してみる。

```bash
# volume 配下にテストファイルを作成
$kubectl exec -it cassandra-0 -- touch /cassandra_data/test.txt

# 存在確認
$ kubectl exec -it cassandra-0 -- ls /cassandra_data/
commitlog  data  hints  lost+found  saved_caches  test.txt

# Pod を明示的に削除	
$kubectl delete pod cassandra-0
pod "cassandra-0" deleted

# すぐに同じ名前で Pod が作れる
$kubectl get pods -l="app=cassandra"
NAME          READY     STATUS              RESTARTS   AGE
cassandra-0   0/1       ContainerCreating   0          4s
cassandra-1   1/1       Running             0          29m
cassandra-2   1/1       Running             0          27m

# 同じ Volume を使っているので先程作成した test.txt が存在する
$kubectl exec -it cassandra-0 -- ls /cassandra_data/
commitlog  data  hints  lost+found  saved_caches  test.txt

# pvc は同じ
$ kubectl describe pod cassandra-0 |grep Claim
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  cassandra-data-cassandra-0

# pvc は再作成などされておらず、同じ Volume がマウントされている
$kubectl get pvc
NAME                         STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
cassandra-data-cassandra-0   Bound     pvc-9c0a9803-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            33m
cassandra-data-cassandra-1   Bound     pvc-bd510f3a-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            32m
cassandra-data-cassandra-2   Bound     pvc-ee1121f4-0964-11e9-b090-0ae6cc179478   1Gi        RWO            gp2            31m

```