How to migrate Longhorn volumes to a CSI

Jerome

In this article we’re going to see how to easily migrate Longhorn volumes to a CSI driver that provides storage directly from the cloud provider.

In this example, we’re using Exoscale as the cloud provider but the concept is the same whatever cloud provider you’re using, as long as they provide block storage through a Kubernetes CSI driver.

This is for Exoscale SKS only, see your cloud provider documentation to find out how to do the equivalent with them.

On your existing Exoscale SKS cluster, update the cluster to add the Exoscale Container Storage Interface addon:

exo compute sks update \
    --zone ch-gva-2 \
    --enable-csi-addon \
      <cluster name>

We should now have at least two CSI drivers:

$ k get csidrivers
NAME                 ATTACHREQUIRED   PODINFOONMOUNT   STORAGECAPACITY   TOKENREQUESTS   REQUIRESREPUBLISH   MODES        AGE
csi.exoscale.com     true             true             false             <unset>         false               Persistent   6m20s
driver.longhorn.io   true             true             false             <unset>         false               Persistent   19d

and we should have at least two storage classes:

$ k get storageclasses
NAME                 PROVISIONER          RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
exoscale-sbs         csi.exoscale.com     Delete          Immediate           true                   6m58s
longhorn (default)   driver.longhorn.io   Retain          Immediate           true                   301d

We’re going to migrate volumes from the longhorn storage class to the exoscale-sbs storage class.

As a real-world example, let’s say we have a WordPress installation deployed with Helm. It might be difficult to change its PVC name so we want to try and find a transparent way of migrating the data, keeping the same PVC name so that the application doesn’t need any YAML changes.

We’re going to follow the steps below. For ease of use, we’ve created a script that has some additional error control and validation features. You can find it at https://github.com/creativemoods/movestorageclass.

First, let’s define some variables that we’ll need later:

NAMESPACE=mynamespace
STORAGECLASS=exoscale-sbs # Change this with the name of the storage class from your cloud provider
DEPLOY=mydeployment       # The name of your deployment or statefulset
PVC=wordpress-data     # The name of the PVC we're migrating
SIZE=10Gi                 # The size of the new PV, should be enough to fit all the data

First, we’re scaling the deployment to 0 because we don’t want the application to write data to the PV while we’re migrating

kubectl scale --replicas=0 deploy/$DEPLOY -n $NAMESPACE

then we create a new PVC and PV using the cloud provider’s CSI

cat > /tmp/newpvc.yaml <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tmppvc
  namespace: $NAMESPACE
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: $SIZE
  storageClassName: $STORAGECLASS
EOF
kubectl apply -f /tmp/newpvc.yaml

Then we use a pod to mount both volumes (code from https://medium.com/@smathew.35/copying-data-between-persistent-volumes-in-kubernetes-a-simple-guide-acc8f79c2d40)

cat > /tmp/datamover.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: data-mover
  name: data-mover
  namespace: $NAMESPACE
spec:
  replicas: 1
  selector:
    matchLabels:
      app: data-mover
  template:
    metadata:
      labels:
        app: data-mover
    spec:
      containers:
      - args:
        - -c
        - while true; do ping localhost; sleep 60;done
        command:
        - /bin/sh
        image: quay.io/quay/busybox
        name: data-mover
        volumeMounts:
        - mountPath: /source
          name: source
        - mountPath: /destination
          name: destination
      restartPolicy: Always
      volumes:
      - name: source
        persistentVolumeClaim:
          claimName: $PVC
      - name: destination
        persistentVolumeClaim:
          claimName: tmppvc
EOF
kubectl apply -f /tmp/datamover.yaml

We then copy contents from the source to the destination

kubectl exec -n $NAMESPACE deploy/data-mover -- sh -c "cp -aR /source/* /destination/"

And we destroy data-mover as we don’t need it anymore

kubectl -n $NAMESPACE delete deploy/data-mover

We can now delete the original PVC. This might delete the PV but that’s OK because we’ve copied the data. If the PV was not deleted because its retention policy was Retain, delete the PV also.

kubectl -n $NAMESPACE delete pvc/$PVC

We change the Retain Policy on the new PV so that we can delete its PVC while keeping the PV

PV=$(kubectl get pvc tmppvc -n $NAMESPACE -o=json | jq -r '.spec.volumeName')
kubectl patch pv $PV -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

and we delete the new PVC tmppvc

kubectl -n $NAMESPACE delete pvc/tmppvc

We remove the claimref from the new PV, otherwise we won’t be able to reassociate the PV and PVC

kubectl patch pv $PV --type json -p '[{"op": "remove", "path": "/spec/claimRef"}]'

and we recreate a new PVC with the same name as the original PVC and we specify that we want to bind to the new PV

cat > /tmp/bindpvc.yaml <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: $PVC
  namespace: $NAMESPACE
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: $SIZE
  volumeName: $PV
  storageClassName: $STORAGECLASS
EOF
kubectl apply -f /tmp/bindpvc.yaml

Now we wait until the PVC has status Bound. This should happen automatically. Finally, we start our application back up

kubectl scale --replicas=1 deploy/$DEPLOY -n $NAMESPACE

Our application should now be up and running and using data from our CSI instead of Longhorn.

About the author

Jerome is a seasoned IT specialist with a strong passion for full-stack development with Symfony, React, Flutter and Python, and for DevOps culture, Kubernetes, containerization, and CI/CD pipelines.

This site uses cookies to improve your experience. Learn more.