Build a Kubernetes volume using local nodes disks

Posted by ZedTuX 0n R00t on February 4, 2019

Given you are building a CI/CD Kubernetes cluster and you need some Persistent Volume, using a cloud storage sounds too expensive especially when you have 150Gb of local disks across your nodes.

In my case, I’m using the DigitalOcean Kubernetes product with 3 nodes having 50Gb of disk each for $10 per nodes per months with a total of $30 per months.

In this article I’m exploring Rook which is a cloud storage orchestration. The first and most stable cloud storage driver is the Ceph one, so I’m using it here, but it shouldn’t be too different for the other available drivers.

So let’s see what we can get from this.

In this article I’m assuming you have a k8s cluster and the kubectl command is configured correctly to access your cluster.

Rook deployment

Rook operator

The rook operator is reponsible to discover disks and create the volumes.

The rook team is providing an Helm chart to install it so let’s follow the instructions from the documentation in order to install the stable version :

1
2
3
4
5
6
$ helm init
$ kubectl --namespace kube-system create sa tiller
$ kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
$ kubectl --namespace kube-system patch deploy/tiller-deploy -p '{"spec": {"template": {"spec": {"serviceAccountName": "tiller"}}}}'
$ helm repo add rook-stable https://charts.rook.io/stable
$ helm install --namespace rook-ceph-system rook-stable/rook-ceph

Now that the operator is deployed, we have to deploy rook cluster.

Rook cluster

The rook cluster deployement consist of creating a rook-ceph namespace, some ServiceAccount and Roles/RoleBindings and finally a CephCluster.

We will also enable the Ceph dashboard without the SSL/TLS so that we will be able to access it locally using the Kubernetes proxy.

First of all, have a look at the Ceph Docker tags in order to get the latest one and create the following rook-cluster.yaml file :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
apiVersion: v1
kind: Namespace
metadata:
  name: rook-ceph
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rook-ceph-osd
  namespace: rook-ceph
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rook-ceph-mgr
  namespace: rook-ceph
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-osd
  namespace: rook-ceph
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: [ "get", "list", "watch", "create", "update", "delete" ]
---
# Aspects of ceph-mgr that require access to the system namespace
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-mgr-system
  namespace: rook-ceph
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
---
# Aspects of ceph-mgr that operate within the cluster's namespace
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-mgr
  namespace: rook-ceph
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - services
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - batch
  resources:
  - jobs
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - delete
- apiGroups:
  - ceph.rook.io
  resources:
  - "*"
  verbs:
  - "*"
---
# Allow the operator to create resources in this cluster's namespace
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-cluster-mgmt
  namespace: rook-ceph
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: rook-ceph-cluster-mgmt
subjects:
- kind: ServiceAccount
  name: rook-ceph-system
  namespace: rook-ceph-system
---
# Allow the osd pods in this namespace to work with configmaps
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-osd
  namespace: rook-ceph
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rook-ceph-osd
subjects:
- kind: ServiceAccount
  name: rook-ceph-osd
  namespace: rook-ceph
---
# Allow the ceph mgr to access the cluster-specific resources necessary for the mgr modules
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-mgr
  namespace: rook-ceph
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rook-ceph-mgr
subjects:
- kind: ServiceAccount
  name: rook-ceph-mgr
  namespace: rook-ceph
---
# Allow the ceph mgr to access the rook system resources necessary for the mgr modules
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-mgr-system
  namespace: rook-ceph-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rook-ceph-mgr-system
subjects:
- kind: ServiceAccount
  name: rook-ceph-mgr
  namespace: rook-ceph
---
# Allow the ceph mgr to access cluster-wide resources necessary for the mgr modules
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rook-ceph-mgr-cluster
  namespace: rook-ceph
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: rook-ceph-mgr-cluster
subjects:
- kind: ServiceAccount
  name: rook-ceph-mgr
  namespace: rook-ceph
---
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    # For the latest ceph images, see https://hub.docker.com/r/ceph/ceph/tags
    image: ceph/ceph:v13.2.3-20190109
  dataDirHostPath: /var/lib/rook
  mon:
    count: 3
    allowMultiplePerNode: true
  dashboard:
    enabled: true
    port: 8080
    ssl: false
  storage:
    useAllNodes: true
    useAllDevices: false
    config:
      databaseSizeMB: "1024"
      journalSizeMB: "1024"

Deploy it :

1
$ kubectl apply -f rook-cluster.yaml

It will take some time to get the nodes discovery and all the rook pods up and running, so follow up using the kubectl get pods --namespace=rook-ceph command.

Your cluster is ready when you have a mon and an osd pod per nodes, and a mgr pod :

1
2
3
4
5
6
7
8
9
$ kubectl get pods --namespace=rook-ceph
NAME                                                 READY   STATUS      RESTARTS   AGE
rook-ceph-mgr-a-7cbd67c79b-lrhrk                     1/1     Running     0          2m42s
rook-ceph-mon-a-56579fbc78-qs52z                     1/1     Running     0          4m11s
rook-ceph-mon-b-7b4c8f48dd-vvk2h                     1/1     Running     0          3m42s
rook-ceph-mon-c-5688b75489-ng7l7                     1/1     Running     0          3m12s
rook-ceph-osd-0-5f4c54c69b-7dr27                     1/1     Running     0          89s
rook-ceph-osd-1-769f54d9f6-h699t                     1/1     Running     0          88s
rook-ceph-osd-2-868dfcdf69-gqbx8                     1/1     Running     0          89s

Ceph Dashboard

Ceph

Now you can access the Ceph dashboard :

  1. Start the Kubernetes proxy with kubectl proxy
  2. Open the following URL : http://localhost:8001/api/v1/namespaces/rook-ceph/services/rook-ceph-mgr-dashboard:https-dashboard/proxy

