AWS Storage Blog

Simplifying Amazon EBS volume migration and modification on Kubernetes using the EBS CSI Driver

Enterprises running critical applications in containers may require access to a persistent storage layer that extends beyond the lifetime of a container instance. A block storage solution such as Amazon Elastic Block Store (Amazon EBS) is a good fit due to its high performance, low latency, and persistence which ensures that data can be re-attached to new containers. In particular, AWS customers deploying and running containerized applications using Kubernetes (K8s) orchestration or a container orchestration service on AWS such as Amazon Elastic Kubernetes Service (Amazon EKS) choose the SSD-based general-purpose EBS volumes as their preferred storage solution. Using EBS customers can achieve high performance at low cost for a wide variety of applications, such as virtual desktops, databases, as well as development and testing environments.

Kubernetes customers use the EBS Container Storage Interface (CSI) driver to provision and manage their EBS volumes. In August 2022, AWS enabled CSI migration (CSIMigration) feature by default in EKS 1.23, making CSI driver the default storage driver for EKS customers using EBS and replacing the Kubernetes “in-tree” storage driver that exists in the Kubernetes project source code. Using the CSI driver, customers can now use all available EBS volume types and additional feature set for their storage needs, previously not available via the in-tree driver. Until now, K8s customers modify their volume performance and types directly using AWS Console or APIs. However, when using the K8s control plane, users build workarounds, sometimes recreating the volumes, to migrate between EBS volume types and change performance attributes. The volume type migration often requires a downtime of applications, slowing down enterprise customers’ adoption of newer generation of EBS volumes.

Customers like SAP want to use the EBS CSI driver to be the one stop shop for all their storage management needs running on EKS or other K8s managements, without having to use AWS specific APIs. We are working with the K8s community to make volume modification available for all storage providers using standard Kubernetes interfaces. Given that EKS 1.22 is End of Support (EOS) on June 4, 2023 and EKS 1.23 has CSIMigration enabled by default, customers increasingly depend on the CSI driver, we felt that it was important to provide a solution to AWS customers now, before a standardized solution becomes available. To simplify operations, we added support for new ModifyVolume capabilities within the CSI driver. In addition to the existing ability to increase volume size, customers can now also change the volume type and adjust the performance (IOPS and throughput) of their EBS volumes, by modifying the annotations within their Persistent Volume Claims (PVC). Changes made through annotations apply dynamically to the Amazon EBS volumes without the need to detach them.

In this blog post, we cover how the new ModifyVolume capability works with the EBS CSI driver. Then, we walk through an example of seamlessly migrating to EBS gp3 volumes, a newer generation of general-purpose volumes that provides 20% lower price per GB than the gp2 volumes, and changing the performance characteristics, using Kubernetes native API via the EBS CSI driver. This enables customers to update volume properties directly using the CSI driver, using Kubernetes APIs without downtime of the applications, while the changes take effect.

Solution overview

In general quality-of-service related volume parameters throughput and input output per second (IOPS), as well as volume types, are parameters of the K8s StorageClass as described here. Amazon EBS CSI driver uses the following parameters as part of the StorageClass specification definition. Here is an example:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  csi.storage.k8s.io/fstype: xfs
  type: gp3
  iops: 5000
  throughput: 250 
  encrypted: "true"

These parameters of a StorageClass are immutable, such that neither PersistentVolume (PV) nor PVC are allowed to change them. Only the size of a PVC can be modified via spec.requests.resources.storage. That means to modify the K8s storage properties you have to do a storage migration by creating another StorageClass and migrating the PVC/PV which requires application downtime. This is a cumbersome and error-prone task especially given the fact that cloud storage providers usually allow online modification of volume parameters.

The EBS CSI driver contains two main components – the controller which is implemented as a K8s Deployment with two replicas and a node component which is a DaemonSet. The controller pods are then comprised of several sidecars. The new ModifyVolume feature is implemented as an additional sidecar, called “volumemodifier”. With volumemodifier sidecar, there is no need to change the IAM permissions for the controller component because it already utilizes the AWS EC2 ModifyVolume API call for PersistentVolumeClaim (PVC) resize which is a standard K8s feature.

