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 +9 -5
- package/eslint.config.js +25 -0
- package/index.js +10 -10
- package/lib.js +61 -95
- package/package.json +12 -7
- package/prettytable.js +1 -3
- package/renovate.json +3 -0
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,
|
|
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
|
package/eslint.config.js
ADDED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
71
|
-
for await (const
|
|
72
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
158
|
+
export async function deleteAMI(ec2, ami) {
|
|
159
|
+
await ec2.send(new DeregisterImageCommand({
|
|
193
160
|
ImageId: ami.id
|
|
194
|
-
})
|
|
161
|
+
}));
|
|
195
162
|
console.log(`AMI ${ami.id} deregistered`);
|
|
196
163
|
for (const blockDevice of ami.blockDeviceMappings) {
|
|
197
|
-
await ec2.
|
|
164
|
+
await ec2.send(new DeleteSnapshotCommand({
|
|
198
165
|
SnapshotId: blockDevice.snapshotId
|
|
199
|
-
})
|
|
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.
|
|
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
|
-
"
|
|
23
|
-
"p-limit": "3.1.0",
|
|
24
|
+
"p-limit": "6.2.0",
|
|
24
25
|
"wildcard": "2.0.1"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
-
|
|
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