diff --git a/.gitignore b/.gitignore index 7d91e78..6095e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ terraform.tfvars .terraform.lock.hcl .tmuxp.yaml + +# Ignore JS/TS dependencies installed with npm +node_modules diff --git a/cleanup-imported-ebs-snapshots/README.md b/cleanup-imported-ebs-snapshots/README.md index 7009ad4..6cb3419 100644 --- a/cleanup-imported-ebs-snapshots/README.md +++ b/cleanup-imported-ebs-snapshots/README.md @@ -29,15 +29,7 @@ Because the source code for the lambda function is written in a separate TypeScr npm install ``` -Then run the build after you modify the code of the lambda function inside of `src/index.ts`. - -```bash -npm run build -``` - -Then copy the output of thee JS file from the `dist/index.js` into the `cloudformation.json` file as the `InlineCode` property of the lambda function resource at the end of the template. Make sure to indent the code according to YAML syntax (the code must be two spaces deeper than the `InlineCode:` key). - -Now you can deploy the resulting stack with the following bash script. +Then run the following script to build the TypeScript code, modify the lambda function inside of `src/index.ts` and deploy a test CFN stack out of it. Make sure you have `yq` installed. ``` ./deploy.sh diff --git a/cleanup-imported-ebs-snapshots/cloudformation.yaml b/cleanup-imported-ebs-snapshots/cloudformation.yaml index c3eaf16..166922f 100644 --- a/cleanup-imported-ebs-snapshots/cloudformation.yaml +++ b/cleanup-imported-ebs-snapshots/cloudformation.yaml @@ -4,15 +4,14 @@ Parameters: KeepMaxAgeInDays: Type: Number Default: '7' - Description: > + Description: |- The maximum age of snapshots to keep in days. If a snapshot is older than this value, it will be deleted unless there it fits into {KeepMinAmount} of latest snapshots. - KeepMinAmount: Type: Number Default: '7' - Description: > + Description: |- The minimum amount of snapshots to keep for each volume even if they are older than {KeepMaxAgeInDays}. If there are less than this amount of snapshots for a volume, none of them will be deleted. Resources: @@ -23,18 +22,19 @@ Resources: Version: "2012-10-17" Statement: - Effect: Allow - Principal: { Service: [lambda.amazonaws.com] } - Action: [sts:AssumeRole] + Principal: + Service: [lambda.amazonaws.com] + Action: ['sts:AssumeRole'] Policies: - PolicyName: EBSSnashotsPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow - Action: [ec2:DescribeSnapshots] + Action: ['ec2:DescribeSnapshots'] Resource: "*" - Effect: Allow - Action: [ec2:DeleteSnapshot] + Action: ['ec2:DeleteSnapshot'] Resource: "*" Condition: StringLike: @@ -42,9 +42,8 @@ Resources: - Effect: Allow # We don't give the lambda a permission to create log groups # because we pre-create the log group ourselves - Action: [logs:CreateLogStream, logs:PutLogEvents] + Action: ['logs:CreateLogStream', 'logs:PutLogEvents'] Resource: arn:aws:logs:*:*:* - # The default log group that AWS Lambda creates has retention disabled. # We don't want to store logs indefinitely, so we create a custom log group with # retention enabled. @@ -55,7 +54,6 @@ Resources: # This lambda does destructive operations, so we want to keep the logs for a long time # to be safe and to be able to debug any issues if they happen. RetentionInDays: 180 - Lambda: Type: AWS::Serverless::Function # We want to create a custom log group with retention enabled first, @@ -64,7 +62,7 @@ Resources: Properties: FunctionName: elastio-cleanup-imported-ebs-snapshots Description: "A Lambda function to delete EBS snapshots tagged with elastio:imported-to-rp" - Runtime: nodejs18.x + Runtime: nodejs20.x Handler: index.handler Role: !GetAtt LambdaExecutionRole.Arn Events: @@ -72,16 +70,14 @@ Resources: Type: ScheduleV2 Properties: ScheduleExpression: rate(1 hour) - Environment: Variables: DELETE_BY_TAG_KEY: elastio:imported-to-rp KEEP_MAX_AGE_IN_DAYS: !Ref KeepMaxAgeInDays KEEP_MIN_AMOUNT: !Ref KeepMinAmount - # Change the code if the lambda in the `src/index.ts` file and then run `npm run build`. # After that, copy the content of the `dist/index.js` file and paste it here. - InlineCode: | + InlineCode: |- "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CleanupContext = exports.handler = void 0; @@ -93,9 +89,9 @@ Resources: const BROKEN_VOLUME_ID = 'vol-ffffffff'; const ec2 = new client_ec2_1.EC2Client({}); /** - * A handler that is invoked periodically based on an AWS Scheduler schedule - * and cleans up EBS snapshots that were already imported to Elastio. - */ + * A handler that is invoked periodically based on an AWS Scheduler schedule + * and cleans up EBS snapshots that were already imported to Elastio. + */ async function handler() { const now = new Date(); console.log(`Starting the cleanup relative to the current timestamp ${now.toISOString()}`); @@ -206,8 +202,8 @@ Resources: return new CleanupTotal(volumes, snapshots); } /** - * Retain only snapshots that need to be deleted - */ + * Retain only snapshots that need to be deleted + */ filterSnapshotsToDelete() { const toDeleteMap = new Map(); for (const [volumeId, toDelete] of this.snapshots) { diff --git a/cleanup-imported-ebs-snapshots/deploy.sh b/cleanup-imported-ebs-snapshots/deploy.sh index 42b18b4..3d93737 100755 --- a/cleanup-imported-ebs-snapshots/deploy.sh +++ b/cleanup-imported-ebs-snapshots/deploy.sh @@ -2,8 +2,16 @@ set -euo pipefail -project_dir=$(readlink -f $(dirname $0)) +cd "$(dirname "${BASH_SOURCE[0]}")" -aws cloudformation deploy --template-file $project_dir/cloudformation.yaml \ +npm run build + +code=$(cat dist/index.js) \ +yq --inplace '.Resources.Lambda.Properties.InlineCode = strenv(code)' \ + cloudformation.yaml + +set -x + +aws cloudformation deploy --template-file cloudformation.yaml \ --capabilities CAPABILITY_IAM \ --stack-name elastio-imported-ebs-snapshots-cleanup-stack diff --git a/cleanup-leftover-ebs-volumes/.gitignore b/cleanup-leftover-ebs-volumes/.gitignore new file mode 100644 index 0000000..b65beee --- /dev/null +++ b/cleanup-leftover-ebs-volumes/.gitignore @@ -0,0 +1,5 @@ + +# We don't lock the dependencies and use the version of AWS SDK installed in the +# managed AWS Lambda NodeJS runtime. However, we use a `package.json` to install +# some TypeScript types to assist with development. +package-lock.json diff --git a/cleanup-leftover-ebs-volumes/cleanup-elastio-ebs-vols.yaml b/cleanup-leftover-ebs-volumes/cleanup-elastio-ebs-vols.yaml index 0adf103..3ca4da1 100644 --- a/cleanup-leftover-ebs-volumes/cleanup-elastio-ebs-vols.yaml +++ b/cleanup-leftover-ebs-volumes/cleanup-elastio-ebs-vols.yaml @@ -6,59 +6,69 @@ Resources: Properties: FunctionName: EBSVolumeCleanupFunction Description: 'A Lambda function to delete un-attached EBS volumes older than 24 hours with the elastio:resource tag' - Runtime: nodejs16.x + Runtime: nodejs20.x Handler: index.handler Role: !GetAtt LambdaExecutionRole.Arn - InlineCode: | - const AWS = require('aws-sdk'); - const ec2 = new AWS.EC2(); + InlineCode: |- + // @ts-check + + const { EC2, paginateDescribeVolumes } = require('@aws-sdk/client-ec2'); + const ec2 = new EC2(); const isVolumeOlderThan24Hours = (createTime) => { - const currentTime = new Date(); - const volumeCreateTime = new Date(createTime); - const timeDifference = currentTime - volumeCreateTime; - const hoursDifference = timeDifference / (1000 * 60 * 60); - return hoursDifference > 24; + const currentTime = new Date(); + const volumeCreateTime = new Date(createTime); + const timeDifference = currentTime - volumeCreateTime; + const hoursDifference = timeDifference / (1000 * 60 * 60); + return hoursDifference > 24; }; - exports.handler = async (event) => { - try { - console.log("Getting list of unattached EBS volumes with elastio:resource tag"); - const { Volumes } = await ec2.describeVolumes({ - Filters: [ - { - Name: 'status', - Values: ['available'] - }, - { - Name: 'tag:elastio:resource', - Values: ['*'] + exports.handler = async (_event) => { + try { + console.log("Getting list of unattached EBS volumes with elastio:resource tag"); + + const volumesPaginator = paginateDescribeVolumes( + { + client: ec2, + pageSize: 500. + }, + { + Filters: [ + { + Name: 'status', + Values: ['available'] + }, + { + Name: 'tag:elastio:resource', + Values: ['*'] + } + ] + } + ); + + for await (const page of volumesPaginator) { + for (const volume of page.Volumes ?? []) { + if (isVolumeOlderThan24Hours(volume.CreateTime)) { + console.log(`Deleting unattached Elastio EBS volume ${volume.VolumeId} older than 24 hours`); + await ec2.deleteVolume({ VolumeId: volume.VolumeId }); + console.log(`EBS volume ${volume.VolumeId} deleted`); + } else { + console.log(`EBS volume ${volume.VolumeId} is not old enough; skipping it`); + } + } } - ] - }).promise(); - for (const volume of Volumes) { - if (isVolumeOlderThan24Hours(volume.CreateTime)) { - console.log("Deleting unattached Elastio EBS volume ${volume.VolumeId} older than 24 hours"); - await ec2.deleteVolume({ VolumeId: volume.VolumeId }).promise(); - console.log("EBS volume ${volume.VolumeId} deleted"); - } else { - console.log("EBS volume ${volume.VolumeId} is not old enough; skipping it"); - } + return { statusCode: 200, body: JSON.stringify({ message: 'EBS volumes cleanup successful' }) }; + } catch (error) { + console.error(error); + throw error; } - - return { statusCode: 200, body: JSON.stringify({ message: 'EBS volumes cleanup successful' }) }; - } catch (error) { - console.error(error); - throw error; - } }; Events: Schedule1: Type: Schedule Properties: Schedule: 'rate(1 hour)' - LambdaExecutionRole: Type: 'AWS::IAM::Role' Properties: @@ -94,7 +104,6 @@ Resources: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' - EBSVolumeCleanupLogGroup: Type: 'AWS::Logs::LogGroup' Properties: diff --git a/cleanup-leftover-ebs-volumes/deploy.sh b/cleanup-leftover-ebs-volumes/deploy.sh new file mode 100755 index 0000000..da3af9a --- /dev/null +++ b/cleanup-leftover-ebs-volumes/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# This is a script that updates the labmda source code in the CFN template from the +# lambda.js file where it's easier to edit and maintain. Then it deploys the CFN +# stack with the updated lambda code for testing. +# +# You need yq and AWS CLI set up. + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +code=$(cat lambda.js) \ +yq --inplace '.Resources.EBSVolumeCleanupFunction.Properties.InlineCode = strenv(code)' \ + cleanup-elastio-ebs-vols.yaml + +set -x + +aws cloudformation deploy --template-file cleanup-elastio-ebs-vols.yaml \ + --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ + --stack-name elastio-cleanup-elastio-ebs-volumes diff --git a/cleanup-leftover-ebs-volumes/lambda.js b/cleanup-leftover-ebs-volumes/lambda.js new file mode 100644 index 0000000..af7cc3d --- /dev/null +++ b/cleanup-leftover-ebs-volumes/lambda.js @@ -0,0 +1,54 @@ +// @ts-check + +const { EC2, paginateDescribeVolumes } = require('@aws-sdk/client-ec2'); +const ec2 = new EC2(); + +const isVolumeOlderThan24Hours = (createTime) => { + const currentTime = new Date(); + const volumeCreateTime = new Date(createTime); + const timeDifference = currentTime - volumeCreateTime; + const hoursDifference = timeDifference / (1000 * 60 * 60); + return hoursDifference > 24; +}; + +exports.handler = async (_event) => { + try { + console.log("Getting list of unattached EBS volumes with elastio:resource tag"); + + const volumesPaginator = paginateDescribeVolumes( + { + client: ec2, + pageSize: 500. + }, + { + Filters: [ + { + Name: 'status', + Values: ['available'] + }, + { + Name: 'tag:elastio:resource', + Values: ['*'] + } + ] + } + ); + + for await (const page of volumesPaginator) { + for (const volume of page.Volumes ?? []) { + if (isVolumeOlderThan24Hours(volume.CreateTime)) { + console.log(`Deleting unattached Elastio EBS volume ${volume.VolumeId} older than 24 hours`); + await ec2.deleteVolume({ VolumeId: volume.VolumeId }); + console.log(`EBS volume ${volume.VolumeId} deleted`); + } else { + console.log(`EBS volume ${volume.VolumeId} is not old enough; skipping it`); + } + } + } + + return { statusCode: 200, body: JSON.stringify({ message: 'EBS volumes cleanup successful' }) }; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/cleanup-leftover-ebs-volumes/package.json b/cleanup-leftover-ebs-volumes/package.json new file mode 100644 index 0000000..3fcd939 --- /dev/null +++ b/cleanup-leftover-ebs-volumes/package.json @@ -0,0 +1,7 @@ +{ + "name": "cleanup-leftover-ebs-volumes", + "version": "1.0.0", + "dependencies": { + "@aws-sdk/client-ec2": "3.561" + } +}