Prerequisites

The new feature is available starting with EBS CSI version v1.19.0, which is used by Helm chart 2.19.0 or EKS managed add-on v1.19.0-eksbuild.2, but the feature will not be enabled by default.

Self-managed configuration using Helm

You have to opt-in by either modifying the Helm chart “values.yaml” file as follows:

# modify EBS CSI Helm chart
$ cat values.yaml
...
controller:
...
  volumeModificationFeature:
    enabled: true
...

# apply changes according to your Helm chart and repository names, for example
$ helm upgrade --install aws-ebs-csi-driver --namespace kube-system \
   aws-ebs-csi-driver/aws-ebs-csi-driver -f values.yaml

EKS managed add-on

Use EKS API CreateAddon, DescribeAddonConfiguration and UpdateAddon to modify the add-on configuration.

Note: The following output use the “jq” utility to enable a better visualization of the EBS CSI driver configuration schema. Jq has to be installed separately.

Here you can see sample outputs from modifying an existing EBS CSI add-on in a demo cluster called “tf-git-eks-demo-ipv4”.

# list all configured EKS add-ons
$ aws eks list-addons --cluster-name tf-git-eks-demo-ipv4
{
    "addons": [
        "kube-proxy",
        "vpc-cni",
        "aws-ebs-csi-driver"
    ]
}

