aws-amicleaner 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,11 +3,15 @@ To clean up your AWS AMIs:
3
3
  2. Exclude AMIs in use, younger than N days, or the newest N images.
4
4
  3. Manually confirm the list of AMIs for deletion.
5
5
 
6
+ ## Requirements
7
+
8
+ Requires Node.js >= 18.
9
+
6
10
  ## Examples
7
11
 
8
12
  To delete all AMIs in eu-west-1 where the name starts with amiprefix-, are older than 5 days, and not the newest 3 images, run:
9
13
  ```bash
10
- aws-amicleaner --region eu-west-1 --include-name 'amiprefix-*' --exclude-newest 3 --exclude-days 5 --exclude-in-use --verbose
14
+ npx aws-amicleaner --region eu-west-1 --include-name 'amiprefix-*' --exclude-newest 3 --exclude-days 5 --exclude-in-use --verbose
11
15
  ```
12
16
 
13
17
  A typical confirmation screen:
@@ -35,17 +39,17 @@ Do you want to continue and remove 6 AMIs [y/N] ? :
35
39
 
36
40
  To delete all AMIs in eu-west-* (eu-west-1, eu-west-2, eu-west-3) tagged with CostCenter=X342-*-1111, are older than 7 days (default), are not the newest 5 images (default), and are not in use (default), run:
37
41
  ```bash
38
- aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111'
42
+ npx aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111'
39
43
  ```
40
44
 
41
45
  Run the command without confirmation (useful in scripts):
42
46
  ```bash
43
- aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111' --force-delete
47
+ npx aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111' --force-delete
44
48
  ```
45
49
 
46
50
  To disable the defaults, run:
47
51
  ```bash
48
- aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0 --no-exclude-in-use --no-verbose
52
+ npx aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0 --no-exclude-in-use --no-verbose
49
53
  ```
50
54
 
51
55
  ## Arguments
@@ -64,7 +68,7 @@ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0
64
68
  --exclude-days EXCLUDEDAYS
65
69
  Exclude AMIs from deletion that are younger than N days
66
70
  --exclude-in-use, --no-exclude-in-use
67
- Exclude AMIs from deletion that are in use by EC2 instances, ASGs, Launch Configurations, and Launch Templates (default: true)
71
+ Exclude AMIs from deletion that are in use by EC2 instances, Launch Configurations, and Launch Templates (default: true)
68
72
  -f, --force-delete, --no-force-delete
69
73
  Skip confirmation before deletion (default: false)
70
74
  --verbose, --no-verbose
@@ -0,0 +1,25 @@
1
+ import {defineConfig} from 'eslint/config';
2
+ import globals from 'globals';
3
+ import js from '@eslint/js';
4
+
5
+ export default defineConfig([{
6
+ files: ['**/*.js'],
7
+ plugins: {
8
+ js,
9
+ },
10
+ extends: ['js/recommended'],
11
+ languageOptions: {
12
+ globals: {
13
+ ...globals.commonjs,
14
+ ...globals.node,
15
+ ...globals.mocha
16
+ },
17
+ ecmaVersion: 2022,
18
+ },
19
+ rules: {
20
+ indent: ['error', 2],
21
+ 'linebreak-style': ['error', 'unix'],
22
+ quotes: ['error', 'single'],
23
+ semi: ['error', 'always'],
24
+ },
25
+ }]);
package/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { createInterface } = require('node:readline');
4
- const { ArgumentParser, BooleanOptionalAction } = require('argparse');
5
- const { fetchAMIs, deleteAMI, fetchRegions} = require('./lib.js');
6
- const pLimit = require('p-limit');
7
-
8
- const AWS = require('aws-sdk');
9
- const PrettyTable = require('./prettytable.js');
3
+ import {createInterface} from 'node:readline';
4
+ import {ArgumentParser, BooleanOptionalAction} from 'argparse';
5
+ import {fetchAMIs, deleteAMI, fetchRegions} from './lib.js';
6
+ import pLimit from 'p-limit';
7
+ import {AutoScalingClient} from '@aws-sdk/client-auto-scaling';
8
+ import {EC2Client} from '@aws-sdk/client-ec2';
9
+ import PrettyTable from './prettytable.js';
10
10
 
11
11
  const rl = createInterface({
12
12
  input: process.stdin,
@@ -36,13 +36,13 @@ async function run({
36
36
  const autoscaling = {};
37
37
  const ec2Client = (region) => {
38
38
  if (!(region in ec2)) {
39
- ec2[region] = new AWS.EC2({apiVersion: '2016-11-15', region});
39
+ ec2[region] = new EC2Client({apiVersion: '2016-11-15', region});
40
40
  }
41
41
  return ec2[region];
42
42
  };
43
43
  const autoscalingClient = (region) => {
44
44
  if (!(region in autoscaling)) {
45
- autoscaling[region] = new AWS.AutoScaling({apiVersion: '2011-01-01', region});
45
+ autoscaling[region] = new AutoScalingClient({apiVersion: '2011-01-01', region});
46
46
  }
47
47
  return autoscaling[region];
48
48
  };
@@ -119,7 +119,7 @@ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0
119
119
  parser.add_argument('--include-tag-value', {dest: 'includeTagValue', type: 'string', help: 'The tag value (for the tag key) that must be present, wildcard * supported'});
120
120
  parser.add_argument('--exclude-newest', {dest: 'excludeNewest', type: 'int', default: 5, help: 'Exclude the newest N AMIs'});
121
121
  parser.add_argument('--exclude-days', {dest: 'excludeDays', type: 'int', default: 7, help: 'Exclude AMIs from deletion that are younger than N days'});
122
- parser.add_argument('--exclude-in-use', {dest: 'excludeInUse', default: true, action: BooleanOptionalAction, help: 'Exclude AMIs from deletion that are in use by EC2 instances, ASGs, Launch Configurations, and Launch Templates'});
122
+ parser.add_argument('--exclude-in-use', {dest: 'excludeInUse', default: true, action: BooleanOptionalAction, help: 'Exclude AMIs from deletion that are in use by EC2 instances, Launch Configurations, and Launch Templates'});
123
123
  parser.add_argument('-f', '--force-delete', {dest: 'forceDelete', default: false, action: BooleanOptionalAction, help: 'Skip confirmation before deletion'});
124
124
  parser.add_argument('--verbose', {dest: 'verbose', default: true, action: BooleanOptionalAction, help: 'Display additional information'});
125
125
  const args = parser.parse_args();
package/lib.js CHANGED
@@ -1,17 +1,17 @@
1
- const wildcard = require('wildcard');
2
- const pLimit = require('p-limit');
3
-
4
- const MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE = 50;
1
+ import wildcard from 'wildcard';
2
+ import pLimit from 'p-limit';
3
+ import {paginateDescribeLaunchConfigurations} from '@aws-sdk/client-auto-scaling';
4
+ import {DescribeRegionsCommand, paginateDescribeInstances, DescribeLaunchTemplateVersionsCommand, paginateDescribeImages, DeregisterImageCommand, DeleteSnapshotCommand, paginateDescribeLaunchTemplates} from '@aws-sdk/client-ec2';
5
5
 
6
6
  function mapAMI(raw) {
7
7
  return {
8
8
  id: raw.ImageId,
9
9
  name: raw.Name,
10
10
  creationDate: Date.parse(raw.CreationDate),
11
- tags: raw.Tags.reduce((acc, {Key: key, Value: value}) => {
11
+ tags: Array.isArray(raw.Tags) ? raw.Tags.reduce((acc, {Key: key, Value: value}) => {
12
12
  acc[key] = value;
13
13
  return acc;
14
- }, {}),
14
+ }, {}) : {},
15
15
  blockDeviceMappings: raw.BlockDeviceMappings.filter(raw => raw.Ebs).map(raw => ({snapshotId: raw.Ebs.SnapshotId})),
16
16
  excluded: false,
17
17
  excludeReasons: [],
@@ -20,7 +20,7 @@ function mapAMI(raw) {
20
20
  };
21
21
  }
22
22
 
23
- async function fetchRegions(ec2, rawRegions) {
23
+ export async function fetchRegions(ec2, rawRegions) {
24
24
  const regions = new Set();
25
25
 
26
26
  if (rawRegions.length === 0) {
@@ -31,7 +31,7 @@ async function fetchRegions(ec2, rawRegions) {
31
31
 
32
32
  const rawRegionsWithWildcard = rawRegions.filter(region => region.includes('*'));
33
33
  if (rawRegionsWithWildcard.length !== 0) {
34
- const {Regions} = await ec2.describeRegions({}).promise();
34
+ const {Regions} = await ec2.send(new DescribeRegionsCommand({}));
35
35
  rawRegionsWithWildcard.forEach(rawRegionWithWildcard => {
36
36
  wildcard(rawRegionWithWildcard, Regions.map(r => r.RegionName)).forEach(region => regions.add(region));
37
37
  });
@@ -39,100 +39,68 @@ async function fetchRegions(ec2, rawRegions) {
39
39
 
40
40
  return regions;
41
41
  }
42
- exports.fetchRegions = fetchRegions;
43
42
 
44
- async function fetchInUseAMIIDs(ec2, autoscaling) {
43
+ export async function fetchInUseAMIIDs(ec2, autoscaling) {
45
44
  const inUseAMIIDs = new Set();
46
45
 
47
- for await (const reservation of (async function*() {
48
- let nextToken = '';
49
- while (nextToken !== undefined) {
50
- const {Reservations: reservations, NextToken} = await ec2.describeInstances({
51
- NextToken: (nextToken === '') ? undefined : nextToken,
52
- Filters: [{
53
- Name: 'instance-state-name',
54
- Values: [
55
- 'pending',
56
- 'running',
57
- 'shutting-down',
58
- 'stopping',
59
- 'stopped'
60
- ]
61
- }]
62
- }).promise();
63
- yield* reservations;
64
- nextToken = NextToken;
46
+ const instancePaginator = paginateDescribeInstances({
47
+ client: ec2
48
+ }, {
49
+ Filters: [{
50
+ Name: 'instance-state-name',
51
+ Values: [
52
+ 'pending',
53
+ 'running',
54
+ 'shutting-down',
55
+ 'stopping',
56
+ 'stopped'
57
+ ]
58
+ }]
59
+ });
60
+ for await (const page of instancePaginator) {
61
+ for (const reservation of page.Reservations) {
62
+ reservation.Instances.forEach(instance => inUseAMIIDs.add(instance.ImageId));
65
63
  }
66
- })()) {
67
- reservation.Instances.forEach(instance => inUseAMIIDs.add(instance.ImageId));
68
64
  }
69
65
 
70
- const asgs = [];
71
- for await (const asg of (async function*() {
72
- let nextToken = '';
73
- while (nextToken !== undefined) {
74
- const {AutoScalingGroups, NextToken} = await autoscaling.describeAutoScalingGroups({
75
- NextToken: (nextToken === '') ? undefined : nextToken
76
- }).promise();
77
- yield* AutoScalingGroups;
78
- nextToken = NextToken;
79
- }
80
- })()) {
81
- asgs.push(asg);
66
+ const lcPaginator = paginateDescribeLaunchConfigurations({client: autoscaling}, {});
67
+ for await (const page of lcPaginator) {
68
+ page.LaunchConfigurations.forEach(lc => inUseAMIIDs.add(lc.ImageId));
82
69
  }
83
70
 
84
- // in use by ASG -> Launch Configuration
85
- const inUseLCNames = asgs.filter(asg => 'LaunchConfigurationName' in asg).map(asg => asg.LaunchConfigurationName);
86
- if (inUseLCNames.length > 0) {
87
- for (let i = 0; i < Math.ceil(inUseLCNames.length/MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE); i++) {
88
- const {LaunchConfigurations: lcs} = await autoscaling.describeLaunchConfigurations({
89
- LaunchConfigurationNames: inUseLCNames.slice(i*MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE, (i+1)*MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE)
90
- }).promise();
91
- lcs.forEach(lc => inUseAMIIDs.add(lc.ImageId));
92
- }
71
+ const ltPaginator = paginateDescribeLaunchTemplates({client: ec2}, {});
72
+ const ltLimit = pLimit(5);
73
+ for await (const page of ltPaginator) {
74
+ await Promise.all(
75
+ page.LaunchTemplates.map(({LaunchTemplateId: id, DefaultVersionNumber: version}) => ltLimit(async () => {
76
+ const data = await ec2.send(new DescribeLaunchTemplateVersionsCommand({
77
+ LaunchTemplateId: id,
78
+ Versions: [version]
79
+ }));
80
+ inUseAMIIDs.add(data.LaunchTemplateVersions[0].LaunchTemplateData.ImageId);
81
+ }))
82
+ );
93
83
  }
94
84
 
95
- const inUseLTs = [
96
- ...asgs.filter(asg => 'LaunchTemplate' in asg).map(asg => ({id: asg.LaunchTemplate.LaunchTemplateId, version: asg.LaunchTemplate.Version})),
97
- ...asgs.filter(asg => 'MixedInstancesPolicy' in asg).map(asg => ({id: asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateId, version: asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.Version}))
98
- ];
99
- const limit = pLimit(5);
100
- await Promise.all(
101
- inUseLTs.map(({id, version}) => limit(() =>
102
- ec2.describeLaunchTemplateVersions({
103
- LaunchTemplateId: id,
104
- Versions: [version]
105
- }).promise().then(data => data.LaunchTemplateVersions[0].LaunchTemplateData.ImageId))
106
- )
107
- ).then(amiIDs => amiIDs.forEach(amiID => inUseAMIIDs.add(amiID)));
108
-
109
85
  return inUseAMIIDs;
110
86
  }
111
- exports.fetchInUseAMIIDs = fetchInUseAMIIDs;
112
87
 
113
- async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays) {
88
+ export async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays) {
114
89
  let amis = [];
115
- for await (const rawAMI of (async function*() {
116
- let nextToken = '';
117
- while (nextToken !== undefined) {
118
- const params = {
119
- Owners: ['self']
120
- };
121
- if (includeTagKey !== undefined) {
122
- params.Filters = [{
123
- Name: 'tag-key',
124
- Values: [includeTagKey]
125
- }];
126
- }
127
- if (nextToken !== '') {
128
- params.NextToken = nextToken;
129
- }
130
- const {Images, NextToken} = await ec2.describeImages(params).promise();
131
- yield* Images;
132
- nextToken = NextToken;
133
- }
134
- })()) {
135
- amis.push(mapAMI(rawAMI));
90
+ const input = {
91
+ Owners: ['self']
92
+ };
93
+ if (includeTagKey !== undefined) {
94
+ input.Filters = [{
95
+ Name: 'tag-key',
96
+ Values: [includeTagKey]
97
+ }];
98
+ }
99
+ const paginator = paginateDescribeImages({
100
+ client: ec2
101
+ }, input);
102
+ for await (const page of paginator) {
103
+ page.Images.forEach(rawAMI => amis.push(mapAMI(rawAMI)));
136
104
  }
137
105
 
138
106
  if (includeName !== undefined) {
@@ -186,18 +154,16 @@ async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, incl
186
154
 
187
155
  return amis;
188
156
  }
189
- exports.fetchAMIs = fetchAMIs;
190
157
 
191
- async function deleteAMI(ec2, ami) {
192
- await ec2.deregisterImage({
158
+ export async function deleteAMI(ec2, ami) {
159
+ await ec2.send(new DeregisterImageCommand({
193
160
  ImageId: ami.id
194
- }).promise();
161
+ }));
195
162
  console.log(`AMI ${ami.id} deregistered`);
196
163
  for (const blockDevice of ami.blockDeviceMappings) {
197
- await ec2.deleteSnapshot({
164
+ await ec2.send(new DeleteSnapshotCommand({
198
165
  SnapshotId: blockDevice.snapshotId
199
- }).promise();
166
+ }));
200
167
  console.log(`snapshot ${blockDevice.snapshotId} of AMI ${ami.id} deleted`);
201
168
  }
202
169
  }
203
- exports.deleteAMI = deleteAMI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-amicleaner",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "To clean up your AWS AMIs: First, include AMIs by name or tag. Second, exclude AMIs in use, younger than N days, or the newest N images. Third, manually confirm the list of AMIs for deletion.",
5
5
  "main": "index.js",
6
6
  "bin": "index.js",
@@ -18,15 +18,19 @@
18
18
  },
19
19
  "homepage": "https://github.com/widdix/aws-amicleaner#readme",
20
20
  "dependencies": {
21
+ "@aws-sdk/client-auto-scaling": "3.777.0",
22
+ "@aws-sdk/client-ec2": "3.777.0",
21
23
  "argparse": "2.0.1",
22
- "aws-sdk": "2.1387.0",
23
- "p-limit": "3.1.0",
24
+ "p-limit": "6.2.0",
24
25
  "wildcard": "2.0.1"
25
26
  },
26
27
  "devDependencies": {
27
- "c8": "7.13.0",
28
- "eslint": "8.41.0",
29
- "mocha": "10.2.0"
28
+ "@eslint/js": "9.23.0",
29
+ "aws-sdk-client-mock": "4.1.0",
30
+ "c8": "10.1.3",
31
+ "eslint": "9.23.0",
32
+ "globals": "16.0.0",
33
+ "mocha": "11.1.0"
30
34
  },
31
35
  "mocha": {
32
36
  "timeout": 10000
@@ -36,5 +40,6 @@
36
40
  "text",
37
41
  "html"
38
42
  ]
39
- }
43
+ },
44
+ "type": "module"
40
45
  }
package/prettytable.js CHANGED
@@ -37,7 +37,7 @@
37
37
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
38
  // SOFTWARE.
39
39
 
40
- var PrettyTable = function () {
40
+ export default function PrettyTable() {
41
41
  // Skeleton structure of table with list of column names, row and max width of each column element
42
42
  this.table = {
43
43
  'columnNames': [],
@@ -237,5 +237,3 @@ PrettyTable.prototype.deleteTable = function () {
237
237
  'maxWidth': []
238
238
  };
239
239
  };
240
-
241
- module.exports = PrettyTable;
package/renovate.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json"
3
+ }