In this blog post I am going over how to automate the conversion of EBS volumes from the GP2 type to GP3 by using CloudWatch Events to trigger a Lambda function upon volume creation ensuring that new volumes are automatically optimized.

For a 2000 GB volume with approximate usage of 12 hours in a month, the charges for GP3 will be $2.667 and for GP2 it will be $3.33. Reference: EBS-Pricing

In addition to cost, other advantages include higher baseline performance, higher throughput, more flexibility in performance tuning.

  • Create a basic Lambda function in AWS named ebs_volume_check:
$ aws lambda list-functions
{
    "Functions": [
        {
            "FunctionName": "ebs_volume_check",
            "FunctionArn": "arn:aws:lambda:us-east-2:<REDACTED>:function:ebs_volume_check",
            "Runtime": "python3.12",
            "Role": "arn:aws:iam::<REDACTED>:role/service-role/ebs_volume_check-role-epi8gi8n",
            "Handler": "lambda_function.lambda_handler",
            "CodeSize": 262,
            "Description": "",
            "Timeout": 3,
            "MemorySize": 128,
            "LastModified": "2024-04-04T20:58:19.000+0000",
            "CodeSha256": "yXyGKJIc05IjQ67u3bnJGmNr4RJZRezt3nAfhsy2BE8=",
            "Version": "$LATEST",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "1950ea64-40a8-42a4-82a7-eca9a59e5e81",
            "PackageType": "Zip",
            "Architectures": [
                "x86_64"
            ],
            "EphemeralStorage": {
                "Size": 512
            },
            "SnapStart": {
                "ApplyOn": "None",
                "OptimizationStatus": "Off"
            }
        }
    ]
}

Python Script lambda_function that prints the event:

import json
def lambda_handler(event, context):
    
    print(event)
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
  • Create a rule in CloudWatch that will trigger the Lambda function:
$ aws events list-rules
{
    "Rules": [
        {
            "Name": "EBS_Volume_Notification",
            "Arn": "arn:aws:events:us-east-2:<REDACTED>:rule/EBS_Volume_Notification",
            "EventPattern": "{\"source\":[\"aws.ec2\"],\"detail-type\":[\"EBS Volume Notification\"],\"detail\":{\"event\":[\"createVolume\"]}}",
            "State": "ENABLED",
            "Description": "EBS_Volume_Notification",
            "EventBusName": "default"
        }
    ]
}

  • Verification (To check if cloudwatch triggers the lambda function when new volume is created):

Created a test gp2 type EBS Volume:

$ aws ec2 describe-volumes --volume-ids vol-0617133f2c7829c2c
{
    "Volumes": [
        {
            "Attachments": [],
            "AvailabilityZone": "us-east-2a",
            "CreateTime": "2024-04-04T20:59:28.263Z",
            "Encrypted": false,
            "Size": 1,
            "SnapshotId": "",
            "State": "available",
            "VolumeId": "vol-0617133f2c7829c2c",
            "Iops": 100,
            "VolumeType": "gp2",
            "MultiAttachEnabled": false
        }
    ]
}

Show log group:

$ aws logs describe-log-groups
{
    "logGroups": [
        {
            "logGroupName": "/aws/lambda/ebs_volume_check",
            "creationTime": 1712164400141,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:us-east-2:<REDACTED>:log-group:/aws/lambda/ebs_volume_check:*",
            "storedBytes": 0
        }
    ]
}

Find the logStream names for the logGroup:

$ aws logs describe-log-streams --log-group-name /aws/lambda/ebs_volume_check

Get the generated log events for the latest logStreamName:

$ aws logs get-log-events --log-group-name /aws/lambda/ebs_volume_check --log-stream-name='2024/04/04/[$LATEST]a6749b74000b40a6b80a87e51d0ec42b'
{
    "events": [
...
        {
            "timestamp": 1712264369854,
            "message": "{'version': '0', 'id': '7029ca50-18e3-38f9-0ee6-e293a446feba', 'detail-type': 'EBS Volume Notification', 'source': 'aws.ec2', 'account': '<REDACTED>', 'time': '2024-04-04T20:59:29Z', 'region': 'us-east-2', 
            'resources': ['arn:aws:ec2:us-east-2:<REDACTED>:volume/vol-0617133f2c7829c2c'], 'detail': {'result': 'available', 'cause': '', 
            'event': 'createVolume', 'request-id': '710509c3-2753-4541-aa7b-82df0cf4a255'}}\n","ingestionTime": 1712264378870
        },
        ],
}
  • Now, let’s modify the Lambda function ebs_volume_check to convert the volume type from gp2 to gp3.

Updated lambda_function script:

