Introduction

This article covers our migration from OpenEBS to Longhorn on Kubernetes. We show how we moved persistent volumes over safely, and how we set up automated backups in Longhorn afterward.

Running two storage backends on the same Kubernetes cluster adds operational overhead you don’t need. Different failure modes, different backup mechanisms, more surface area to monitor and patch. In our case, some workloads sat on OpenEBS’s LVM local PV provisioner while everything else already ran on Longhorn.

Background: Why We Moved to Longhorn

Our cluster originally used OpenEBS’s LVM local PV provisioner for some workloads, alongside Longhorn for others. Running two storage backends side by side added operational overhead: different failure modes, different backup mechanisms, and more surface area to monitor and patch. We decided to standardize on Longhorn as the single storage layer for the cluster.

What is Longhorn?

Longhorn is a cloud-native distributed block storage system for Kubernetes. Unlike OpenEBS’s LVM local PV, which ties a volume’s data to a single node’s local disk, Longhorn replicates each volume across multiple nodes. This gives us a few concrete advantages:

  • Data replication across nodes, so a single node failure doesn’t mean data loss.
  • Volumes can be scheduled onto (and moved between) any node in the cluster, rather than being pinned to wherever the data physically lives.
  • A built-in UI for managing volumes, snapshots, and backups – no extra tooling required.
  • Native support for scheduled, incremental backups to S3-compatible object storage.
  • Easier disaster recovery: volumes can be restored from backup onto a fresh cluster if needed.

In short, Longhorn gave us both better resilience and a simpler day-2 operating model, which made it worth the one-time cost of migrating the OpenEBS-backed volumes.
More info about Longhorn Persistent Storage for Kubernetes

Planning the OpenEBS to Longhorn Migration

Before touching anything, we identified which PVCs were still running on the OpenEBS storage class, so we knew exactly what needed to move.

$ kubectl get sc
$ kubectl get pvc -o wide -A

Example output: (the pvc using the openebs-lvmpv sc is the one we going to migrate)

NAMESPACE    NAME                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE     VOLUMEMODE
default      smoke-longhorn          Bound    pvc-790ba330-49da-4104-9f46-2b23fe4a7a15   1Gi        RWO            longhorn        12d     Filesystem
default      smoke-lvm               Bound    pvc-56a43352-2fb1-4834-978e-dd09f3fbaaa1   1Gi        RWO            openebs-lvmpv   12d     Filesystem
monitoring   data-thanos-receive-0   Bound    pvc-5e51655b-f7cc-4f4b-8707-9ea7c93dfb9b   10Gi       RWO            longhorn        11d     Filesystem
wazuh        data-wazuh-indexer-0    Bound    pvc-e861d929-f449-4c08-9866-5d3247d1d1bd   10Gi       RWO            longhorn        4d20h   Filesystem

We then inspected the pod using that PVC to confirm its mount path and any other volume-related configuration before planning the cutover:

$ kubectl get pod lvm-test -o yaml
$ kubectl describe pod lvm-test

Migration: Step by Step

Our approach was straightforward and safe for small-to-medium volumes: provision a new Longhorn PVC, copy the data across via a temporary pod that mounts both volumes, verify the copy, then cut the workload over to the new volume.

Step 1 – Create the New Longhorn PVC

We defined a new PVC using the longhorn storage class:

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: smoke-longhorn-migrated
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 1Gi
$ kubectl apply -f pvc.yaml

Step 2 – Stop the Workload

To avoid writing to the old volume while data is being copied, we stopped any pod using it. For a bare pod, that’s a simple delete:

$ kubectl delete pod lvm-test

If the workload is managed by a Deployment or StatefulSet, scale it down instead.

$ kubectl scale deployment <deployment-name> --replicas=0
$ kubectl scale statefulset <statefulset-name> --replicas=0

Step 3 – Create a Temporary Migration Pod

We used a lightweight Alpine pod that mounts both the old (OpenEBS) and new (Longhorn) PVCs at the same time, purely to copy data between them:

# migrate.yaml
apiVersion: v1
kind: Pod
metadata:
  name: migrate-lvm
  namespace: default
