Skip to content

Storage Infrastructure ​

The cluster uses LINSTOR/Piraeus Datastore for distributed block storage with synchronous DRBD replication, managed by the Piraeus Operator.

Storage Pools ​

Two LUKS2-encrypted storage pools are available:

PoolMediaDevice
nvmeNVMe/dev/disk/by-id/dm-name-luks2-r-nvme1
ssdSSD/dev/disk/by-id/dm-name-luks2-r-ssd1

Storage Classes ​

Replicated (3-way) - for single-instance workloads:

  • nvme-replicated-retain / nvme-replicated-delete
  • ssd-replicated-retain / ssd-replicated-delete

Local (single replica) - for workloads with built-in replication (database clusters):

  • nvme-local-retain / nvme-local-delete
  • ssd-local-retain / ssd-local-delete

All classes use XFS, support volume expansion, and use WaitForFirstConsumer binding mode.

Choosing a storage class:

  • Database clusters (PostgreSQL, MariaDB) → *-local-* (the operator handles replication)
  • Single-instance apps → *-replicated-* (LINSTOR handles replication)
  • High IOPS workloads → nvme-*
  • Large sequential workloads → ssd-*
  • Data must survive PVC deletion → *-retain

Local Storage and Database Replication ​

With replicated storage, LINSTOR copies every block write to 3 nodes via DRBD. If a node fails, the pod can reschedule to another node and still access the data. This is ideal for single-instance applications that don't handle their own replication.

DRBD stands for Distributed Replicated Block Device. It's a Linux kernel module that mirrors block devices (like disks or partitions) between nodes over the network in real-time.

With local storage, each PVC exists only on one node - no DRBD replication. This sounds risky, but it makes sense for database clusters that replicate data themselves.

Consider the Keycloak PostgreSQL cluster (clusters/main/apps/idp/keycloak/db.yaml):

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: keycloak-db
spec:
  instances: 4
  storage:
    storageClass: nvme-local-retain
    size: 5Gi

CloudNativePG creates 4 PostgreSQL instances, each with its own 5Gi PVC. The instances are scheduled to different nodes, and PostgreSQL streaming replication keeps them in sync:

Node A                Node B                Node C                Node D
┌─────────────┐       ┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│ keycloak-db │       │ keycloak-db │       │ keycloak-db │       │ keycloak-db │
│   primary   │──────▶│   replica   │──────▶│   replica   │──────▶│   replica   │
│             │  WAL  │             │  WAL  │             │  WAL  │             │
├─────────────┤ stream├─────────────┤ stream├─────────────┤ stream├─────────────┤
│  PVC (5Gi)  │       │  PVC (5Gi)  │       │  PVC (5Gi)  │       │  PVC (5Gi)  │
│   local     │       │   local     │       │   local     │       │   local     │
└─────────────┘       └─────────────┘       └─────────────┘       └─────────────┘

If Node A fails, CloudNativePG promotes a replica to primary. The data on Node A's PVC is lost, but the other 3 replicas have complete copies. When Node A recovers (or a replacement is added), a new replica syncs from the current primary.

Using replicated storage here would be wasteful - you'd have 3x DRBD replication and 4x PostgreSQL replication, copying data 12 times total instead of 4.

Adding Storage to an Application ​

All changes must go through Git and Flux. Never create or patch PVCs manually.

1. Add a PVC manifest to the application directory:

yaml
# clusters/main/apps/<namespace>/<app>/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-data
  namespace: my-namespace
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: nvme-replicated-retain

2. Add the PVC to the application's kustomization.yaml:

yaml
resources:
  - pvc.yaml
  # ... other resources

3. Commit and push. Flux will reconcile the changes.

Expanding a Volume ​

1. Edit the PVC manifest in Git and increase the storage request:

yaml
spec:
  resources:
    requests:
      storage: 20Gi  # was 10Gi

2. Commit and push. Flux applies the change, and LINSTOR expands the volume online.

Inspecting Storage ​

Check PVC and PV status:

bash
kubectl get pvc -A
kubectl get pv

Access the LINSTOR CLI for detailed diagnostics:

bash
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor resource list
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor storage-pool list
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor volume list

Database Storage ​

PostgreSQL clusters are managed by CloudNativePG with 4 instances each. Storage is defined in the Cluster CRD:

ClusterNamespaceStorageStorageClass
keycloak-dbidp5Ginvme-local-retain
zammad-dbtickets20Gissd-replicated-retain
mailctl-dbmail5Ginvme-local-retain
lists-dbmail250Ginvme-local-retain
studi-dbmail75Ginvme-local-retain

MariaDB (webspaces namespace) uses nvme-replicated-retain with 10Gi.

Troubleshooting ​

If a volume is stuck pending, check pool capacity and LINSTOR errors:

bash
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor storage-pool list
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor error-report list

If replication is degraded, check resource sync status:

bash
kubectl exec -it -n piraeus-datastore deploy/linstor-controller -- linstor resource list-volumes

Configuration ​

Storage infrastructure is defined in:

  • clusters/main/infrastructure/essentials/piraeus.yaml - Helm release
  • clusters/main/infrastructure/essentials/piraeus/storage-classes.yaml - StorageClasses
  • clusters/main/infrastructure/essentials/piraeus/linstor-cluster.yaml - Cluster config
  • clusters/main/infrastructure/essentials/piraeus/linstor-satellite-configuration.yaml - Storage pools