import json
import boto3
import botocore

# Get Volume ID
def get_volume_id(volume_arn):
    volume_arn_split = volume_arn.split(':')
    volume_id = volume_arn_split[-1].split('/')[-1]
    return volume_id

# Main handler function
def lambda_handler(event, context):
    # Get Volume ARN from the event
    volume_arn = event['resources'][0]
    if not volume_arn:
        print("No volume ARN found in event.")
        return False
        
    # Get Volume ID from the ARN
    volume_id = get_volume_id(volume_arn)
    if not volume_id:
        print("Failed to get volume ID from ARN {}.".format(volume_arn))
        return False
    
    while True:
        client = boto3.client('ec2')    # Instantiating boto3 client
        try:
            describe_response = client.describe_volumes(VolumeIds=[volume_id])
            volume_state = describe_response['Volumes'][0]['State']
            volume_type = describe_response['Volumes'][0]['VolumeType']
            print("Volume {} is currently in {} state with type {}.".format(volume_id, volume_state, volume_type))
            # Check to verify the volume state and type
            if volume_type == 'gp2':
                if volume_state == 'available':
                    try:
                        client.modify_volume(
                            VolumeId=volume_id,
                            VolumeType='gp3',
                        )
                        print("Volume conversion to type gp3 initiated.")
                        break
                    except botocore.exceptions.ClientError as ex:
                        print("Failed to modify volume {} to type gp3: {}.".format(volume_id, ex))
                        continue
                else:
                    print("Volume {} is not in available state.".format(volume_id))
            else:
                print("Volume {} type is NOT gp2. Exiting.".format(volume_id))
                break
        except botocore.exceptions.ClientError as ex:
            print("Failed to describe volume {}: {}".format(volume_id, ex))
            continue

Before testing out the workflow end to end, we need to add a policy to modify the volume to the IAM role associated with the Lambda function.

$ aws iam list-attached-role-policies --role-name ebs_volume_check-role-epi8gi8n
{
    "AttachedPolicies": [
        {
            "PolicyName": "AWSLambdaBasicExecutionRole-0eecf21e-e002-4849-a294-de15f32f1310",
            "PolicyArn": "arn:aws:iam::<REDACTED>:policy/service-role/AWSLambdaBasicExecutionRole-0eecf21e-e002-4849-a294-de15f32f1310"
        }
    ]
}
$ aws iam get-role-policy --role-name ebs_volume_check-role-epi8gi8n --policy-name ebs-volume-check-inline-policy
{
    "RoleName": "ebs_volume_check-role-epi8gi8n",
    "PolicyName": "ebs-volume-check-inline-policy",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "ec2:ModifyVolume",
                    "ec2:DescribeVolumes"
                ],
                "Resource": "*"
            }
        ]
    }
}

Now for the last part, let’s create a new gp2 type volume to check if the function execution takes place as expected:

New Log Stream output shows modify volume function execution took place:

$ aws logs get-log-events --log-group-name /aws/lambda/ebs_volume_check --log-stream-name='2024/04/05/[$LATEST]4f5f53d094a547ce854e69bf7da49455'
{
    "events": [
        {
            "timestamp": 1712333519847,
            "message": "INIT_START Runtime Version: python:3.12.v21\tRuntime Version ARN: arn:aws:lambda:us-east-2::runtime:0087788f33e3d6b95522422d734bb2b31308197021920fd844db09552d6fa015\n",
            "ingestionTime": 1712333523229
        },
...
        {
            "timestamp": 1712333579115,
            "message": "Volume vol-06a6259717c3e1b0a is currently in available state with type gp2.\n",
            "ingestionTime": 1712333585347
        },
        {
            "timestamp": 1712333579269,
            "message": "Volume conversion to type gp3 initiated.\n",
            "ingestionTime": 1712333585347
        },
        {
            "timestamp": 1712333579307,
            "message": "END RequestId: df2ad912-e225-4059-81f3-a3452cbeb8d5\n",
            "ingestionTime": 1712333585347
        },
}

Volume type now modified to gp3:

$ aws ec2 describe-volumes --volume-ids vol-06a6259717c3e1b0a{
    "Volumes": [
        {
            "Attachments": [],
            "AvailabilityZone": "us-east-2a",
            "CreateTime": "2024-04-05T16:11:58.246Z",
            "Encrypted": false,
            "Size": 2,
            "SnapshotId": "",
            "State": "available",
            "VolumeId": "vol-06a6259717c3e1b0a",
            "Iops": 3000,
            "VolumeType": "gp3",
            "MultiAttachEnabled": false,
            "Throughput": 125
        }
    ]
}

That’s it. Thank you for reading!