Migrate a PVC to EBS CSI from In-tree EBS

Ryan Graham
4 min readApr 5, 2023

--

This is how you convert a legacy In-tree EBS volume to EBS CSI.

Photo by Growtika on Unsplash

I am going to assume you already applied this method to your EKS cluster. But now you want to schedule backups for an old non-CSI PVC associated with an old pod.

These are the high level steps.

  1. Find the volume ID from the legacy PVC
  2. Take a snapshot of the legacy volume with AWS CLI
  3. Wait for the snapshot to complete
  4. Create a VolumeSnapshotContent object with the snapshot ID
  5. Create a VolumeSnapshot object referencing the VolumeSnapshotContent object
  6. Create a PersistentVolumeClaim (PVC) that references the VolumeSnapshot object
  7. Pass the new PVC to your Pod/Deployment/Release, etc.

In the following examples I’ll use what I did for my legacy Grafana helm release. If you want another perspective on the process, this is the guide I followed.

Find the legacy volume ID

First get the legacy In-tree EBS PVC ID

➜ kubectl get pvc -n grafana
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
grafana Bound pvc-fda7b565-01a6-43aa-9122-2f47444de173 256Gi RWO gp2 4d19h

Then use the PVC ID to get the volume ID from the PV

➜ kubectl get pv pvc-fda7b565-01a6-43aa-9122-2f47444de173 -n grafana -o jsonpath='{.spec.awsElasticBlockStore.volumeID}'
aws://us-east-1b/vol-0e3874465e9377743

Take a snapshot of the legacy volume

Use the volume ID from the step before and tag the snapshot for EBS CSI.

➜ aws ec2 create-snapshot --volume-id vol-0e3874465e9377743 --tag-specifications 'ResourceType=snapshot,Tags=[{Key="ec2:ResourceTag/ebs.csi.aws.com/cluster",Value="true"}]'
...
{
"Description": "",
"Encrypted": false,
"OwnerId": "redacted",
"Progress": "",
"SnapshotId": "snap-052b62c9a1495046c",
"StartTime": "2023-04-05T17:05:49.763000+00:00",
"State": "pending",
"VolumeId": "vol-0e3874465e9377743",
"VolumeSize": 256,
"Tags": [
{
"Key": "ec2:ResourceTag/ebs.csi.aws.com/cluster",
"Value": "true"
}
]
}

Now check the snapshot State until it changes from pending to completed.

➜ aws ec2 describe-snapshots --snapshot-ids snap-052b62c9a1495046c
...
{
"Snapshots": [
{
"Description": "",
"Encrypted": false,
"OwnerId": "redacted",
"Progress": "100%",
"SnapshotId": "snap-052b62c9a1495046c",
"StartTime": "2023-04-05T17:05:49.763000+00:00",
"State": "completed",
"VolumeId": "vol-0e3874465e9377743",
"VolumeSize": 256,
"Tags": [
{
"Key": "ec2:ResourceTag/ebs.csi.aws.com/cluster",
"Value": "true"
}
],
"StorageTier": "standard"
}
]
}

Create a VolumeSnapshotContent object with the snapshot ID

Since this is a 1-time operation just apply this and other manifests— no need for Flux. Note that in addition to specifying the snapshot ID from the step before I also chose my own namespace.

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotContent
metadata:
name: imported-grafana-content
namespace: grafana
spec:
volumeSnapshotRef:
kind: VolumeSnapshot
name: imported-grafana-snapshot
namespace: grafana
source:
snapshotHandle: snap-052b62c9a1495046c # <-- snapshot to import
driver: ebs.csi.aws.com
deletionPolicy: Delete
volumeSnapshotClassName: ebs-csi-aws
➜ kubectl apply -f volume_snapshot_content.yaml
volumesnapshotcontent.snapshot.storage.k8s.io/imported-grafana-content created
➜ kubectl get volumesnapshotcontent imported-grafana-content -n grafana
NAME READYTOUSE RESTORESIZE DELETIONPOLICY DRIVER VOLUMESNAPSHOTCLASS VOLUMESNAPSHOT VOLUMESNAPSHOTNAMESPACE AGE
imported-grafana-content true 274877906944 Delete ebs.csi.aws.com ebs-csi-aws imported-grafana-snapshot grafana 38s

Note that this references a VolumeSnapshot which doesn’t exist yet. Thats okay. We are about to create it.

Create a VolumeSnapshot

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: imported-grafana-snapshot
namespace: grafana
spec:
volumeSnapshotClassName: ebs-csi-aws
source:
volumeSnapshotContentName: imported-grafana-content
➜ kubectl apply -f volume_snapshot.yaml
volumesnapshot.snapshot.storage.k8s.io/imported-grafana-snapshot created
➜ kubectl get volumesnapshot imported-grafana-snapshot -n grafana
NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE
imported-grafana-snapshot true imported-grafana-content 256Gi ebs-csi-aws imported-grafana-content 10m 16s

Create a new PersistentVolumeClaim from the VolumeSnapshot

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: imported-grafana-pvc
namespace: grafana
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3
resources:
requests:
storage: 256Gi
dataSource:
name: imported-grafana-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
➜ kubectl apply -f pvc.yaml
persistentvolumeclaim/imported-grafana-pvc created
➜ kubectl get pvc -n grafana
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
grafana Bound pvc-fda7b565-01a6-43aa-9122-2f47444de173 256Gi RWO gp2 4d19h
imported-grafana-pvc Pending gp3 5s

Note that the legacy PVC is still bound and the new PVC will stay pending until we bind it instead.

Use the new PVC

Now in my example I am passing it to a Grafana helm release which will add the PVC to a deployment. But you could do the exact same thing with your own Deployment or Pod manifests.

This is the diff for my helm release in Flux. I just specified the name of my new PVC, imported-grafana-vpc, as an existingClaim. This is a field in the values for this particular helm chart which eventually gets interpolated into the Deployment template as the new PVC.

       enabled: true
type: pvc
size: 256Gi
+ existingClaim: imported-grafana-pvc
serviceAccount:
name: grafana
annotations:

After the release the old PVC disappears and the new PVC shows as bound.

➜ kubectl get pvc -n grafana
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
imported-grafana-pvc Bound pvc-1ac1c6a5-768b-4035-ab94-0e472a80ab1b 256Gi RWO gp3 31m

Now make a point of enabling EBS CSI as the default in all of your new clusters. This manual migration process is annoying and you won’t want to do it often.

--

--

No responses yet