Featured image of post Mounting CephFS in Kubernetes pods

Mounting CephFS in Kubernetes pods

In a previous post, I set up a Ceph cluster as a storage backend and default storage class for my Kubernetes cluster. It works great for workloads such as databases and object storage, but not ideal for:

  1. shared storage for multiple applications
  2. accessing files from outside the cluster

Using CephFS as a shared filesystem solves both of these problems. The trade-off is that it is not as performant as using Ceph RBD, so it will be only for certain situations where I absolutely need shared storage. The default storage class is still RBD.

Overview

With a Ceph cluster, CephFS, and a Kubernetes cluster running, in this post, I will set up a nginx pod that serves files from a CephFS volume.

As an example, I am serving my resume here: https://share.junyi.me/resume_en.pdf for public access, which is also linked in my portfolio website. The nice thing about this setup is that each time I update my resume, I simply need to put the new one in the CephFS volume, without needing to rebuilt/restart any application.

I also set up a NFS server pod to expose that CephFS volume to my home network so that it’s more convenient to access files from my devices.

Install CephFS CSI

CephFS CSI can be installed with a helm chart. Following is the configuration I used:

 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
# values.yaml

csiConfig: 
  - clusterID: "<cluster_id>"
    monitors:
      - <node_1_ip>:6789
      - <node_2_ip>:6789
      - <node_3_ip>:6789
      # ...

storageClass:
  create: true
  name: csi-cephfs-sc
  clusterID: "<cluster_id>"
  fsName: "<cephfs_name>"
  provisionerSecret: csi-cephfs-secret
  controllerExpandSecret: csi-cephfs-secret
  nodeStageSecret: csi-cephfs-secret
  reclaimPolicy: Retain
  allowVolumeExpansion: true

secret:
  create: true
  name: csi-cephfs-secret
  annotations: {}
  adminID: <admin_id>
  adminKey: <admin_key>
  userID: <user_id>
  userKey: <user_key>

Cluster ID of ceph cluster can be retrieved by:

1
ceph status

Admin key can be retrieved by:

1
ceph auth get-key client.admin

For the user id and key, you can create a new user with the following command:

1
ceph auth get-or-create-key client.<user_id> mon 'allow r' osd 'allow rwx pool=<pool_name>'

Install the helm chart by:

1
2
3
helm repo add ceph-csi https://ceph.github.io/csi-charts
helm repo update
helm install csi-cephfs ceph-csi/csi-cephfs -f values.yaml

For more information about ceph-csi-cephfs and helm values, see the official GitHub repo.

After installing, confirm that the storage class is created:

1
kubectl get sc

nginx pod

This is the nginx pod that I’m using to serve files from the CephFS volume to the internet:

  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
apiVersion: v1
kind: PersistentVolume
metadata:
  name: public
  namespace: network
spec:
  storageClassName: csi-cephfs-sc
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 1Gi
  csi:
    driver: cephfs.csi.ceph.com
    nodeStageSecretRef:
      name: csi-cephfs-secret
      namespace: ceph-csi-cephfs
    volumeAttributes:
      "fsName": "<cephfs_name>"
      "clusterID": "<cluster_id>"
      "staticVolume": "true"
      "rootPath": /public # only this subdirectory will be mounted, and thus served
    volumeHandle: public-handle
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: public
  namespace: network
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: "csi-cephfs-sc"
  volumeMode: Filesystem
  volumeName: public

---

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-public-cm
  namespace: network
data:
  nginx.conf: |
    server {
      listen       80;
      listen  [::]:80;
      server_name  _;

      root /mnt/data;

      add_header 'Access-Control-Allow-Origin' '*.junyi.me' always;
      add_header 'Access-Control-Allow-Methods' 'GET' always;
      limit_except GET { deny  all; }

      location = /healthz {
        access_log off;
        return 200 'OK';
        add_header Content-Type text/plain;
      }
    }    

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-public
  namespace: network
  labels:
    app: nginx-public
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-public
  template:
    metadata:
      labels:
        app: nginx-public
    spec:
      containers:
      - name: nginx-public
        image: nginx:bookworm
        ports:
          - containerPort: 80
        volumeMounts:
        - mountPath: /mnt/data
          name: data
        - mountPath: /etc/nginx/conf.d/default.conf
          name: nginx-config
          subPath: nginx.conf
        livenessProbe:
          httpGet:
            path: /healthz
            port: 80
            httpHeaders:
          initialDelaySeconds: 10
          periodSeconds: 30
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: public
      - name: nginx-config
        configMap:
          name: nginx-public-cm

---

apiVersion: v1
kind: Service
metadata:
  name: nginx-public
  namespace: network
spec:
  selector:
    app: nginx-public
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Apply the above along with an ingress pointing to the service to expose the nginx pod to the internet.

NFS pod

This is the NFS server pod that I’m using to provide access to my home network:

  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
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  storageClassName: csi-cephfs-sc
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 1Gi # doesn't matter
  csi:
    driver: cephfs.csi.ceph.com
    nodeStageSecretRef:
      name: csi-cephfs-secret
      namespace: ceph-csi-cephfs
    volumeAttributes:
      "fsName": "<cephfs_name>"
      "clusterID": "<cluster_id>"
      "staticVolume": "true"
      "rootPath": / # entire cephfs volume will be mounted
    volumeHandle: nfs-pvc
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
  namespace: admin
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: "csi-cephfs-sc"
  volumeMode: Filesystem
  volumeName: nfs-pv

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-server
  namespace: admin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-server
  template:
    metadata:
      labels:
        app: nfs-server
    spec:
      containers:
      - name: nfs-server
        image: itsthenetwork/nfs-server-alpine
        ports:
          - name: nfs
            containerPort: 2049
          - name: rpcbind
            containerPort: 111
        securityContext:
          privileged: true
        env:
          - name: SHARED_DIRECTORY
            value: "/cephfs"
        volumeMounts:
          - name: cephfs-storage
            mountPath: "/cephfs"
      volumes:
      - name: cephfs-storage
        persistentVolumeClaim:
          claimName: nfs-pvc

---

apiVersion: v1
kind: Service
metadata:
  name: nfs-service
  namespace: admin
spec:
  selector:
    app: nfs-server
  ports:
    - name: nfs
      protocol: TCP
      port: 2049
      targetPort: 2049
    - name: rpcbind
      protocol: TCP
      port: 111
      targetPort: 111
  type: LoadBalancer
  loadBalancerIP: <load_balancer_ip>
Note

The volumeHandle in the PV spec can be anything, but must be unique across all PVs in the cluster.

Now the NFS share can be mounted like any other NFS share with the load balancer IP.

Conclusion

I’m happy that now I have a convenient way to serve static files from my CephFS volume, and also share some files across myself and my pods.

Currently I only have one CephFS configured, and everything inside is managed by subdirectories. So far I hadn’t had any problem with this setup, but I assume if it’s for a corporate environment, it would be better to have some kind of more fine-grained access control.

Built with Hugo
Theme Stack designed by Jimmy