To get the login and password, use the following:

1
2
$ kubectl logs rook-ceph-mgr-a-7cbd67c79b-lrhrk --namespace=rook-ceph | grep "dashboard set-login-credentials"
2019-02-04 07:43:20.128 7f89f4286700  0 log_channel(audit) log [DBG] : from='client.14126 10.244.38.0:0/3232824830' entity='client.admin' cmd=[{"username": "admin", "prefix": "dashboard set-login-credentials", "password": "Yb2V1yjHmm", "target": ["mgr", ""], "format": "json"}]: dispatch

You can see here that the login is admin and password Yb2V1yjHmm.

Ceph-dashboard

Ceph-dashboard-hosts

Kubernetes StorageClass

Now we have to tell Kubernetes about the Rook StorageClass so that we will be able to create Volumes.

Deploy the StorageClass

Create a rook-storage-class.yaml file with the following :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  blockPool: replicapool
  # Specify the namespace of the rook cluster from which to create volumes.
  # If not specified, it will use `rook` as the default namespace of the cluster.
  # This is also the namespace where the cluster will be
  clusterNamespace: rook-ceph
  # Specify the filesystem type of the volume. If not specified, it will use `ext4`.
  # fstype: xfs
  # (Optional) Specify an existing Ceph user that will be used for mounting storage with this StorageClass.
  #mountUser: user1
  # (Optional) Specify an existing Kubernetes secret name containing just one key holding the Ceph user secret.
  # The secret must exist in each namespace(s) where the storage will be consumed.
  #mountSecret: ceph-user1-secret
reclaimPolicy: Retain

(I have commented the fstype attribute as DigitalOcean doesn’t support XFS)

And deploy it :

1
$ kubectl apply -f rook-storage-class.yaml

Now check the StorageClasses :

1
2
3
4
$ kubectl get storageclasses
NAME                         PROVISIONER                 AGE
do-block-storage (default)   dobs.csi.digitalocean.com   108m
rook-ceph-block              ceph.rook.io/block          3s

Also, from the Ceph Dashboard, the created Pool is visible :

ceph-pool

Defines as a default

The DigitalOcean block storage (made with their CSI Driver) is the default one, to avoid mistakes, we update the rook-ceph-block StorageClass as the default one as describe in the Kubernetes doc :

1
2
3
4
5
6
7
8
$ kubectl patch storageclass do-block-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

$ kubectl patch storageclass rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

$ kubectl get storageclasses
NAME                        PROVISIONER                 AGE
do-block-storage            dobs.csi.digitalocean.com   114m
rook-ceph-block (default)   ceph.rook.io/block          5m58s

Deploy a PersistentVolumeClaim

To allow a Pod to use a PersistentVolume, we have to create a PersistentVolumeClaim in the new file rook-persistent-volume-claim.yaml :

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pv-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: rook-ceph-block

And deploy it :

1
$ kubectl apply -f rook-persistent-volume-claim.yaml

And check it :

1
2
3
$ kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
redis-pv-claim   Bound    pvc-ce88c79d-2861-11e9-9714-6e4e5abdddf2   5Gi        RWO            rook-ceph-block   6s

Use the Rook volumes

Let’s deploy a pod requesting a volume. Create the test-pod-volume.yaml file with the following :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  containers:
  - name: redis
    image: redis:4.0.5-alpine
    resources:
      requests:
        cpu: "200m"
        memory: "64Mi"
    volumeMounts:
    - name: redis-pv
      mountPath: /data/redis
  volumes:
  - name: redis-pv
    persistentVolumeClaim:
      claimName: redis-pv-claim

(The redis container will keep it up-and-running so that we will be able to executed commands in the pod)

And deploy it :

1
$ kubectl apply -f test-pod-volume.yaml

Let’s check the PersistentVolumes and PersistentVolumeClaims :

1
2
3
4
5
6
7
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS      REASON   AGE
pvc-5c827a63-285e-11e9-9714-6e4e5abdddf2   5Gi        RWO            Retain           Bound    default/redis-pv-claim   rook-ceph-block            7m48s

$ kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
redis-pv-claim   Bound    pvc-5c827a63-285e-11e9-9714-6e4e5abdddf2   5Gi        RWO            rook-ceph-block   8m20s

To test that everything works fine we will attach to the pod, create a file, delete the pod and re-create it and check the file :

First create a file in the pod :

1
2
3
4
5
6
7
8
9
10
11
$ kubectl exec -it redis /bin/sh
/data # cd redis/
/data/redis # ls -al
total 24
drwxr-xr-x    3 redis    root          4096 Feb  4 09:22 .
drwxr-xr-x    3 redis    redis         4096 Feb  4 09:22 ..
drwx------    2 redis    root         16384 Feb  4 09:22 lost+found
/data/redis # echo "Please, don't delete me !" > testfile
/data/redis # cat testfile
Please, don't delete me !
/data/redis # %

Now let’s delete the pod and re-create it :

1
2
$ kubectl delete -f test-pod-volume.yaml
$ kubectl apply -f test-pod-volume.yaml

Finally let’s check our file :

1
2
3
4
5
6
7
8
9
10
$ kubectl exec -it redis /bin/sh
/data # cd redis/
/data/redis # ls -al
total 28
drwxr-xr-x    3 redis    root          4096 Feb  4 09:23 .
drwxr-xr-x    3 redis    redis         4096 Feb  4 09:25 ..
drwx------    2 redis    root         16384 Feb  4 09:22 lost+found
-rw-r--r--    1 redis    root            26 Feb  4 09:23 testfile
/data/redis # cat testfile
Please, don't delete me !

As you can see, the file has been persisted between the pod recreation. 🎉