Introduction
For the past few months, I’ve been on a journey of moving all of my self-hosted stuff into kubernetes in order to learn it better. During this time I have also deployed a bunch of new apps in order to replace some of the hosted solutions I used prior to this in hope to save some money on the subscriptions, and with time, to reinvest in my home lab further.
One of this apps has been excellent Audiobookshelf, to replace both PocketCasts for podcasts and Audible for audio books. I have been amazed by the app during the past few months I have been using it.
Initially when I decided to deploy Audiobookshelf on my K8s cluster, I pieced together a StatefulSet
definition which created one instance of the app (1 pod) and PersistentVolumeClaim
for each of the volumeMounts
(config
, metadata
, podcasts
, audiobooks
).
When I started I was conservative with the space allocated to the volumes via volumeClaimTemplates
definition, which meant I quickly ran out of space while the actual usage hit the service (podcasts started downloading, I started uploading audio books etc.). I then quickly realized that I can’t simply change the StatefulSet
manifest and apply it to resize the volumes, instead, I had to reside to the manual volume resize which was no big deal.
As I deploy all of the apps using ArgoCD now (which is beyond the scope of this blog post), this meant that I either had to:
- have ArgoCD not being able to sync the state of the
StatefulSet
if I changed the manifest in my repository, and thus not be able to manage deployments - have value that doesn’t match the reality in the
volumeClaimTemplates
section in git, which triggers my CDO (OCD in the alphabetical order) and also means I’m managing volume size from that point manually and directly on the cluster
I’m sure StatefulSet
has its use-cases (like for managing database instances for example), but I’m now thinking this app wasn’t really a good fit for it, so I decided to transition to the Deployment
Easier said than done (not quite)
As this is not a mission critical operation, I had the privilege of scaling app to 0, and make necessary steps. Initial plan was quite simple:
- write definition for each of the PV and PVC resources and commit to git
- comment out
StatefulSet
definition - write
Deployment
definition - let ArgoCD sort things out by syncing it to the cluster
I haven’t given this too much thought as I quickly got some other ideas in my mind, so perhaps such scenario would not even work in the first place (I would probably realize it too late and have to restore things from the backup).
As I’m (still) provisioning and managing persistent volumes using Longhorn, and as podcasts and audio books had 60+GB worth of data in them, and as that was a cause of frequent problems with my kubernetes storage (one example: node crashes for misc reasons, longhorn starts re-sync in order to ensure number of desired copies, grinds other nodes to halt as that is IO intensive with that amount of data, node comes back up, longhorn triggers re-sync again to ensure no 2 copies are on the same node etc. Perhaps I’ll go into detail about this part of the setup sometime in the future(tm), but I’m not currently very happy with Longhorn as it stands) I came up with a brilliant idea.
Anyhow, my brilliant idea (almost as brilliant as I, myself, am) was to provision those large volumes using NFS (I have nfs-csi configured already in the cluster), and keep config
and metadata
on longhorn cluster (it should in theory be more performant since it uses local storage).
This required some variations in the initial plan, so the process went somewhat like this:
- create PV (NFS provisioned)
podcasts
audiobooks
- create PVC for each of the volumes
podcasts
-> NFS volumeaudiobooks
-> NFS volumeconfig
-> longhornmetadata
-> longhorn
Longhorn will take care of creating new volumes for the PVC. I also used different (simpler) names to make everything clean and consistent, which also resulted with added benefit that I could easily revert back to the old setup if something went wrong horribly wrong.
With all pieces in place, it was time to migrate the data. In order to avoid shenanigans with mounting volumes to nodes manually, syncing things etc. I found a small utility called pv-migrate that did that for me. It basically created a new pod which mounted both the old and the new volume, and synced the data to the new volume using rsync. I used this for each of the 4 volumes.
Once the data migration process was complete, I applied the Deployment definition and application started up with 1 replica. I then verified if everything worked like and after confirming so, I deleted the StatefulSet
.
Audiobookshelf configuration
For anyone interested, here’s my full up-to-the-time-of-publishing-this-article service definition (perhaps I’ll open up git repository and link it here at one point in the future):
# namespace.yaml
kind: Namespace
apiVersion: v1
metadata:
name: audiobookshelf
# service.yaml
kind: Service
apiVersion: v1
metadata:
name: audiobookshelf
namespace: audiobookshelf
spec:
selector:
app: audiobookshelf
ports:
- protocol: TCP
port: 80
name: http
# networking.yaml
# generated with https://editor.networkpolicy.io/
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: audiobookshelf-network-policy
namespace: audiobookshelf
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: haproxy-controller
podSelector:
matchLabels:
app.kubernetes.io/name: kubernetes-ingress
ports:
- port: 80
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
# ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: audiobookshelf
namespace: audiobookshelf
spec:
ingressClassName: haproxy
rules:
- host: audiobookshelf.MYINTERNALDNS
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: audiobookshelf
port:
name: http
# volumes.yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: audiobookshelf-podcasts
annotations:
pv.kubernetes.io/provisioned-by: nfs.csi.k8s.io
spec:
capacity:
storage: 2Ti
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
mountOptions:
- nfsvers=4.1
csi:
driver: nfs.csi.k8s.io
volumeHandle: st00rage.MYINTERNALDNS.#volume1#audiobookshelf-podcasts
volumeAttributes:
server: st00rage.MYINTERNALDNS.
share: /volume1/audiobookshelf-podcasts
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: audiobookshelf-audiobooks
annotations:
pv.kubernetes.io/provisioned-by: nfs.csi.k8s.io
spec:
capacity:
storage: 2Ti
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
mountOptions:
- nfsvers=4.1
csi:
driver: nfs.csi.k8s.io
volumeHandle: st00rage.MYINTERNALDNS.#volume1#audiobookshelf-audiobooks
volumeAttributes:
server: st00rage.MYINTERNALDNS.
share: /volume1/audiobookshelf-audiobooks
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: audiobookshelf-podcasts
namespace: audiobookshelf
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
volumeName: audiobookshelf-podcasts
storageClassName: ""
resources:
requests:
storage: 2Ti
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: audiobookshelf-audiobooks
namespace: audiobookshelf
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
volumeName: audiobookshelf-audiobooks
storageClassName: ""
resources:
requests:
storage: 2Ti
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: audiobookshelf-config
namespace: audiobookshelf
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 128Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: audiobookshelf-metadata
namespace: audiobookshelf
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 512Mi
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: audiobookshelf
namespace: audiobookshelf
labels:
app: audiobookshelf
spec:
replicas: 1
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: audiobookshelf
template:
metadata:
labels:
app: audiobookshelf
spec:
containers:
- name: audiobookshelf
image: ghcr.io/advplyr/audiobookshelf:2.9.0
volumeMounts:
- name: config
mountPath: /config
- name: metadata
mountPath: /metadata
- name: podcasts
mountPath: /podcasts
- name: audiobooks
mountPath: /audiobooks
volumes:
- name: config
persistentVolumeClaim:
claimName: audiobookshelf-config
- name: metadata
persistentVolumeClaim:
claimName: audiobookshelf-metadata
- name: audiobooks
persistentVolumeClaim:
claimName: audiobookshelf-audiobooks
- name: podcasts
persistentVolumeClaim:
claimName: audiobookshelf-podcasts
I’m aware that there are plenty of things that can be improved, but so far, this works fine for me. If you see some mistakes, or have some advice how to improve it though, don’t hesitate to reach out, I’m always glad to hear that someone reads this stream of semi-filtered thoughts and unnecessarily long articles on my blog :-)
Outro
During my learning process, I was bound to make some mistakes, and I will probably make a bunch more, but hey, that’s life.
I know I have been slow in posting certain stuff on this blog, but as you all know, life happens, so I’ll do that whenever I have some extra time and energy to write about certain topics. If you have some suggestions or something you’d like to read about next in my setup, feel free to reach out via email or something :-)