OpenEBS is a CNCF storage project that provides persistent volumes for Kubernetes using the disks already attached to your nodes. Since version 4, it focuses on two families: Local PV engines (Hostpath, LVM and ZFS), which give each volume fast storage on a single node, and Replicated PV Mayastor, which replicates volumes across nodes over NVMe-oF. In this tutorial you will install OpenEBS with Helm on Ubuntu 24.04 nodes, provision volumes with Local PV Hostpath, and then use Local PV LVM for volumes that have real capacity limits and can be expanded online.

The older Jiva and cStor engines are no longer part of OpenEBS 4. If you still run them, plan a migration to Local PV or Mayastor.

Prerequisites

To follow this tutorial, you will need:

  • A Kubernetes cluster (1.23 or newer) with worker nodes running Ubuntu 24.04, for example CubePath VPS instances.
  • kubectl and Helm 3 on the machine you manage the cluster from.
  • SSH access with a sudo user to the worker nodes.
  • For Step 4: an unused block device on at least one worker node (for example a second disk at /dev/vdb). Everything on that disk will be erased.

Local PV volumes live on one node. If that node fails, the pod cannot start elsewhere until the node comes back. Use them for workloads that replicate data themselves (databases with replication, Kafka, Elasticsearch) or for data you can recreate.

Step 1 - Installing OpenEBS with Helm

Add the OpenEBS chart repository:

helm repo add openebs https://openebs.github.io/openebs
helm repo update

Install the chart into the openebs namespace. This tutorial uses only the Local PV engines, so disable Mayastor, which otherwise requires huge pages and dedicated disks on every storage node:

helm install openebs openebs/openebs \
  --namespace openebs \
  --create-namespace \
  --set engines.replicated.mayastor.enabled=false

Wait until the pods are running:

kubectl -n openebs get pods
NAME                                              READY   STATUS    RESTARTS   AGE
openebs-localpv-provisioner-6b8bff68bd-4tq9x      1/1     Running   0          90s
openebs-lvm-localpv-controller-7b6d6b4665-pk2rm   5/5     Running   0          90s
openebs-lvm-localpv-node-8xw4c                    2/2     Running   0          90s
openebs-lvm-localpv-node-q9dfz                    2/2     Running   0          90s
openebs-zfs-localpv-controller-f78f7467c-9lzfs    5/5     Running   0          90s
openebs-zfs-localpv-node-5vwbn                    2/2     Running   0          90s
openebs-zfs-localpv-node-mcxm8                    2/2     Running   0          90s

Pod names and counts depend on the chart version and the number of nodes. The chart creates a StorageClass for Local PV Hostpath:

kubectl get storageclass
NAME               PROVISIONER        RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
openebs-hostpath   openebs.io/local   Delete          WaitForFirstConsumer   false                  2m

Step 2 - Provisioning a Local PV Hostpath volume

Local PV Hostpath creates a directory on the node for each volume, under /var/openebs/local by default. It needs no extra setup, which makes it a good replacement for manual hostPath volumes. Create a claim and a pod that uses it:

nano hostpath-test.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: hostpath-pvc
spec:
  storageClassName: openebs-hostpath
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: hostpath-test
spec:
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "echo hello from openebs > /data/hello.txt && sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: hostpath-pvc
kubectl apply -f hostpath-test.yaml

The StorageClass uses WaitForFirstConsumer, so the claim stays Pending until the pod is scheduled, and the volume is then created on the node the scheduler picked. Check both:

kubectl get pvc hostpath-pvc
kubectl get pod hostpath-test -o wide
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       AGE
hostpath-pvc   Bound    pvc-9a1d2c3b-4e5f-4a6b-8c7d-0e1f2a3b4c5d   1Gi        RWO            openebs-hostpath   30s
NAME            READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
hostpath-test   1/1     Running   0          30s   10.0.1.143   k8s-w1   <none>           <none>

On that node (k8s-w1 here), the data is a plain directory:

sudo cat /var/openebs/local/pvc-9a1d2c3b-4e5f-4a6b-8c7d-0e1f2a3b4c5d/hello.txt
hello from openebs

To store volumes on a different disk, for example a dedicated mount at /mnt/fast, create your own StorageClass with the BasePath option:

nano hostpath-fast.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: hostpath-fast
  annotations:
    openebs.io/cas-type: local
    cas.openebs.io/config: |
      - name: StorageType
        value: hostpath
      - name: BasePath
        value: /mnt/fast
provisioner: openebs.io/local
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
kubectl apply -f hostpath-fast.yaml

Step 3 - Preparing an LVM volume group on the nodes

Local PV LVM carves a logical volume out of an LVM volume group for each claim. Sizes are enforced, volumes can be expanded online, and you can choose the filesystem. On each worker node that should provide LVM storage, install the tools and check the spare disk:

sudo apt update
sudo apt install -y lvm2
lsblk -d -o NAME,SIZE,TYPE,FSTYPE

The spare disk has no filesystem:

NAME  SIZE TYPE FSTYPE
vda    80G disk
vdb   100G disk

Create a physical volume and a volume group called lvmvg on the empty disk. Replace /dev/vdb with your device name and double-check it, since this erases the disk:

sudo pvcreate /dev/vdb
sudo vgcreate lvmvg /dev/vdb
sudo vgs
  VG    #PV #LV #SN Attr   VSize    VFree
  lvmvg   1   0   0 wz--n- <100.00g <100.00g

Step 4 - Provisioning Local PV LVM volumes

From your workstation, create a StorageClass that uses the lvmvg volume group and formats volumes with XFS, a good default for databases:

nano openebs-lvm.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: openebs-lvm
provisioner: local.csi.openebs.io
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
parameters:
  storage: "lvm"
  volgroup: "lvmvg"
  fsType: "xfs"

WaitForFirstConsumer lets the scheduler choose a node that has the volume group and enough free space. Apply it:

kubectl apply -f openebs-lvm.yaml

Create a claim and a pod:

nano lvm-test.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: lvm-pvc
spec:
  storageClassName: openebs-lvm
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: lvm-test
spec:
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: lvm-pvc
kubectl apply -f lvm-test.yaml
kubectl get pvc lvm-pvc
NAME      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
lvm-pvc   Bound    pvc-5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e   5Gi        RWO            openebs-lvm    25s

Check the size inside the pod:

kubectl exec lvm-test -- df -h /data
Filesystem                Size      Used Available Use% Mounted on
/dev/lvmvg/pvc-5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e
                          5.0G     68.0M      4.9G   1% /data

On the node, sudo lvs shows a logical volume with the same name as the PV. OpenEBS also tracks it as an LVMVolume object:

kubectl -n openebs get lvmvolumes

Step 5 - Expanding an LVM volume online

Because the StorageClass sets allowVolumeExpansion: true, you can grow the claim while the pod is running. Increase it to 10 GiB:

kubectl patch pvc lvm-pvc -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'

The CSI driver extends the logical volume and grows the XFS filesystem. After a few seconds:

kubectl exec lvm-test -- df -h /data
Filesystem                Size      Used Available Use% Mounted on
/dev/lvmvg/pvc-5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e
                          9.9G    104.0M      9.8G   1% /data

Expansion only works while the volume group has free space on that node. Add another disk to the group with sudo vgextend lvmvg /dev/vdc when it runs low.

Step 6 - Cleaning up the test resources

Delete the test pods and claims. With reclaimPolicy: Delete, OpenEBS removes the directory or logical volume on the node:

kubectl delete -f hostpath-test.yaml -f lvm-test.yaml

Run sudo lvs on the node to confirm the logical volume is gone.

Troubleshooting

The claim stays Pending with no pod using it. This is normal with WaitForFirstConsumer: the volume is created only when a pod that uses the claim is scheduled. kubectl describe pvc your_pvc shows waiting for first consumer to be created before binding.

LVM claim stays Pending after the pod is created. No node has a volume group with the configured name and enough free space, or the node plugin is not running there. Check kubectl -n openebs get pods -o wide | grep lvm-localpv-node and the controller logs with kubectl -n openebs logs deploy/openebs-lvm-localpv-controller --all-containers.

The pod is stuck Pending after its node was removed. A Local PV is tied to one node through node affinity. If the node is gone for good, the data is lost: delete the PVC and let the application recreate or resync its data.

Conclusion

You installed OpenEBS 4 with Helm, used Local PV Hostpath for simple node-local volumes and Local PV LVM for volumes with enforced sizes and online expansion. From here you can install the snapshot controller to take VolumeSnapshot objects of LVM volumes, try Local PV ZFS for compression and checksums, or enable Replicated PV Mayastor on nodes with NVMe disks when you need volumes that survive the loss of a node.