GlusterFS

Published: Sep 20, 2022 by Isaac Johnson

GlusterFS is a free, scalable and open-source network filesystem we can use to keep paths synchronized across systems. Today we’ll setup GlusterFS on an on-prem 3 node cluster and then use it in a variety of ways.

In addition, we’ll piggyback on last week’s blog to tie this into GCP Cloud Buckets for backups. We’ll see how GlusterFS and GCSFuse can tie nicely together to provide a cohesive durable storage solution.

GCFuse for GCP Buckets

To mount the directory for GCP Cloud Storage, we can fix the local permissions we ignored in our last post.

First, I want to determine my user ID and group ID

builder@anna-MacBookAir:~$ id -u builder
1000
builder@anna-MacBookAir:~$ cat /etc/group | grep ^builder
builder:x:1000:

Next, I’ll create a dir for mounting

$ sudo mkdir /mnt/gcpbucket2

Then lastly, assuming we still have saved our sa-storage-key.json (see last blog entry for how to get them ).

Now we can mount it

builder@anna-MacBookAir:~$ sudo mount -t gcsfuse -o gid=1000,uid=1000,allow_other,implicit_dirs,rw,noauto,file_mode=777,
dir_mode=777,key_file=/home/builder/sa-storage-key.json myk3spvcstore /mnt/gcpbucket2
Calling gcsfuse with arguments: --key-file /home/builder/sa-storage-key.json -o rw --gid 1000 --uid 1000 -o allow_other --implicit-dirs --file-mode 777 --dir-mode 777 myk3spvcstore /mnt/gcpbucket2
2022/09/17 13:34:32.380108 Start gcsfuse/0.41.6 (Go version go1.18.4) for app "" using mount point: /mnt/gcpbucket2
2022/09/17 13:34:32.393651 Opening GCS connection...
2022/09/17 13:34:33.415344 Mounting file system "myk3spvcstore"...
2022/09/17 13:34:33.430344 File system has been successfully mounted.

And I can test

builder@anna-MacBookAir:~$ ls -ltra /mnt/gcpbucket2
total 1
-rwxrwxrwx 1 builder builder  0 Sep  9 07:20 testing
-rwxrwxrwx 1 builder builder 51 Sep  9 12:02 longhorn-disk.cfg
drwxrwxrwx 1 builder builder  0 Sep 17 13:34 replicas

We’ve mounted the PVC storage from before. Now I should create a bucket for glusterfs

$ gcloud alpha storage buckets create gs://mkk3sglusterfs
Creating gs://mkk3sglusterfs/...

I can also create with the Cloud Console

/content/images/2022/09/glusterfs-02.png

Which shows as

/content/images/2022/09/glusterfs-03.png

I’ll want to mount this locally to use with glusterfs

builder@anna-MacBookAir:~$ sudo mount -t gcsfuse -o gid=1000,uid=1000,allow_other,implicit_dirs,rw,noauto,file_mode=777,dir_mode=777,key_file=/home/builder/sa-storage-key.json myk3sglusterfs /mnt/glusterfs
Calling gcsfuse with arguments: --key-file /home/builder/sa-storage-key.json -o rw --gid 1000 --uid 1000 -o allow_other --implicit-dirs --file-mode 777 --dir-mode 777 myk3sglusterfs /mnt/glusterfs
2022/09/17 13:56:22.374727 Start gcsfuse/0.41.6 (Go version go1.18.4) for app "" using mount point: /mnt/glusterfs
2022/09/17 13:56:22.390939 Opening GCS connection...
2022/09/17 13:56:22.898022 Mounting file system "myk3sglusterfs"...
2022/09/17 13:56:22.902388 File system has been successfully mounted.

Verification

We can touch a file to see it get created in our GCP Storage Bucket

/content/images/2022/09/glusterfs-04.png

GlusterFS install

Now we can install GlusterFS

