KubernetesにRookを導入して、Block Storageを試してみた

# はじめに

:::note alert
この記事は完全に時代遅れです。
filestoreを使用した古いrook/cephはアップグレードせず、HDDを加えてbluestoreを有効にした最新版を新規に導入することをお勧めします。
:::

kubernetes環境にCeph環境を構築しようとして、Rookを試しています。

基本的な導入手順は、下記のドキュメントにありますが、バージョンが新しくなって、ほんの少し変更が入っているのでメモを残しておきます。

> Rookのv1.x以降では、各Kubernetesノードに専用のHDD/SSDが搭載されている事を前提としています。テスト用に引き続きdirectories: [path: /var/lib/rook]を指定することはできますが、cluster.yamlではuseAllDevices: trueがデフォルトの設定となっていて、未使用のブロックデバイス(e.g. /dev/sdb)の存在が期待されています。新しいバージョンのRookを利用する際には注意が必要です。可能であればFlex Volumeの使用は避けることをお勧めします。

## 参考資料
* [Cloud NativeなストレージRookの検証@ysakashita](https://qiita.com/ysakashita/items/f27c0948b4e339c5d1e0)
* [Official Ceph Storage Quickstart for v0.8](https://rook.github.io/docs/rook/v0.8/ceph-quickstart.html)

# 環境

* Kubernetes v1.12.1 (Xeon e3-1220v2, 24GB Memory, Ubuntu 16.04.5) x4台
* Rook tag:v0.8.3 (git clone https://github.com/rook/rook)

ノードはBaremetal(Ubuntu16.04.5)上にkubesprayで構築しています。

実行前のNamespaceの状況は次のとおりです。

```bash
$ kubectl get ns
NAME             STATUS    AGE
default          Active    181d
ingress-nginx    Active    11d
istio-system     Active    13h
kube-public      Active    181d
kube-system      Active    181d
metallb-system   Active    179d
```

## 【メモ】転送速度について

近い内に全体を最新のv1.6.xに更新する予定です。

> Rook/Ceph v1.6の古いバージョンには致命的な欠陥があります。
必ず最新のv1.7やv1.6.8以降を利用するようにしてください。

その前に2つのCeph filesystem間でデータを転送したので環境は以下のとおりです。

* Kubernetes: v1.16.9 (TX1310m3 x4, Xeon E3-1225v6, 4TB HDD x2 RAID1) 
* Kubernetes: v1.19.9 (TX1320m4 x5, Xeon E-2234, 4TB HDD + 500GB SSD)
* Local Network (K8sクラスター内部、両方とも): 10Gbps X520-DA1 + DAC + CRS309-1G-8S+
* Backend Network (K8sクラスター間接続): 1Gbps RJ45ケーブル + Switch
* Rook/Ceph v1.0.6 → v1.5.5

ネットワークは単一の192.168.1.0/24の内部で構成しています。

1ファイル 4KB未満のファイルを中心に構成されているFilesystem上にある約3GB(実転送データは約2.5GB)のデータ(ファイル数 2255364)を、片方のクラスターからもう片方(pod-to-pod)にrsyncで転送した結果は以下のようになりました。

```text
sent 2,491,171,331 bytes  received 39,713,035 bytes  136,763.90 bytes/sec
total size is 2,945,556,696  speedup is 1.16
```

# 準備

kubectlコマンドが実行できるノードで、rookのリポジトリをgit cloneで取得します。

```bash
$ git clone https://github.com/rook/rook
$ cd rook
$ git checkout refs/tags/v0.8.3 -b v0.8.3
```

# Deployment

基本的には公式のドキュメントに従います。
公式ドキュメントは普通にアクセスすると、masterブランチが表示されるので、左下のメニューでv0.8を選択します。

先ほどの手順に続いて、rookディレクトリから、次のコマンドを実行することで、必要なサービスがデプロイされます。

minikubeを利用している場合は、cluster.yamlの```dataDirHostPath: /var/lib/rook```を修正する必要があります。

```
$ cd cluster/examples/kubernetes/ceph
$ kubectl create -f operator.yaml
$ kubectl create -f cluster.yaml
```
*[2018/11/20追記] ここでFLEXVOLUME_DIR_PATHを変更せずにoperator.yamlを実行すると動かない可能性があります。
修正可能ですが、2度手間を避けたい場合には、後半のFLEXVOLUME_DIR_PATHについてのセクションを参照してください。*

ここまで進めると、namespaceは次のように変化しました。

```
$ kubectl get ns
NAME               STATUS    AGE
...
rook-ceph          Active    1m
rook-ceph-system   Active    1m
```

# StorageClassでの利用

公式ドキュメントでは[Block Storage](https://rook.github.io/docs/rook/v0.8/block.html)の項目にあるStorageClassの操作を試してみます。


前の操作に引き続き、 cluster/examples/kubernetes/ceph がカレントディレクトリである事を想定しています。

公式ドキュメントにあるように、storageclass.yaml ファイルを適用し、StorageClassの状態を確認します。

```bash
$ kubectl apply -f storageclass.yaml
$ kubectl -n rook-ceph get storageclass
NAME              PROVISIONER          AGE
rook-ceph-block   ceph.rook.io/block   1m
```

関連する定義を確認するため、```-o yaml```をつけて実行しました。```$ kubectl -n rook-ceph get storageclass -o yaml```

```bash
- apiVersion: storage.k8s.io/v1
  kind: StorageClass
  metadata:
    ...
    name: rook-ceph-block
    namespace: ""
    ...
```

namespaceが空なので実際には、どのnamespaceを指定してもstorageclassの定義を参照できることになります。

```bash
$ kubectl -n default get storageclass
NAME              PROVISIONER          AGE
rook-ceph-block   ceph.rook.io/block   1h

$ kubectl -n kube-system get storageclass
NAME              PROVISIONER          AGE
rook-ceph-block   ceph.rook.io/block   1h
```

そのため、システム全体から利用することができます。

## StatefulSetを利用した事例について

ROOKの公式ドキュメントに含まれているwordpressのサンプルは、オリジナルがKubernetes公式ドキュメントに掲載されている([Example: Deploying WordPress and MySQL with Persistent Volumes](https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/))だと思われます。

これ自体はあまりおもしろくないので、以前、書いた [Kubernetes公式のExample: CassandraをPersistentVolumeで構成する](https://qiita.com/YasuhiroABE/items/1c1d8cf54a43afd4a14b) を、オリジナルに近い形に戻すことにしました。

このCassandraの例では volumeClaimTemplates を使用していて、StorageClassの定義が含まれています。

Service定義は公式ドキュメントのとおりで問題ありませんが、namespaceとして、*cassandra*を作成しています。

```bash
$ kubectl create ns cassandra
$ alias kubectl='kubectl -n cassandra'
$ kubectl create -f https://k8s.io/examples/application/cassandra/cassandra-service.yaml
```

StatefulSetは公式ドキュメントにあるcassandra-statefulset.yamlと比較すると次のようになります。

```diff
--- cassandra-statefulset.yaml.orig  2018-11-05 17:17:44.057436173 +0900
+++ cassandra-statefulset.yaml       2018-11-05 17:22:58.843780317 +0900
@@ -53,7 +53,7 @@
           - name: HEAP_NEWSIZE
             value: 100M
           - name: CASSANDRA_SEEDS
-            value: "cassandra-0.cassandra.default.svc.cluster.local"
+            value: "cassandra-0.cassandra.cassandra.svc.cluster.local"
           - name: CASSANDRA_CLUSTER_NAME
             value: "K8Demo"
           - name: CASSANDRA_DC
@@ -86,15 +86,7 @@
       name: cassandra-data
     spec:
       accessModes: [ "ReadWriteOnce" ]
-      storageClassName: fast
+      storageClassName: rook-ceph-block
       resources:
         requests:
           storage: 1Gi
----
-kind: StorageClass
-apiVersion: storage.k8s.io/v1
-metadata:
-  name: fast
-provisioner: k8s.io/minikube-hostpath
-parameters:
-  type: pd-ssd

```

namespaceを指定したので、"default"から"cassandra"に変更しています。
この前のところで、aliasでkubectlに"-n cassandra"を追加しているので、コマンドラインではnamespaceを意識することはありません。

また、StorageClassの部分をバッサリ削除して、storageClassNameの指定を先ほど確認したStorageClassの metadata.name の、*rook-ceph-block* に変更しています。

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

ここまで実行しても、Podが起動しなくて困ってしまいました。

```bash
$ kubectl describe pod cassandra-0
...
Events:
  Type     Reason       Age                From               Message
  ----     ------       ----               ----               -------
  Normal   Scheduled    24m                default-scheduler  Successfully assigned cassandra/cassandra-0 to node6
  Warning  FailedMount  2m (x10 over 22m)  kubelet, node6     Unable to mount volumes for pod "cassandra-0_cassandra(0ad97640-e0d4-11e8-8510-000db93312a4)": timeout expired waiting for volumes to attach or mount for pod "cassandra"/"cassandra-0". list of unattached volumes=[cassandra-data default-token-hlb95]
```

ここで、サンプルのwordpress.yamlなども実行してみましたが、PVもPVCも問題なく作成されているようにみえるものの、Podからマウントできないようにみえます。

Podが稼動しているnode6の/var/log/syslogを確認してみます。

```text
Nov  5 18:57:28 node6 kubelet[1264]: E1105 18:57:28.898651    1264 desired_state_of_world_populator.go:311] Failed to add volume "cassandra-data" (specName: "pvc-15227c15-e0df-11e8-8510-000db93312a4") for pod "1527d22f-e0df-11e8-8510-000db93312a4" to desiredStateOfWorld. err=failed to get Plugin from volumeSpec for volume "pvc-15227c15-e0df-11e8-8510-000db93312a4" err=no volume plugin matched
```

# FLEXVOLUME_DIR_PATH 関連のトラブル

似ている事例としては、次の事例が該当すると思われました。

* https://github.com/rook/rook/issues/1888

ここからリンクされている[公式ドキュメント](https://rook.io/docs/rook/v0.8/flexvolume.html)によれば、理由として考えられるものは次の2点です。

1. FlexVolumeの設定
2. 指定したFlexVolumeがkubeletに指定されている

最初のFlexVolumeの設定については、デフォルトの /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ にディレクトリが作成されている事を確認しました。

```bash
$ ls -l /usr/libexec/kubernetes/kubelet-plugins/volume/exec/
total 16
drwxr-xr-x 2 root root 4096 Nov  6 00:10 ceph.rook.io~rook
drwxr-xr-x 2 root root 4096 Nov  6 00:10 ceph.rook.io~rook-ceph-system
drwxr-xr-x 2 root root 4096 Nov  6 00:10 rook.io~rook
drwxr-xr-x 2 root root 4096 Nov  6 00:10 rook.io~rook-ceph-system
```

この状態であれば、デフォルト設定でうまく動くかと思ったのですが、次のkubeletの設定について調べてみると、次のように /var/lib/kubelet/volume-plugins と、違う場所がポイントされています。

```bash
$ ps aux|grep volume-plugin-dir
root      1179 11.9  1.7 1848808 142048 ?      Ssl  Nov05  41:51 /usr/local/bin/kubelet    ...  --volume-plugin-dir=/var/lib/kubelet/volume-plugins
```

このため、cluster/examples/kubernetes/ceph の operator.yaml を編集して、FLEXVOLUME_DIR_PATHを設定しています。

```bash
$ cd cluster/examples/kubernetes/ceph
```

ここで、operator.yamlを編集してから、```$ git diff operator.yaml```を実行した結果は次のようになりました。

```diff
--- a/cluster/examples/kubernetes/ceph/operator.yaml
+++ b/cluster/examples/kubernetes/ceph/operator.yaml
@@ -308,8 +308,8 @@ spec:
         # - name: AGENT_TOLERATION_KEY
         #  value: "<KeyOfTheTaintToTolerate>"
         # Set the path where the Rook agent can find the flex volumes
-        # - name: FLEXVOLUME_DIR_PATH
-        #  value: "<PathToFlexVolumes>"
+        - name: FLEXVOLUME_DIR_PATH
+          value: "/var/lib/kubelet/volume-plugins/"
         # Rook Discover toleration. Will tolerate all taints with all keys.
         # Choose between NoSchedule, PreferNoSchedule and NoExecute:
         # - name: DISCOVER_TOLERATION
```

この operator.yaml を再び適用して、しばらくするとPodがリスタートされていきます。

```bash
$ kubectl apply -f operator.yaml
```

# 稼動の確認

ここまでの作業で無事にrook-cephが稼動するようになりました。

```bash
$ kubectl get all
NAME              READY     STATUS    RESTARTS   AGE
pod/cassandra-0   1/1       Running   0          6m
pod/cassandra-1   1/1       Running   0          4m
pod/cassandra-2   1/1       Running   0          1m

NAME                TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/cassandra   ClusterIP   None         <none>        9042/TCP   7h

NAME                         DESIRED   CURRENT   AGE
statefulset.apps/cassandra   3         3         6m
```

## 外部へのサービスの公開

ここまできて改めてtype: LoadBalancerなServiceを追加し、外部からcassandraにアクセスできるようにしました。

```bash
$ cat 03.cassandra-service.yaml 
apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandracl
spec:
  ports:
  - port: 9042
  selector:
    app: cassandra
  type: LoadBalancer

$ kubectl apply -f 03.cassandra-service.yaml 
service/cassandracl created
```

ここまでで、外部からアクセス可能なCassandraクラスタが稼動しました。

```bash
$ kubectl get svc
NAME          TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
cassandra     ClusterIP      None           <none>        9042/TCP         24m
cassandracl   LoadBalancer   10.233.32.79   192.168.100.156   9042:30653/TCP   23m
```

# 突然動かなくなる

この後に電源停止を想定して、ノード全体をリスタートしたところ、うまく動かなくなりました。
状態を確認すると、次のようになっています。

```bash
$ kubectl describe pod/cassandra-0
...
Events:
  Type     Reason       Age              From               Message
  ----     ------       ----             ----               -------
  Normal   Scheduled    5m               default-scheduler  Successfully assigned cassandra/cassandra-0 to node3
  Warning  FailedMount  1m (x2 over 3m)  kubelet, node3     Unable to mount volumes for pod "cassandra-0_cassandra(243ea4ec-e188-11e8-8a8f-000db93312a4)": timeout expired waiting for volumes to attach or mount for pod "cassandra"/"cassandra-0". list of unmounted volumes=[cassandra-data]. list of unattached volumes=[cassandra-data default-token-kl6r4]
```

Podのログをみると起動しているのに、cassandra-0のIPがlookupできていません。

当初はkube-dnsを疑っていたのですが、StatefulSetを利用する時には、ClusterIP: NoneなService(svc)定義がないと、cassandra-0のIPが引けなくなる事に気がつきました。

そのためsvcは"cassandra"と"cassandracl"の2つを定義しています。

この結果、kube-dns(10.233.0.3)にクラスター名を問合せると、配下のPODのAレコード(IP)がRoundRobinで返ってきます。

```bash
$ nslookup cassandra.cassandra.svc.cluster.local 10.233.0.3
Server:         10.233.0.3
Address:        10.233.0.3#53

Name:   cassandra.cassandra.svc.cluster.local
Address: 10.233.100.160
Name:   cassandra.cassandra.svc.cluster.local
Address: 10.233.71.56
Name:   cassandra.cassandra.svc.cluster.local
Address: 10.233.74.92
```

当初、ClusterIP: Noneなsvc定義を削除してから、type: LoadBalancerなsvcを再定義していたので、再起動のタイミングで問題が判明したのだと思われます。

## 次のPVCが作成できない問題

次にStorageClassを利用してrook-cephから作成することができなくなりました。

```yaml
$ kubectl -n cassandra describe pvc myclaim
Name:          myclaim
Namespace:     cassandra
StorageClass:  rook-ceph-block
Status:        Pending
Volume:        
Labels:        <none>
Annotations:   control-plane.alpha.kubernetes.io/leader={"holderIdentity":"dd36afbf-e457-11e8-8335-1a7fdbbc44fe","leaseDurationSeconds":15,"acquireTime":"2018-11-20T05:03:14Z","renewTime":"2018-11-20T05:08:29Z","lea...
               kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"myclaim","namespace":"cassandra"},"spec":{"accessModes":["ReadWr...
               volume.beta.kubernetes.io/storage-provisioner=ceph.rook.io/block
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      
Access Modes:  
Events:
  Type     Reason              Age              From                                                                                         Message
  ----     ------              ----             ----                                                                                         -------
  Normal   Provisioning        1m (x9 over 5m)  ceph.rook.io/block rook-ceph-operator-6cc45dfb48-kpkj6 dd36afbf-e457-11e8-8335-1a7fdbbc44fe  External provisioner is provisioning volume for claim "cassandra/myclaim"
  Warning  ProvisioningFailed  1m (x9 over 5m)  ceph.rook.io/block rook-ceph-operator-6cc45dfb48-kpkj6 dd36afbf-e457-11e8-8335-1a7fdbbc44fe  Failed to provision volume with StorageClass "rook-ceph-block": Failed to create rook block image replicapool/pvc-958a9ee5-ec81-11e8-849d-000db9331290: failed to create image pvc-958a9ee5-ec81-11e8-849d-000db9331290 in pool replicapool of size 5368709120: Failed to complete '': exit status 2. rbd: error opening pool 'replicapool': (2) No such file or directory
. output:
  Normal  ExternalProvisioning  16s (x78 over 5m)  persistentvolume-controller  waiting for a volume to be created, either by external provisioner "ceph.rook.io/block" or manually created by system administrator
```

toolbox.yamlを適応して、cephコマンドからpoolの状態を確認すると、replicapoolの存在が確認できない事が分かります。

```
$ kubectl -n rook-ceph exec -it rook-ceph-tools -- ceph osd lspools
$
```

poolの定義をみると、replicatedとerasureCodedの両方を指定していたので、[構成例](https://github.com/rook/rook/blob/master/Documentation/ceph-pool-crd.md)にもないのでシンプルにreplicatedだけにしました。

```yaml
$ kubectl -n rook-ceph get pool replicapool -o yaml
apiVersion: ceph.rook.io/v1beta1
kind: Pool
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"ceph.rook.io/v1beta1","kind":"Pool","metadata":{"annotations":{},"name":"replicapool","namespace":"rook-ceph"},"spec":{"erasureCoded":{"codingChunks":1,"dataChunks":2},"replicated":{"size":3}}}
  creationTimestamp: 2018-11-20T05:02:46Z
  generation: 1
  name: replicapool
  namespace: rook-ceph
  resourceVersion: "32423166"
  selfLink: /apis/ceph.rook.io/v1beta1/namespaces/rook-ceph/pools/replicapool
  uid: 8539e6e1-ec81-11e8-849d-000db9331290
spec:
  erasureCoded:
    codingChunks: 1
    dataChunks: 2
  replicated:
    size: 3
```

poolを削除してから、spec.erasureCodedを削除し、spec.replicatedだけを残したYAMLファイルを再度apply -fで適用して無事に動きました。

これも"apply -f"しただけでは即時に反映されないため、以前にstorageclass.yamlを変更したタイミングから少し遅れてSCを作り直したタイミングで発覚したものと思われます。

# 【閑話休題】 トラブルシューティング

## DashBoard

トラブルシューティングのために、Dashboard ServiceのタイプをClusterIPから、LoadBalancerに変更しました。

```bash
$ kubectl -n rook-ceph edit svc rook-ceph-mgr-dashboard
$ kubectl -n rook-ceph get svc rook-ceph-mgr-dashboard
NAME                      TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
rook-ceph-mgr-dashboard   LoadBalancer   10.233.49.37   192.168.100.110   7000:32606/TCP   6h
```

ブラウザからDashboardを確認しましたが、あまりおもしろい結果は得られませんでした。
Dashboardは見栄えはともかく、あまりお勧めできません。

# ToolBox

cephコマンドや、rdbコマンドを実行する時には、次のようにします。

あらかじめ toolbox.yaml を kubectl apply -f していることが前提です。

```bash
$ kubectl -n rook-ceph exec -it rook-ceph-tools bash
..# ceph osd status
# ceph osd status
+----+-------------------------------------+-------+-------+--------+---------+--------+---------+-----------+
| id |                 host                |  used | avail | wr ops | wr data | rd ops | rd data |   state   |
+----+-------------------------------------+-------+-------+--------+---------+--------+---------+-----------+
| 0  | rook-ceph-osd-id-0-6b577745cd-2dlcf | 35.4G |  421G |    0   |     0   |    0   |     0   | exists,up |
| 1  | rook-ceph-osd-id-1-56c8cb7449-wpmk9 | 21.0G |  206G |    0   |     0   |    0   |     0   | exists,up |
| 2  |  rook-ceph-osd-id-2-b8c5d98d5-cmkwh | 20.8G |  206G |    0   |     0   |    1   |    16   | exists,up |
| 3  |  rook-ceph-osd-id-3-894fc4d55-7ttr6 | 18.0G | 89.9G |    0   |     0   |    0   |     0   | exists,up |
| 4  | rook-ceph-osd-id-4-86d4557d64-85vvs | 17.5G | 90.4G |    0   |     0   |    1   |    90   | exists,up |
| 5  |  rook-ceph-osd-id-5-cbfdb6c8b-ws966 | 16.7G | 37.9G |    0   |     0   |    0   |     0   | exists,up |
| 6  |  rook-ceph-osd-id-6-ff948fbbd-dzvfd | 21.0G | 92.2G |    0   |     0   |    0   |     0   | exists,up |
+----+-------------------------------------+-------+-------+--------+---------+--------+---------+-----------+
[root@rook-ceph-tools /]# ceph df
GLOBAL:
    SIZE      AVAIL     RAW USED     %RAW USED
    1295G     1145G         150G         11.63
POOLS:
    NAME              ID     USED      %USED     MAX AVAIL     OBJECTS
    replicapool       1       134M      0.02          834G          48
    myfs-metadata     2      15962         0          834G          21
    myfs-data0        3         98         0          834G           2
```

toolbox.yamlを利用して、"ceph osd status"をalias設定しておくなどすると便利だと思われます。

```alias show_ceph_osd_status="kubectl -n rook-ceph exec -it rook-ceph-tools -- ceph osd status"```

以上