# describe the currently installed EBS CSI driver add-on including addon version
$ aws eks describe-addon --cluster-name tf-git-eks-demo-ipv4 --addon-name aws-ebs-csi-driver
{
    "addon": {
        "addonName": "aws-ebs-csi-driver",
        "clusterName": "tf-git-eks-demo-ipv4",
        "status": "ACTIVE",
        "addonVersion": "v1.19.0-eksbuild.1",
…
}

# describe the available EBS CSI driver add-on versions for EKS 1.27
$ aws eks describe-addon-versions --addon-name aws-ebs-csi-driver --kubernetes-version 1.27 
{
    "addons": [
        {
            "addonName": "aws-ebs-csi-driver",
            "type": "storage",
            "addonVersions": [
                {
                    "addonVersion": "v1.19.0-eksbuild.2",
                    "architecture": [
                        "amd64",
                        "arm64"
                    ],
                    "compatibilities": [
                        {
                            "clusterVersion": "1.27",
                            "platformVersions": [
                                "*"
                            ],
                            "defaultVersion": false
                        }
                    ],
                    "requiresConfiguration": false
                },
…
}

# describe the EBS CSI driver add-on configuration and parse the configurationSchema attribute
$ aws eks describe-addon-configuration --addon-name aws-ebs-csi-driver --addon-version v1.19.0-eksbuild.2 --query configurationSchema --output text | jq
{
…
  "additionalProperties": false,
  "description": "Configurable parameters of the AWS EBS CSI Driver",
..
        "volumeModificationFeature": {
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "default": false,
              "description": "Enable modification of volume type, iops, etc via volume-modifier-for-k8s sidecar",
              "type": "boolean"
            }
          },
          "type": "object"
…
}

# create a customized configuration file to enable the "volumeModificationFeature" feature
$ cat aws-ebs-csi-addon-config.json
{
  "controller": {
    "volumeModificationFeature": {
       "enabled": true,
    },
  }
}

# update the EBS CSI driver add-on
$ aws eks update-addon --cluster-name tf-git-eks-demo-ipv4 --addon-name aws-ebs-csi-driver --addon-version v1.19.0-eksbuild.2 --configuration-values 'file://aws-ebs-csi-addon-config.json'
{
    "update": {
…
        "status": "InProgress",
        "type": "AddonUpdate",
        "params": [
            {
                "type": "AddonVersion",
                "value": "v1.19.0-eksbuild.2"
            },
            {
                "type": "ConfigurationValues",
                "value": "{\n  \"controller\": {\n    \"volumeModificationFeature\": {\n       \"enabled\": true,\n    },\n  }\n}"
            }
        ],
}

Here you can see example outputs of creating the EBS CSI add-on with the ModifyVolume feature enabled in a demo cluster called “tf-git-eks-demo-ipv4”:

# create the EBS CSI driver add-on
$ aws eks create-addon --cluster-name tf-git-eks-demo-ipv4 --addon-name aws-ebs-csi-driver --addon-version v1.19.0-eksbuild.2 --configuration-values 'file://aws-ebs-csi-addon-config.json'
{
        "addonName": "aws-ebs-csi-driver",
        "clusterName": " tf-git-eks-demo-ipv4 ",
        "status": "CREATING",
        "addonVersion": "v1.19.0-eksbuild.2",
…
    }
}

Depending on your environment the controller now contains 6 (without usage of external-snapshotter sidecar) or 7 sidecar containers:

$ kubectl get pod -n kube-system -l=app=ebs-csi-controller
NAME                                  READY   STATUS    RESTARTS   AGE
ebs-csi-controller-794d6dfc54-l5qwq   7/7     Running   0          20m
ebs-csi-controller-794d6dfc54-q2wmk   7/7     Running   0          20m

$ kubectl get pod -n kube-system ebs-csi-controller-794d6dfc54-l5qwq -o jsonpath='{.spec.containers[*].name}{"\n"}'
ebs-plugin csi-provisioner csi-attacher csi-snapshotter volumemodifier csi-resizer liveness-probe

Walkthrough

The ModifyVolume feature is implemented by annotating the PVC with 3 possible annotations, two of them change the so-called quality-of-service parameters volume throughput and volume IOPS, one modifies the EBS volume type.

Here is an example of possible annotations:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
  annotations:
    "ebs.csi.aws.com/volumeType": "gp3"
    "ebs.csi.aws.com/iops": "5000"
    "ebs.csi.aws.com/throughput": "250"
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-sc
  resources:
    requests:
      storage: 4Gi

The annotations will be reconciled with the AWS EC2 API for new and existing PVC i.e. one can now change the EBS volume type of an existing PVC.

In this section, we walk through the following three real-word examples using the ModifyVolume feature in EBS CSI driver:

  1. Volume type change
  2. IOPS modification
  3. Multiple modifications in one step

Note: Be aware that there is a limitation of one volume change per six hours according to EBS documentation. Either use different EBS volumes or apply one change with multiple annotations to an EBS volume. Otherwise, you will get an error message similar to the following example output:

$ kubectl describe pvc -n prometheus prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0
...
Events:
  Type     Reason                     Age                From                                     Message
  ----     ------                     ----               ----                                     -------
    Warning  VolumeModificationFailed   17s                volume-modifier-for-k8s-ebs.csi.aws.com  rpc error: code = Internal desc = Could not modify volume "vol-0a57fe49357e9effc": unable to modify AWS volume "vol-0a57fe49357e9effc": VolumeModificationRateExceeded: You've reached the maximum modification rate per volume limit. Wait at least 6 hours between modifications per EBS volume.
           status code: 400, request id: 37938e79-6eca-42c4-afdb-28bed041adaf

Volume type change

  1. In the first example we are going to migrate an in-tree provisioned gp2 based EBS volume to gp3. The PVC was initially created by in-tree provisioner based StorageClass gp2 and migrated to EBS CSI driver and is of EBS volume type gp2 which can be seen in the following outputs:
    $ kubectl get sc
    NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
    gp2 (default)   kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  8d
    
    $ kubectl get pvc redis-data-redis-master-0
    NAME                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    redis-data-redis-master-0     Bound    pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e   8Gi        RWO            gp2            5d22h
    
    # get the underlying PV
    $ kubectl get pv pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
    NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                               STORAGECLASS   REASON   AGE
    pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e   8Gi        RWO            Delete           Bound    default/redis-data-redis-master-0   gp2                     5d22h
    
    # get details about the PVC
    $ kubectl get pvc redis-data-redis-master-0 -o yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      annotations:
        pv.kubernetes.io/bind-completed: "yes"
        pv.kubernetes.io/bound-by-controller: "yes"
        volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
        volume.kubernetes.io/selected-node: ip-<redacted>.eu-west-1.compute.internal
        volume.kubernetes.io/storage-provisioner: ebs.csi.aws.com
    ...
      finalizers:
      - kubernetes.io/pvc-protection
      labels:
        app.kubernetes.io/component: master
        app.kubernetes.io/instance: redis
        app.kubernetes.io/name: redis
      name: redis-data-redis-master-0
      namespace: default
    ...
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 8Gi
      storageClassName: gp2
      volumeMode: Filesystem
      volumeName: pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
    status:
      accessModes:
      - ReadWriteOnce
      capacity:
        storage: 8Gi
      phase: Bound
    
    # get details about the underlying PV  
    $ kubectl get pv pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e -o yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      annotations:
        pv.kubernetes.io/migrated-to: ebs.csi.aws.com
        pv.kubernetes.io/provisioned-by: kubernetes.io/aws-ebs
    ...
      finalizers:
      - kubernetes.io/pv-protection
      - external-attacher/ebs-csi-aws-com
      labels:
        topology.kubernetes.io/region: eu-west-1
        topology.kubernetes.io/zone: eu-west-1b
      name: pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
    ...
    spec:
      accessModes:
      - ReadWriteOnce
      awsElasticBlockStore:
        fsType: ext4
        volumeID: vol-041a6c12bcc9e4cfc
      capacity:
        storage: 8Gi
      claimRef:
        apiVersion: v1
        kind: PersistentVolumeClaim
        name: redis-data-redis-master-0
        namespace: default
    ...
      nodeAffinity:
        required:
          nodeSelectorTerms:
          - matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values:
              - eu-west-1b
            - key: topology.kubernetes.io/region
              operator: In
              values:
              - eu-west-1
      persistentVolumeReclaimPolicy: Delete
      storageClassName: gp2
      volumeMode: Filesystem
    status:
      phase: Bound
    
    # get details about the corresponding AWS EBS object
    $ aws ec2 describe-volumes --volume-ids vol-041a6c12bcc9e4cfc
    {
        "Volumes": [
            {
                "Attachments": [
                    {
                        "AttachTime": "2023-05-08T07:56:33+00:00",
                        "Device": "/dev/xvdaa",
                        "InstanceId": "i-<redacted>",
                        "State": "attached",
                        "VolumeId": "vol-041a6c12bcc9e4cfc",
                        "DeleteOnTermination": false
                    }
                ],
                "AvailabilityZone": "eu-west-1b",
                "CreateTime": "2023-05-03T08:39:41.629000+00:00",
                "Encrypted": false,
                "Size": 8,
                "SnapshotId": "",
                "State": "in-use",
                "VolumeId": "vol-041a6c12bcc9e4cfc",
                "Iops": 100,
    ...
                "VolumeType": "gp2",
                "MultiAttachEnabled": false
            }
        ]
    }
  2. Here we imperatively change the PVC by adding the annotation for volume type modification ebs.csi.aws.com/volumeType=”gp3″:
    $ kubectl annotate pvc redis-data-redis-master-0 ebs.csi.aws.com/volumeType="gp3"
    persistentvolumeclaim/redis-data-redis-master-0 annotate
    
    $ kubectl get pvc redis-data-redis-master-0 -o yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      annotations:
        ebs.csi.aws.com/volumeType: gp3
        pv.kubernetes.io/bind-completed: "yes"
        pv.kubernetes.io/bound-by-controller: "yes"
        volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
        ...
  3. Using the kubectl describe call for the PVC we are able to easily see the modification in the “Events” section of the output:
    $ kubectl describe pvc redis-data-redis-master-0
    ..
    Events:
      Type    Reason                        Age   From                                     Message
      ----    ------                        ----  ----                                     -------
      Normal  VolumeModificationStarted     21s   volume-modifier-for-k8s-ebs.csi.aws.com  External modifier is modifying volume pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
      Normal  VolumeModificationSuccessful  10s   volume-modifier-for-k8s-ebs.csi.aws.com  External modifier has successfully modified volume pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
  4. The modification will be reconciled with the AWS EC2 (EBS) API and is visible with the following AWS CLI command:
    $ aws ec2 describe-volumes --volume-ids vol-041a6c12bcc9e4cfc
    {
        "Volumes": [
            {
      ...
                "VolumeId": "vol-041a6c12bcc9e4cfc",
                "Iops": 3000,
     ...
                "VolumeType": "gp3",
                "MultiAttachEnabled": false,
                "Throughput": 125
            }
        ]
    }
  5. For more information on how to monitor this progress, see the documentation on monitoring the progress of volume modifications.
    Note: The underlying PV will be annotated automatically with the same annotations applied to the PVC:

    # get details about the underlying PV  
    $ kubectl get pv pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e -o yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      annotations:
        ebs.csi.aws.com/volumeType: gp3
        pv.kubernetes.io/migrated-to: ebs.csi.aws.com
        pv.kubernetes.io/provisioned-by: kubernetes.io/aws-ebs
    …

IOPS modification

  1. In the second example we are modifying the IOPS of an EBS CSI based volume:
    $ kubectl get pvc -n prometheus prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0 -o yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      annotations:
        pv.kubernetes.io/bind-completed: "yes"
        pv.kubernetes.io/bound-by-controller: "yes"
        volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
        volume.kubernetes.io/selected-node: ip-<redacted>.eu-west-1.compute.internal
        volume.kubernetes.io/storage-provisioner: ebs.csi.aws.com
    ...
      finalizers:
      - kubernetes.io/pvc-protection
    ...
      name: prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0
      namespace: prometheus
    ...
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 50Gi
      storageClassName: gp3
      volumeMode: Filesystem
      volumeName: pvc-8a920f77-0187-45ba-bbff-cc4fb749480b
    status:
      accessModes:
      - ReadWriteOnce
      capacity:
        storage: 50Gi
      phase: Bound
    
    $ kubectl get pv pvc-8a920f77-0187-45ba-bbff-cc4fb749480b -o yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      annotations:
        pv.kubernetes.io/provisioned-by: ebs.csi.aws.com
    ...
      finalizers:
      - kubernetes.io/pv-protection
      - external-attacher/ebs-csi-aws-com
      name: pvc-8a920f77-0187-45ba-bbff-cc4fb749480b
    ...
    spec:
      accessModes:
      - ReadWriteOnce
      capacity:
        storage: 50Gi
      claimRef:
        apiVersion: v1
        kind: PersistentVolumeClaim
        name: prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0
        namespace: prometheus
    ...
      csi:
        driver: ebs.csi.aws.com
        fsType: ext4
        volumeAttributes:
          storage.kubernetes.io/csiProvisionerIdentity: 1683103179760-8081-ebs.csi.aws.com
        volumeHandle: vol-0a57fe49357e9effc
      nodeAffinity:
        required:
          nodeSelectorTerms:
          - matchExpressions:
            - key: topology.ebs.csi.aws.com/zone
              operator: In
              values:
              - eu-west-1a
      persistentVolumeReclaimPolicy: Delete
      storageClassName: gp3
      volumeMode: Filesystem
    status:
      phase: Bound
    
    $ aws ec2 describe-volumes --volume-ids vol-0a57fe49357e9effc
    {
        "Volumes": [
            {
                "Attachments": [
                    {
                        "AttachTime": "2023-05-09T07:06:52+00:00",
                        "Device": "/dev/xvdaa",
                        "InstanceId": "i-<redacted>",
                        "State": "attached",
                        "VolumeId": "vol-0a57fe49357e9effc",
                        "DeleteOnTermination": false
                    }
                ],
                "AvailabilityZone": "eu-west-1a",
                "CreateTime": "2023-05-03T08:39:43.260000+00:00",
                "Encrypted": true,
                "KmsKeyId": "arn:aws:kms:eu-west-1:<redacted>:key/<redacted>",
                "Size": 50,
                "SnapshotId": "",
                "State": "in-use",
                "VolumeId": "vol-0a57fe49357e9effc",
                "Iops": 3000,
    ...
                "VolumeType": "gp3",
                "MultiAttachEnabled": false,
                "Throughput": 125
            }
        ]
    }
  2. This time we apply the annotation for volume IOPS modification ebs.csi.aws.com/iops=3500:
    $ kubectl annotate pvc -n prometheus  prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0 ebs.csi.aws.com/iops=3500
    persistentvolumeclaim/prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0 annotate
    
    $ kubectl get pvc -n prometheus prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0 -o yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      annotations:
        ebs.csi.aws.com/iops: "3500"
        pv.kubernetes.io/bind-completed: "yes"
        pv.kubernetes.io/bound-by-controller: "yes"
        volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
        ...

    Note: K8s annotations can be managed declaratively in K8s YAML manifests as well allowing these changes to be applied via Infrastructure as Code (IaC) or GitOps tools like Terraform, ArgoCD or Flux.

  3. Again, using the kubectl describe call for the PVC we are able to easily see the modification in the Events section of the output:
    $ kubectl describe pvc -n prometheus  prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0
    ..
    Events:
      Type    Reason                        Age   From                                     Message
      ----    ------                        ----  ----                                     -------
      Normal  VolumeModificationStarted     16s   volume-modifier-for-k8s-ebs.csi.aws.com  External modifier is modifying volume pvc-8a920f77-0187-45ba-bbff-cc4fb749480b
      Normal  VolumeModificationSuccessful  10s   volume-modifier-for-k8s-ebs.csi.aws.com  External modifier has successfully modified volume pvc-8a920f77-0187-45ba-bbff-cc4fb749480b
  4. The AWS CLI describe-voume call shows the expected change:
    $ aws ec2 describe-volumes --volume-ids vol-0a57fe49357e9effc
    {
        "Volumes": [
    ...
                "AvailabilityZone": "eu-west-1a",
                "CreateTime": "2023-05-03T08:39:43.260000+00:00",
                "Encrypted": true,
                "KmsKeyId": "arn:aws:kms:eu-west-1:<redacted>:key/<redacted>",
                "Size": 50,
                "SnapshotId": "",
                "State": "in-use",
                "VolumeId": "vol-0a57fe49357e9effc",
                "Iops": 3500,
    ...
                "MultiAttachEnabled": false,
                "Throughput": 125
            }
        ]
    }

Multiple modifications in one step

  1. In the last example we are modifying PVC “redis-data-redis-master-0” again. This time we are going to modify the volume type and IOPS :
    $ kubectl get pvc redis-data-redis-master-0
    NAME                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    redis-data-redis-master-0   Bound    pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e   8Gi        RWO            gp2            13d
    
    $ kubectl describe pvc redis-data-redis-master-0
    Name:          redis-data-redis-master-0
    Namespace:     default
    StorageClass:  gp2
    Status:        Bound
    Volume:        pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
    Labels:        app.kubernetes.io/component=master
                   app.kubernetes.io/instance=redis
                   app.kubernetes.io/name=redis
    Annotations:   ebs.csi.aws.com/volumeType: gp3
                   pv.kubernetes.io/bind-completed: yes
    ...
  2. Because this PVC already contains an annotation “ebs.csi.aws.com/volumeType“ we have to use the “–overwrite” flag. In addition, we have to apply both annotations at once to call the AWS EC2 ModifyVolume API with both changes to avoid running into “maximum modification rate per volume limit of 6 hours”.
    $ kubectl annotate pvc redis-data-redis-master-0  ebs.csi.aws.com/volumeType="io2" ebs.csi.aws.com/iops=4000 --overwrite
    persistentvolumeclaim/redis-data-redis-master-0 annotate
    
    $ k describe pvc redis-data-redis-master-0
    Name:          redis-data-redis-master-0
    Namespace:     default
    StorageClass:  gp2
    Status:        Bound
    Volume:        pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
    Labels:        app.kubernetes.io/component=master
                   app.kubernetes.io/instance=redis
                   app.kubernetes.io/name=redis
    Annotations:   ebs.csi.aws.com/iops: 4000
                   ebs.csi.aws.com/volumeType: io2
                   pv.kubernetes.io/bind-completed: yes
                   pv.kubernetes.io/bound-by-controller: yes
                   volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
                   volume.kubernetes.io/selected-node: ip-<redacted>.eu-west-1.compute.internal
                   volume.kubernetes.io/storage-provisioner: ebs.csi.aws.com
    Finalizers:    [kubernetes.io/pvc-protection]
    Capacity:      8Gi
    Access Modes:  RWO
    VolumeMode:    Filesystem
    Used By:       redis-master-0
    Events:
      Type     Reason                       Age   From                                     Message
      ----     ------                       ----  ----                                     -------
      Normal   VolumeModificationStarted    5s    volume-modifier-for-k8s-ebs.csi.aws.com  External modifier is modifying volume pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e
      Normal   VolumeModificationSuccessful 2s   volume-modifier-for-k8s-ebs.csi.aws.com  External modifier has successfully modified volume pvc-c06e7ecd-c4c8-46d1-801a-471c2541ad6e 
    
    $ aws ec2 describe-volumes --volume-ids vol-041a6c12bcc9e4cfc
    {
        "Volumes": [
            {
    ...
                "Size": 8,
                "SnapshotId": "",
                "State": "in-use",
                "VolumeId": "vol-041a6c12bcc9e4cfc",
                "Iops": 4000,
    ...
                "VolumeType": "io2",
                "MultiAttachEnabled": false
            }
        ]
    }  

    Note: Be aware that for io2 based EBS volumes there is a maximum ratio of 500:1 for IOPS to size.

Recommendation

Even if ModifyVolume feature is now available, a good practice is to create an additional EBS CSI based storage class for gp3 EBS volume type and annotate it as the default StorageClass:

$ kubectl get sc
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2 (default)   kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  8d
gp3             ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   8d

$ kubectl get sc gp2 -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
...
  name: gp2
...
  fsType: ext4
  type: gp2
provisioner: kubernetes.io/aws-ebs
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

$ kubectl get sc gp3 -o yaml
allowVolumeExpansion: true
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
...
  name: gp3
...
parameters:
  encrypted: "true"
  kmsKeyId: arn:aws:kms:<redacted:key/<redacted>
  type: gp3
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

# note the trailing "-" to remove the "default" annotation
$ kubectl annotate sc gp2 storageclass.kubernetes.io/is-default-class-
storageclass.storage.k8s.io/gp2 annotate

# annotate the gp3 storageclass as the default one
$ kubectl annotate sc gp3 storageclass.kubernetes.io/is-default-class=true
storageclass.storage.k8s.io/gp3 annotate

$ kubectl get sc gp3 -o yaml
allowVolumeExpansion: true
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
...

$ kubectl get sc
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2             kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  8d
gp3 (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   8d

Conclusion

In this blog post, we’ve explored a new solution that simplifies volume migration and modifications using the EBS CSI Driver. As the default storage driver for EKS, our mission is to make the CSI driver as user-friendly as possible. The new addition of the ModifyVolume capability makes it simpler for customers migrating from gp2 to gp3 volumes and adjusting performance metrics like IOPS and throughput, using only native K8s APIs and configurations. This development makes it easier for them to right size their storage performance and cost.

To get started, follow the EBS CSI user guide and ModifyVolume example on GitHub. To learn more, you can visit the Amazon EBS product pageAmazon EKS product page, and the open source project on Github.

Thank you for reading this post. If you have any comments or questions, then don’t hesitate to leave them in the comments section.

Jens-Uwe Walther

Jens-Uwe Walther

Jens-Uwe Walther is a Senior Specialist Technical Account Manager for Containers at Amazon Web Services with 28 years of professional experience in IT based in Jena, Germany. He is passionate about Kubernetes and helping customers building container solutions on Amazon EKS. In his free time, he loves playing guitar, taking photographs and traveling with his wife into the wilderness.

Arne Knoeller

Arne Knoeller

Arne Knoeller is a Principal Solutions Architect at Amazon Web Services (AWS). In his role, Arne helps strategic AWS customers to solve critical business challenges, using AWS cloud technologies and services. He is a subject matter expert in cloud transformations, building resilient architecture designs and cost optimization. Before joining AWS, Arne has worked 10+ years in different roles and companies in the SAP eco system.

Kevin Liu

Kevin Liu

Kevin Liu is a Sr. Product Manager on the EBS team at AWS. He advocates for the needs of customers running containerized applications on AWS and EBS. Outside of work, he loves spending time with his family, traveling the world, and trying out new beers.