Migrate your storage to another availability zone
Last updated on
This guide walks you through the steps required to migrate an existing Persistent Volume Claim (PVC) and its data to another availability zone (AZ).
Prerequisites
Section titled “Prerequisites”- A node pool in the target availability zone. For further information, read our documentation on node pools.
- Access to the Kubernetes cluster in which you want to migrate the volumes.
Before you begin
Section titled “Before you begin”Before you start the migration, make sure that you have a tested backup and a rollback plan. A storage migration involves moving and renaming live data, so you should:
- Create a tested backup of the volume data and verify that a restore works.
- Plan a downtime window for the workload that uses the volume.
- Document a rollback procedure (for example, keep the old PVC in a renamed state until the new volume is verified).
Choose a migration approach
Section titled “Choose a migration approach”This guide uses pv-migrate, which copies the data over the network into a freshly provisioned volume. The network copy works independently of the storage backend and deterministically targets the AZ of the pv-migrate pod.
For other workload types, consider these alternatives:
- Stateless/single RWO PVC: Use
pv-migrateas shown in this guide. - StatefulSet: The rename approach described here does not apply, because StatefulSets use fixed PVC names derived from
volumeClaimTemplates. Migrate each replica’s volume individually withpv-migrate, or use the application’s own replication mechanism (for example, database replica streaming) to bring up a new replica in the target AZ. - Database: Use the database’s own backup/restore mechanism or a managed offering such as STACKIT PostgreSQL Flex.
Migrate your storage
Section titled “Migrate your storage”To migrate a volume from one availability zone to another, you can use the tool pv-migrate. Install it by following the installation instructions.
To migrate a volume, you need to create a new volume with the same parameters as the old one. Therefore, fetch the old volume to get its definition with the following command:
kubectl get pvc <pvc-name> -n <namespace> -o yaml > pvc.yamlOpen pvc.yaml in the editor of your choice for the next steps. The result should look similar to the following example:
apiVersion: v1kind: PersistentVolumeClaimmetadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"old-volume","namespace":"docs"},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"1Gi"}},"volumeMode":"Filesystem"}} pv.kubernetes.io/bind-completed: "yes" pv.kubernetes.io/bound-by-controller: "yes" volume.beta.kubernetes.io/storage-provisioner: cinder.csi.openstack.org creationTimestamp: "2022-02-04T13:21:03Z" finalizers: - kubernetes.io/pvc-protection name: old-volume namespace: docs resourceVersion: "49199" uid: 4ad285b1-3be2-44c3-afd6-098631a6a6ecspec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: premium-perf1-stackit volumeMode: Filesystem volumeName: pv-shoot--garden--mig-test-4ad285b1-3be2-44c3-afd6-098631a6a6ecstatus: accessModes: - ReadWriteOnce capacity: storage: 1Gi phase: BoundRemove unnecessary annotations
Section titled “Remove unnecessary annotations”To turn this definition into a manifest for a new volume, remove the fields that Kubernetes manages automatically. From metadata.annotations, delete the following entries:
kubectl.kubernetes.io/last-applied-configurationpv.kubernetes.io/bind-completedpv.kubernetes.io/bound-by-controllervolume.beta.kubernetes.io/storage-provisioner
Also remove:
creationTimestampfinalizersresourceVersionuid
Under spec, delete volumeName. Also delete the entire status section.
Lastly, you have to give the PVC a new name. In this guide new-volume is used. Keep the same storageClassName as the source PVC for now; the next step explains how to verify its binding mode.
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: new-volume namespace: docsspec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: premium-perf1-stackit volumeMode: FilesystemVerify the volume binding mode
Section titled “Verify the volume binding mode”The target PVC must remain Pending until a pod consumes it, so the AZ can be influenced through the pv-migrate pod. This only works with volumeBindingMode: WaitForFirstConsumer. Check the binding mode of the StorageClass used by the source PVC:
kubectl get storageclass <storage-class-name> -o jsonpath='{.volumeBindingMode}{"\n"}'If the result is WaitForFirstConsumer, you can continue with the same StorageClass. If it is Immediate, the volume would be provisioned right away in the source AZ, before pv-migrate can influence scheduling. In that case, you need to create a custom StorageClass with late binding before continuing:
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: premium-perf1-stackit-waitprovisioner: cinder.csi.openstack.orgvolumeBindingMode: WaitForFirstConsumerallowVolumeExpansion: trueThen set storageClassName in the new PVC manifest to premium-perf1-stackit-wait (or the name you chose) and apply it again.
Read our documentation on storage classes and topologies for background on the available classes and binding modes.
After editing the file, save it and create the new PVC using the following command:
kubectl apply -f pvc.yamlCheck if the PVC has been created successfully. The result should look similar to the following example:
kubectl get pvc -n <namespace>NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEnew-volume Pending premium-perf1-stackit-wait 15mold-volume Bound pv-shoot--garden--mig-test-4ad285b1-3be2-44c3-afd6-098631a6a6ec 1Gi RWO premium-perf1-stackit 15mBe aware that the new volume should be in Pending status because of the WaitForFirstConsumer binding mode. If you created a custom StorageClass, make sure to set it in the PVC manifest.
Next, identify the availability zone of the target node pool. List the zones available to your nodes:
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}{end}'Choose your target AZ and create a file called values.yaml with the following content, replacing eu01-3 with your target zone:
rsync: nodeSelector: topology.kubernetes.io/zone: "eu01-3"The pv-migrate pod inherits this node selector, so it is scheduled in the target AZ. Because the new PVC uses WaitForFirstConsumer, the volume is then provisioned in the same AZ as the pv-migrate pod.
Now you should shut down the application mounting the volume you want to migrate. After the pod that mounted the volume has been shut down successfully, run the following command, replacing <old-pvc-name>, <new-pvc-name>, and <namespace> with your values.
pv-migrate --source <old-pvc-name> --dest <new-pvc-name> --source-namespace <namespace> --dest-namespace <namespace> -f values.yaml🚀 Starting migration💭 Will attempt strategies: mount, clusterip🚁 Attempting strategy: clusterip🔑 Generating SSH key pair📂 Copying data... 100% |████████████████████████████████████████████████████████████| ()🧹 Cleaning up✨ Cleanup done✅ Migration succeededIf the migration was successful, the data is now available in the new availability zone. To confirm this, inspect the nodeAffinity of the newly created persistent volume.
kubectl get pv <new-volume-name> -o yaml | grep -A5 nodeAffinitynodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: topology.cinder.csi.openstack.org/zone operator: In values: - eu01-3The values entry shows the availability zone the volume was provisioned in.
As the data is now in the correct availability zone, you need to point your application at the new volume. You can do this in two ways: bind the new PVC directly in your deployment, or rename the new PVC to the old PVC’s name so the deployment stays unchanged.
This guide uses the renaming approach: instead of changing your deployment to reference the new PVC, the old PVC is renamed and the new PVC takes the original name. This keeps the old PVC available in case a rollback is required.
-
Install
rename-pvcby following its installation instructions. -
Verify the installation:
Terminal window rename-pvc --help -
Move the old PVC aside by renaming it to a temporary backup name (
<old-pvc-name>-backup):Terminal window rename-pvc -n <namespace> <old-pvc-name> <old-pvc-name>-backup -
Give the new PVC the original name:
Terminal window rename-pvc -n <namespace> <new-pvc-name> <old-pvc-name>
Now you can safely restart your application. It will use the volume in the correct availability zone without any changes to the deployment’s specification.
After verifying that the application works correctly with the new volume, clean up the old PVC that was renamed to <old-pvc-name>-backup:
kubectl delete pvc <old-pvc-name>-backup -n <namespace>Your storage and its data are now migrated to the new availability zone.