spec:
  restartPolicy: Never
  containers:
  - name: migrate
    image: alpine
    command: ["sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: old
      mountPath: /old
    - name: new
      mountPath: /new
  volumes:
  - name: old
    persistentVolumeClaim:
      claimName: smoke-lvm
  - name: new
    persistentVolumeClaim:
      claimName: smoke-longhorn-migrated
$ kubectl apply -f migrate.yaml

Then we double-check that both PVCs mounted correctly before copying anything:

$ kubectl describe pod migrate-lvm
 
# Confirm:
#   old -> smoke-lvm
#   new -> smoke-longhorn-migrated

Step 4 – Copy the Data

With both volumes mounted, we copied the data across using tar, which preserves permissions with the -p flag:

$ kubectl exec -it migrate-lvm -- sh
# cd /old
# rsync -aHAX --info=progress2 /old/ /new/
## IF YOU DONT HAVE RSYNC INSTALL WITH THIS COMMAND:
# apk add --no-cache rsync

Step 5 – Verify the Copy

Before deleting anything, we confirmed the data matched on both sides:

# du -sh /old
# du -sh /new
# ls /old
# ls /new
# Make sure size and file listing on /old and /new match.

Step 6 – Point the Workload at the New Volume

We created (or updated) the workload to use the new Longhorn-backed PVC:

# lvm-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: lvm-test
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: smoke-longhorn-migrated
$ kubectl apply -f lvm-test.yaml

Step 7 – Validate

We confirmed the pod came up healthy and was actually bound to the new Longhorn PVC:

$ kubectl get pod lvm-test
# Expect: Running
 
$ kubectl describe pod lvm-test | grep ClaimName

And confirmed the application data was intact from inside the pod:

$ kubectl exec -it lvm-test -- sh
# ls /data
# Confirm the same files are present

Step 8 – Clean Up

Once validated, we removed the temporary migration pod and the old OpenEBS PVC:

$ kubectl delete pod migrate-lvm
$ kubectl delete pvc smoke-lvm

Finally, a cluster-wide check confirmed no OpenEBS-backed PVCs remained:

$ kubectl get pvc -A
# Confirm only 'longhorn' storage class is in use - no 'openebs' entries left

NOTICE: Once OpenEBS Deleted there’s no return and the data from OpenEBS is GONE!

Setting Up Backups in Longhorn

With everything running on Longhorn, the next step was to configure automated backups to S3-compatible object storage, so volumes could be restored in case of node loss or data corruption.

Create the S3 Credentials Secret

Longhorn reads S3 credentials from a Kubernetes secret in the longhorn-system namespace:

$ kubectl -n longhorn-system create secret generic s3-secret \
  --from-literal=AWS_ACCESS_KEY_ID=<your-access-key> \
  --from-literal=AWS_SECRET_ACCESS_KEY=<your-secret-key> \
  --from-literal=AWS_ENDPOINTS=<your-s3-endpoint>

Define the Backup Target

We then set the backup target and pointed it at the secret created above:

$ kubectl apply -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: Setting
metadata:
  name: backup-target
  namespace: longhorn-system
value: s3://<your-bucket>@<your-region>/
EOF
 
$ kubectl apply -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: Setting
metadata:
  name: backup-target-credential-secret
  namespace: longhorn-system
value: s3-secret
EOF

Configure the Backup Target in the Longhorn UI

From the Longhorn dashboard, go to Backups → Backup Target:

Click Create Backup Target and fill in:

  • Name – a name for the backup target
  • URL – the S3 (or MinIO) address
  • Credential Secret – the name of the secret created earlier (s3-secret)
  • Poll Interval – we left this at the default, 300 seconds

Attach and Back Up a Volume

In the Volumes tab, select the volume to back up. Make sure it’s attached – if not, attach it to one of the worker nodes first:

Once attached, open the volume and click Create Backup. The backup will move from Progress to Completed:

The completed backup will then be visible under Backup → Backups:

Restore from a Backup

To restore a volume from a backup, go to the Backup and Restore section and click Backup:

Choose the backup you want to restore and click Restore Latest Backup:

Fill in the restore details as needed – the restored volume will become available within minutes under the Volumes section.

Wait until the backup will be ready and then exec the pod and check if the right files are exist.


Summary

In conclusion, our migration from OpenEBS to Longhorn removed a second storage backend from our cluster. We now run on one storage layer instead of two. This gives us node-level resilience through replication. It also gave us native, UI-driven S3 backups, with almost no extra tooling required.

The migration was low-risk by design. The temporary dual-mount pod kept the old volume untouched. We only removed it after verifying the copy. This gave us a fallback until the very last cleanup step. At Octopus we value ours and our customers’ infrastructure and this represent

This pattern can be reused for future migrations and customers. Provision a new PVC. Copy data with a dual-mount pod. Verify the copy. Cut over. Clean up. It works for any storage-class migration, not just OpenEBS to Longhorn.

For more info about longhorn you can visit the docs here.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *