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.
kubectland 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
NoteHostpath volumes do not enforce the requested size. A pod can fill the whole filesystem that holds
/var/openebs/local. If you need hard limits, use Local PV LVM as shown next.
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.