builder@anna-MacBookAir:~$ sudo apt install glusterfs-server
Preparing to unpack .../12-glusterfs-client_7.2-2build1_amd64.deb ...
Unpacking glusterfs-client (7.2-2build1) ...
Selecting previously unselected package glusterfs-server.
Preparing to unpack .../13-glusterfs-server_7.2-2build1_amd64.deb ...
Unpacking glusterfs-server (7.2-2build1) ...
Selecting previously unselected package ibverbs-providers:amd64.
Preparing to unpack .../14-ibverbs-providers_28.0-1ubuntu1_amd64.deb ...
Unpacking ibverbs-providers:amd64 (28.0-1ubuntu1) ...
Setting up libibverbs1:amd64 (28.0-1ubuntu1) ...
Setting up ibverbs-providers:amd64 (28.0-1ubuntu1) ...
Setting up attr (1:2.4.48-5) ...
Setting up libglusterfs0:amd64 (7.2-2build1) ...
Setting up xfsprogs (5.3.0-1ubuntu2) ...
update-initramfs: deferring update (trigger activated)
Setting up liburcu6:amd64 (0.11.1-2) ...
Setting up python3-prettytable (0.7.2-5) ...
Setting up libgfxdr0:amd64 (7.2-2build1) ...
Setting up librdmacm1:amd64 (28.0-1ubuntu1) ...
Setting up libgfrpc0:amd64 (7.2-2build1) ...
Setting up libgfchangelog0:amd64 (7.2-2build1) ...
Setting up libgfapi0:amd64 (7.2-2build1) ...
Setting up glusterfs-common (7.2-2build1) ...
Adding group `gluster' (GID 134) ...
Done.
/usr/lib/x86_64-linux-gnu/glusterfs/python/syncdaemon/syncdutils.py:705: SyntaxWarning: "is" with a literal. Did you mean "=="?
  if dirpath is "/":
Setting up glusterfs-client (7.2-2build1) ...
Setting up glusterfs-server (7.2-2build1) ...
Processing triggers for man-db (2.9.1-1) ...
Processing triggers for libc-bin (2.31-0ubuntu9.9) ...
Processing triggers for initramfs-tools (0.136ubuntu6.7) ...
update-initramfs: Generating /boot/initrd.img-5.15.0-46-generic

Then we can start the GlusterFS service

builder@anna-MacBookAir:~$ sudo service glusterd start
builder@anna-MacBookAir:~$ sudo service glusterd status
● glusterd.service - GlusterFS, a clustered file-system server
     Loaded: loaded (/lib/systemd/system/glusterd.service; disabled; vendor preset: enabled)
     Active: active (running) since Sat 2022-09-17 14:01:57 CDT; 30s ago
       Docs: man:glusterd(8)
    Process: 835492 ExecStart=/usr/sbin/glusterd -p /var/run/glusterd.pid --log-level $LOG_LEVEL $GLUSTERD_OPTIONS (cod>
   Main PID: 835494 (glusterd)
      Tasks: 9 (limit: 9331)
     Memory: 3.9M
     CGroup: /system.slice/glusterd.service
             └─835494 /usr/sbin/glusterd -p /var/run/glusterd.pid --log-level INFO

Sep 17 14:01:55 anna-MacBookAir systemd[1]: Starting GlusterFS, a clustered file-system server...
Sep 17 14:01:57 anna-MacBookAir systemd[1]: Started GlusterFS, a clustered file-system server.

we will want to enable the nodes to all talk to each other so we’ll add to iptables

builder@anna-MacBookAir:~$ sudo iptables -I INPUT -p all -s 192.168.1.159 -j ACCEPT
builder@anna-MacBookAir:~$ sudo iptables -I INPUT -p all -s 192.168.1.205 -j ACCEPT

Using Ansible

I realized; these steps would be prime to automate with Ansible.

Thus, I created a glusterfs playbook


- name: Install and Configure GlusterFS
  hosts: all

  tasks:
  - name: Set IPTables entry (if Firewall enabled)
    ansible.builtin.shell: |
      iptables -I INPUT -p all -s 192.168.1.205 -j ACCEPT
      iptables -I INPUT -p all -s 192.168.1.159 -j ACCEPT
      iptables -I INPUT -p all -s 192.168.1.81 -j ACCEPT
    become: true
    ignore_errors: True

  - name: Install glusterfs server
    ansible.builtin.shell: |
       apt update
       apt install -y glusterfs-server
    become: true

  - name: GlusterFS server - Start and check status
    ansible.builtin.shell: |
       service glusterd start
       sleep 5
       service glusterd status
    become: true


  - name: Create First Brick
    ansible.builtin.shell: |
       mkdir -p /mnt/glusterfs/brick1/gv0
    become: true

- name: Volume Create and Start on Primary
  hosts: AnnaMacbook

  tasks:
  - name: GlusterFS Peer Probe
    ansible.builtin.shell: |
       gluster peer probe 192.168.1.81
       gluster peer probe 192.168.1.159
       gluster peer probe 192.168.1.205
    become: true
    ignore_errors: True

  - name: GlusterFS Peer Status
    ansible.builtin.shell: |
       gluster peer status
    become: true

  - name: Create GV0 Volume
    ansible.builtin.shell: |
       gluster volume create gv0 replica 3 192.168.1.81:/mnt/glusterfs/brick1/gv0 192.168.1.159:/mnt/glusterfs/brick1/gv0 192.168.1.205:/mnt/glusterfs/brick1/gv0
    become: true

  - name: Start GV0 Volume
    ansible.builtin.shell: |
       gluster volume start gv0
       gluster volume info
    become: true

  - name: Check Status of Volume
    ansible.builtin.shell: |
       gluster volume info
    become: true

I then added as a new template to AWX

/content/images/2022/09/glusterfs-01.png

Debugging

In the end, using IP addresses just would not work.

builder@anna-MacBookAir:/mnt$ sudo gluster peer status
Number of Peers: 2

Hostname: 192.168.1.159
Uuid: bf5698c7-3bd4-4b5e-9951-1c7152ae34cf
State: Accepted peer request (Connected)

Hostname: 192.168.1.205
Uuid: ec7e5bd5-5ca9-496d-9ae9-66ef19df5c4c
State: Accepted peer request (Connected)

If I created and updated /etc/hosts files, then joined manually, I was good to go

isaac@isaac-MacBookPro:~$ sudo gluster peer status
Number of Peers: 2

Hostname: builder-MacBookPro2
Uuid: bf5698c7-3bd4-4b5e-9951-1c7152ae34cf
State: Peer in Cluster (Connected)
Other names:
builder-MacBookPro2

Hostname: anna-MacBookAir
Uuid: c21f126c-0bf9-408e-afc7-a9e1d91d6431
State: Peer in Cluster (Connected)

I tried a few variations, but the create command that worked:

builder@anna-MacBookAir:/mnt$ sudo gluster volume create gv0 replica 3 anna-MacBookAir:/mnt/glusterfs/brick1/gv0 isaac-MacBookPro:/mnt/glusterfs/brick1/gv0 builder-MacBookPro2:/mnt/glusterfs/brick1/gv0 force
volume create: gv0: success: please start the volume to access data

I could then start the volume

builder@anna-MacBookAir:/mnt$ sudo gluster volume start gv0
volume start: gv0: success

And get info, which showed the 3 replicas

builder@anna-MacBookAir:/mnt$ sudo gluster volume info

Volume Name: gv0
Type: Replicate
Volume ID: f7f5ff8d-d5e8-4e1b-ab2d-9af2a2ee8b91
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 3 = 3
Transport-type: tcp
Bricks:
Brick1: anna-MacBookAir:/mnt/glusterfs/brick1/gv0
Brick2: isaac-MacBookPro:/mnt/glusterfs/brick1/gv0
Brick3: builder-MacBookPro2:/mnt/glusterfs/brick1/gv0
Options Reconfigured:
transport.address-family: inet
storage.fips-mode-rchecksum: on
nfs.disable: on
performance.client-io-threads: off

Using

we can now mount it locally

builder@anna-MacBookAir:/mnt$ sudo mkdir /mnt/glusterfs2
builder@anna-MacBookAir:/mnt$ sudo mount -t glusterfs anna-MacBookAir:/gv0 /mnt/glusterfs2

Testing

Let’s touch 100 files in the mount

builder@anna-MacBookAir:/mnt$ for i in `seq -w 1 100`; do sudo touch /mnt/glusterfs2/test-$i; done

builder@anna-MacBookAir:/mnt$ sudo ls -ltra /mnt/glusterfs2 | tail -n10
-rw-r--r--  1 root root    0 Sep 18 17:00 test-092
-rw-r--r--  1 root root    0 Sep 18 17:00 test-093
-rw-r--r--  1 root root    0 Sep 18 17:00 test-094
-rw-r--r--  1 root root    0 Sep 18 17:00 test-095
-rw-r--r--  1 root root    0 Sep 18 17:00 test-096
-rw-r--r--  1 root root    0 Sep 18 17:00 test-097
-rw-r--r--  1 root root    0 Sep 18 17:00 test-098
-rw-r--r--  1 root root    0 Sep 18 17:00 test-099
drwxr-xr-x  3 root root 4096 Sep 18 17:00 .
-rw-r--r--  1 root root    0 Sep 18 17:00 test-100

I can see on each host, that the 100 files were replicated

builder@anna-MacBookAir:/mnt$ sudo ls -ltra /mnt/glusterfs/brick1/gv0/ | tail -n5
-rw-r--r--  2 root root    0 Sep 18 17:00 test-098
-rw-r--r--  2 root root    0 Sep 18 17:00 test-099
drw------- 86 root root 4096 Sep 18 17:00 .glusterfs
drwxr-xr-x  3 root root 4096 Sep 18 17:00 .
-rw-r--r--  2 root root    0 Sep 18 17:00 test-100
builder@anna-MacBookAir:/mnt$ sudo ls -ltra /mnt/glusterfs/brick1/gv0/ | wc -l
104

isaac@isaac-MacBookPro:~$ ls -ltra /mnt/glusterfs/brick1/gv0/ | tail -n10
-rw-r--r--  2 root root    0 Sep 18 17:00 test-093
-rw-r--r--  2 root root    0 Sep 18 17:00 test-094
-rw-r--r--  2 root root    0 Sep 18 17:00 test-095
-rw-r--r--  2 root root    0 Sep 18 17:00 test-096
-rw-r--r--  2 root root    0 Sep 18 17:00 test-097
-rw-r--r--  2 root root    0 Sep 18 17:00 test-098
-rw-r--r--  2 root root    0 Sep 18 17:00 test-099
drw------- 86 root root 4096 Sep 18 17:00 .glusterfs
drwxr-xr-x  3 root root 4096 Sep 18 17:00 .
-rw-r--r--  2 root root    0 Sep 18 17:00 test-100
isaac@isaac-MacBookPro:~$ ls -ltra /mnt/glusterfs/brick1/gv0/ | wc -l
104

builder@builder-MacBookPro2:~$ ls -ltra /mnt/glusterfs/brick1/gv0/ | wc -l
104

I’m now going to make a GCP mount

builder@anna-MacBookAir:/mnt$ sudo mkdir /mnt/glusterfsgcp
builder@anna-MacBookAir:/mnt$ sudo mount -t gcsfuse -o gid=1000,uid=1000,rw,noauto,file_mode=777,dir_mode=777,key_file=/home/builder/sa-storage-key.json myk3sglusterfs /mnt/glusterfsgcp
Calling gcsfuse with arguments: --dir-mode 777 --key-file /home/builder/sa-storage-key.json -o rw --gid 1000 --uid 1000 --file-mode 777 myk3sglusterfs /mnt/glusterfsgcp
2022/09/18 17:03:44.405349 Start gcsfuse/0.41.6 (Go version go1.18.4) for app "" using mount point: /mnt/glusterfsgcp
2022/09/18 17:03:44.419975 Opening GCS connection...
2022/09/18 17:03:44.866581 Mounting file system "myk3sglusterfs"...
2022/09/18 17:03:44.882374 File system has been successfully mounted.

I can then rsync the glusterfs distributed local storage out to GCP

builder@anna-MacBookAir:/mnt$ sudo rsync -r /mnt/glusterfs2 /mnt/glusterfsgcp

And after a bit, I can see it all replicated

/content/images/2022/09/glusterfs-05.png

Now I’ll schedule that to happen every hour

0 * * * * rsync -r /mnt/glusterfs2 /mnt/glusterfsgcp

Using crontab -e as root

builder@anna-MacBookAir:/mnt$ sudo crontab -e
no crontab for root - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        <---- easiest
  2. /usr/bin/vim.tiny
  3. /bin/ed

Choose 1-3 [1]: 2
crontab: installing new crontab


builder@anna-MacBookAir:/mnt$ sudo crontab -l
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
0 * * * * rsync -r /mnt/glusterfs2 /mnt/glusterfsgcp
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h  dom mon dow   command

A quick note: The reason you cannot use the GCP Fuse mount for GlusterFS is that you cannot have the same path used by two mounts. Additionally, the data bricks path needs extended attributes, which my ext4 partitions do just fine but a fuse mount does not.

Adding Longhorn

Now that I have some replicated storage that is backed up every hour to GCP, I’ll use it as the basis for a longhorn local stack.

Note, the GlusterFS will be faster than the GCP mount, so I’ll focus on the gluster mount

$ helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace --set defaultSettings.defaultDataPath="/mnt/glusterfs2"
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/builder/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/builder/.kube/config
W0918 17:13:32.013147   10463 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0918 17:13:32.268602   10463 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
NAME: longhorn
LAST DEPLOYED: Sun Sep 18 17:13:31 2022
NAMESPACE: longhorn-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Longhorn is now installed on the cluster!

Please wait a few minutes for other Longhorn components such as CSI deployments, Engine Images, and Instance Managers to be initialized.

Visit our documentation at https://longhorn.io/docs/

Like before, we’ll want a service to route traffic to the longhorn UI

builder@DESKTOP-QADGF36:~$ cat Workspaces/jekyll-blog/longhornsvc.yaml
apiVersion: v1
kind: Service
metadata:
  name: longhorn-ingress-lb
  namespace: longhorn-system
spec:
  selector:
    app: longhorn-ui
  type: LoadBalancer
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http
builder@DESKTOP-QADGF36:~$ kubectl apply -f Workspaces/jekyll-blog/longhornsvc.yaml
service/longhorn-ingress-lb created

Then I’ll port-forward to longhorn

$ kubectl port-forward svc/longhorn-ingress-lb -n longhorn-system 8080:80
Forwarding from 127.0.0.1:8080 -> 8000
Forwarding from [::1]:8080 -> 8000
Handling connection for 8080

/content/images/2022/09/glusterfs-06.png

I’ll create a simple volume

/content/images/2022/09/glusterfs-07.png

And I’ll attach it to a host

/content/images/2022/09/glusterfs-08.png

Which I can see marked healthy

/content/images/2022/09/glusterfs-09.png

GlusterFS Replication

I can see that the Volume has been replicated across the hosts

I’ll hop to a different worker node than the master to which I attached the volume and check the GlusterFS data brick directory

builder@builder-MacBookPro2:~$ ls -ltra /mnt/glusterfs/brick1/gv0/  | tail -n5
-rw-r--r--  2 root root    0 Sep 18 17:00 test-100
drwxr-xr-x  2 root root 4096 Sep 18 17:14 replicas
drwxr-xr-x  4 root root 4096 Sep 18 17:14 .
drw------- 87 root root 4096 Sep 18 17:14 .glusterfs
-rw-r--r--  2 root root   51 Sep 18 17:14 longhorn-disk.cfg

GCP Bucket Backup

As we created the crontab above, every hour it will sync to GCP.

I can verify by looking at the bucket mount

/content/images/2022/09/glusterfs-10.png

as well as GCP

/content/images/2022/09/glusterfs-11.png

Using with Pods

Let’s now use Longhorn for a PVC that will back a simple Hello World pod

$ cat glusterdemo.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gluster-claim
spec:
  storageClassName: longhorn
  accessModes:
  - ReadWriteMany
  resources:
     requests:
       storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: hello-openshift-pod
  labels:
    name: hello-openshift-pod
spec:
  containers:
  - name: hello-openshift-pod
    image: openshift/hello-openshift
    ports:
    - name: web
      containerPort: 80
    volumeMounts:
    - name: gluster-vol1
      mountPath: /usr/share/nginx/html
      readOnly: false
  volumes:
  - name: gluster-vol1
    persistentVolumeClaim:
      claimName: gluster-claim
$ kubectl apply -f glusterdemo.yml
persistentvolumeclaim/gluster-claim created
pod/hello-openshift-pod created

We can see it’s immediately bound

$ kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
gluster-claim   Bound    pvc-3ed1f6a7-982a-47db-85ba-905f16e9d238   1Gi        RWX            longhorn       44s

I can also see it has been created in the GlusterFS space

builder@anna-MacBookAir:~$ sudo ls /mnt/glusterfs2/replicas/
pvc-3ed1f6a7-982a-47db-85ba-905f16e9d238-a152d3eb

As well as on other hosts

builder@builder-MacBookPro2:~$ ls -ltra /mnt/glusterfs2/replicas/
total 16
drwxr-xr-x 3 root root 4096 Sep 18 17:14 ..
drwx------ 2 root root 4096 Sep 18 17:18 myglusterpvctest-45c7b2b3
drwxr-xr-x 4 root root 4096 Sep 19 06:03 .
drwx------ 2 root root 4096 Sep 19 06:03 pvc-3ed1f6a7-982a-47db-85ba-905f16e9d238-ba406489

And after the next rsync, I can see it in GCP as well

/content/images/2022/09/glusterfs-13.png

I can port-forward to the pod and see it is serving files

$ kubectl logs hello-openshift-pod
serving on 8888
serving on 8080
$ kubectl port-forward hello-openshift-pod 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080

/content/images/2022/09/glusterfs-12.png

Sadly, despite docs to the contrary, I could not ssh into the pod

$ kubectl exec -it hello-openshift-pod /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "10b5f3db478c3068d4767b268ce8580b0b92e8819ffbbcef004b5e4b3c193945": OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown
$ kubectl exec -it hello-openshift-pod sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "35d20bc3ef6b85e9cbab9921a171fb5dca6e79a77c3be53beb75154a2950dfa7": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown
$ kubectl exec -it hello-openshift-pod bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "e584776d0c8de17d2d7bdae8de6a2c6db26c623a80680504956ce1e1affc1c39": OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown
$ kubectl exec -it hello-openshift-pod zsh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "5c0d184278f6e3e4541b42b61269e8ceab931318318e3ecde241ae3586bd1098": OCI runtime exec failed: exec failed: unable to start container process: exec: "zsh": executable file not found in $PATH: unknown

I’ll use an NGinx example instead

$ cat glusterdemo2.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: http-claim
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: www
  labels:
    name: www
spec:
  containers:
  - name: www
    image: nginx:alpine
    ports:
      - containerPort: 80
        name: www
    volumeMounts:
      - name: www-persistent-storage
        mountPath: /usr/share/nginx/html
  volumes:
    - name: www-persistent-storage
      persistentVolumeClaim:
        claimName: http-claim
$ kubectl apply -f glusterdemo2.yml
persistentvolumeclaim/http-claim created
pod/www created

and a quick port-forward test

/content/images/2022/09/glusterfs-14.png

Now I’ll create a real file in the PVC

$ kubectl exec www -- sh -c 'echo "Hello Fresh Brewed PVC in Gluster" > /usr/share/nginx/html/index.html'

Then test

/content/images/2022/09/glusterfs-15.png

I can now delete the pod

$ kubectl delete pod && kubectl get pods
pod "www" deleted
NAME                  READY   STATUS    RESTARTS   AGE
hello-openshift-pod   1/1     Running   0          23m

I can re-run the YAML to create the pod (but it will skip the already satisfied PVC)

$ cat glusterdemo2.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: http-claim
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: www
  labels:
    name: www
spec:
  containers:
  - name: www
    image: nginx:alpine
    ports:
      - containerPort: 80
        name: www
    volumeMounts:
      - name: www-persistent-storage
        mountPath: /usr/share/nginx/html
  volumes:
    - name: www-persistent-storage
      persistentVolumeClaim:
        claimName: http-claim
$ kubectl apply -f glusterdemo2.yml
persistentvolumeclaim/http-claim unchanged
pod/www created

And a test of port-forward shows, indeed, the data is stil there

/content/images/2022/09/glusterfs-16.png

Direct Mounts

We can skip longhorn all-together and use local files. Since GlusterFS replicates across systems, I could just use a hostmount knowing it will be on any host in my pool (since I added Gluster to all 3)

On one of the hosts, I’ll create a folder and file to user (here I used the primary node as root)

root@anna-MacBookAir:~# mkdir /mnt/glusterfs2/nginxdemo1
root@anna-MacBookAir:~# echo "Testing Local Path" > /mnt/glusterfs2/nginxdemo1/index.html
root@anna-MacBookAir:~# chmod 755 /mnt/glusterfs2/nginxdemo1
root@anna-MacBookAir:~# chmod 644 /mnt/glusterfs2/nginxdemo1/index.html

Then I can create a definition that will use it’s mounted path…

$ cat glusterdemo3.yml
apiVersion: v1
kind: Pod
metadata:
  name: www2
  labels:
    name: www2
spec:
  containers:
  - name: www2
    image: nginx:alpine
    ports:
      - containerPort: 80
        name: www
    volumeMounts:
      - name: glusterfs-storage
        mountPath: /usr/share/nginx/html
  volumes:
    - name: glusterfs-storage
      hostPath:
        path: /mnt/glusterfs2/ngxindemo1/index.html
$ kubectl apply -f glusterdemo3.yml
pod/www2 created

While this may work, we are trying to hostpath to the Mount against the brick as opposed to the brick itself (which is synced).

A more direct mapping:

$ cat glusterdemo3.yml
apiVersion: v1
kind: Pod
metadata:
  name: www2
  labels:
    name: www2
spec:
  containers:
  - name: www2
    image: nginx:alpine
    ports:
      - containerPort: 80
        name: www
    volumeMounts:
      - name: glusterfs-storage
        mountPath: /usr/share/nginx/html
  volumes:
    - name: glusterfs-storage
      hostPath:
        path: /mnt/glusterfs/brick1/gv0/ngxindemo1
$ kubectl apply -f glusterdemo3.yml
pod/www2 created

which we can see work

/content/images/2022/09/glusterfs-19.png

And as you would expect, Rsync to GCP takes care of the backups

/content/images/2022/09/glusterfs-21.png

Summary

The first thing we tackled was setting up GlusterFS. To do this, we created an ansible playbook but then found some issues with using only IPv4.

Once I modified the /etc/hosts files and re-ran with hostnames, I was able to complete the creation of the GlusterFS Peers and subsequent Volumes.

I then created a Longhorn installation that exposed the mountpoint as a backend for hosting volumes as well as satisfying persistent volume claims (PVC). We then tested this with two different web front-end pods. We tested the durability of the PVC by destroying a pod after manually updating a file and then recreating against the same PVC to see the file persisted.

Essentially, we built out the following:

/content/images/2022/09/glusterfs-17.png

This certainly works to have a per-hour backup of our files. However, we could alternatively use the backup feature in Longhorn to push backups of our PVCs into GCP by way of the S3Proxy (which we covered in our last blog)

/content/images/2022/09/glusterfs-18.png

Our last test was using the GlusterFS data brick as a Pod’s Host mount to skip longhorn altogether. In this example I wouldn’t need a PVC. It’s essentially just exposing a path on the nodes which we happen to know is kept consistent by GlusterFS

/content/images/2022/09/glusterfs-20.png

Overall, GlusterFS is a pretty handy tool to keep paths in sync across hosts. Clearly, we could do similar by mounting an external NFS path or cloud storage as we’ve covered before. But in all of those scenarios, we then have an external dependance on other systems.

With GlusterFS we have a relatively air-gapped solution where the nodes just keep each other in sync. This could be also done hub-n-spoke style with a master path replicating to the workers using rsync - I do not want to discount that approach. But then you have to create a mapping and decide on timings.

I could easily see using GlusterFS, for instance, to sync common build tools across hosts that mount the builder agents (such as Github Runners or AzDO Agents). I could also see it serve a basic internal website or wiki that covers simple system metrics (where each host write stats to a hostname.html file).

gcp kubernetes storage glusterfs

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes