Skip to content

Commit da85e54

Browse files
authored
feat(cli): garbage collect ecr assets (under --unstable flag) (#31841)
Follow up to #31611 which introduced S3 Asset Garbage Collection ## ECR Asset Garbage Collection `cdk gc` now collects ECR assets. ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='ecr' ``` or ```bash cdk gc aws://0123456789012/us-east-1 \ --unstable='gc' \ --type='all' ``` all other options are duplicated from s3. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent cb3ecfe commit da85e54

File tree

11 files changed

+1075
-202
lines changed

11 files changed

+1075
-202
lines changed

packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable no-console */
2+
import * as assert from 'assert';
23
import * as fs from 'fs';
34
import * as os from 'os';
45
import * as path from 'path';
@@ -544,6 +545,17 @@ export class TestFixture extends ShellHelper {
544545
return JSON.parse(fs.readFileSync(templatePath, { encoding: 'utf-8' }).toString());
545546
}
546547

548+
public async bootstrapRepoName(): Promise<string> {
549+
await ensureBootstrapped(this);
550+
551+
const response = await this.aws.cloudFormation.send(new DescribeStacksCommand({}));
552+
553+
const stack = (response.Stacks ?? [])
554+
.filter((s) => s.StackName && s.StackName == this.bootstrapStackName);
555+
assert(stack.length == 1);
556+
return outputFromStack('ImageRepositoryName', stack[0]) ?? '';
557+
}
558+
547559
public get bootstrapStackName() {
548560
return this.fullStackName('bootstrap-stack');
549561
}
@@ -569,7 +581,7 @@ export class TestFixture extends ShellHelper {
569581
}
570582

571583
/**
572-
* Cleanup leftover stacks and buckets
584+
* Cleanup leftover stacks and bootstrapped resources
573585
*/
574586
public async dispose(success: boolean) {
575587
const stacksToDelete = await this.deleteableStacks(this.stackNamePrefix);

packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+14
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,19 @@ class DockerStack extends cdk.Stack {
606606
}
607607
}
608608

609+
class DockerInUseStack extends cdk.Stack {
610+
constructor(parent, id, props) {
611+
super(parent, id, props);
612+
613+
// Use the docker file in a lambda otherwise it will not be referenced in the template
614+
const fn = new lambda.Function(this, 'my-function', {
615+
code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker')),
616+
runtime: lambda.Runtime.FROM_IMAGE,
617+
handler: lambda.Handler.FROM_IMAGE,
618+
});
619+
}
620+
}
621+
609622
class DockerStackWithCustomFile extends cdk.Stack {
610623
constructor(parent, id, props) {
611624
super(parent, id, props);
@@ -814,6 +827,7 @@ switch (stackSet) {
814827
new EcsHotswapStack(app, `${stackPrefix}-ecs-hotswap`);
815828
new AppSyncHotswapStack(app, `${stackPrefix}-appsync-hotswap`);
816829
new DockerStack(app, `${stackPrefix}-docker`);
830+
new DockerInUseStack(app, `${stackPrefix}-docker-in-use`);
817831
new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`);
818832

819833
new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, {

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/garbage-collection.integtest.ts

+195-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { BatchGetImageCommand, ListImagesCommand, PutImageCommand } from '@aws-sdk/client-ecr';
12
import { GetObjectTaggingCommand, ListObjectsV2Command, PutObjectTaggingCommand } from '@aws-sdk/client-s3';
23
import { integTest, randomString, withoutBootstrap } from '../../lib';
34

5+
const S3_ISOLATED_TAG = 'aws-cdk:isolated';
6+
const ECR_ISOLATED_TAG = 'aws-cdk.isolated';
7+
48
jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime
59

610
integTest(
7-
'Garbage Collection deletes unused assets',
11+
'Garbage Collection deletes unused s3 objects',
812
withoutBootstrap(async (fixture) => {
913
const toolkitStackName = fixture.bootstrapStackName;
1014
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`;
@@ -50,7 +54,50 @@ integTest(
5054
);
5155

5256
integTest(
53-
'Garbage Collection keeps in use assets',
57+
'Garbage Collection deletes unused ecr images',
58+
withoutBootstrap(async (fixture) => {
59+
const toolkitStackName = fixture.bootstrapStackName;
60+
61+
await fixture.cdkBootstrapModern({
62+
toolkitStackName,
63+
});
64+
65+
const repoName = await fixture.bootstrapRepoName();
66+
67+
await fixture.cdkDeploy('docker-in-use', {
68+
options: [
69+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
70+
'--toolkit-stack-name', toolkitStackName,
71+
'--force',
72+
],
73+
});
74+
fixture.log('Setup complete!');
75+
76+
await fixture.cdkDestroy('docker-in-use', {
77+
options: [
78+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
79+
'--toolkit-stack-name', toolkitStackName,
80+
'--force',
81+
],
82+
});
83+
84+
await fixture.cdkGarbageCollect({
85+
rollbackBufferDays: 0,
86+
type: 'ecr',
87+
bootstrapStackName: toolkitStackName,
88+
});
89+
fixture.log('Garbage collection complete!');
90+
91+
// assert that the bootstrap repository is empty
92+
await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName }))
93+
.then((result) => {
94+
expect(result.imageIds).toEqual([]);
95+
});
96+
}),
97+
);
98+
99+
integTest(
100+
'Garbage Collection keeps in use s3 objects',
54101
withoutBootstrap(async (fixture) => {
55102
const toolkitStackName = fixture.bootstrapStackName;
56103
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`;
@@ -97,7 +144,50 @@ integTest(
97144
);
98145

99146
integTest(
100-
'Garbage Collection tags unused assets',
147+
'Garbage Collection keeps in use ecr images',
148+
withoutBootstrap(async (fixture) => {
149+
const toolkitStackName = fixture.bootstrapStackName;
150+
151+
await fixture.cdkBootstrapModern({
152+
toolkitStackName,
153+
});
154+
155+
const repoName = await fixture.bootstrapRepoName();
156+
157+
await fixture.cdkDeploy('docker-in-use', {
158+
options: [
159+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
160+
'--toolkit-stack-name', toolkitStackName,
161+
'--force',
162+
],
163+
});
164+
fixture.log('Setup complete!');
165+
166+
await fixture.cdkGarbageCollect({
167+
rollbackBufferDays: 0,
168+
type: 'ecr',
169+
bootstrapStackName: toolkitStackName,
170+
});
171+
fixture.log('Garbage collection complete!');
172+
173+
// assert that the bootstrap repository is empty
174+
await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName }))
175+
.then((result) => {
176+
expect(result.imageIds).toHaveLength(1);
177+
});
178+
179+
await fixture.cdkDestroy('docker-in-use', {
180+
options: [
181+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
182+
'--toolkit-stack-name', toolkitStackName,
183+
'--force',
184+
],
185+
});
186+
}),
187+
);
188+
189+
integTest(
190+
'Garbage Collection tags unused s3 objects',
101191
withoutBootstrap(async (fixture) => {
102192
const toolkitStackName = fixture.bootstrapStackName;
103193
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`;
@@ -142,11 +232,62 @@ integTest(
142232
const tags = await fixture.aws.s3.send(new GetObjectTaggingCommand({ Bucket: bootstrapBucketName, Key: key }));
143233
expect(tags.TagSet).toHaveLength(1);
144234
});
235+
236+
await fixture.cdkDestroy('lambda', {
237+
options: [
238+
'--context', `bootstrapBucket=${bootstrapBucketName}`,
239+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
240+
'--toolkit-stack-name', toolkitStackName,
241+
'--force',
242+
],
243+
});
244+
}),
245+
);
246+
247+
integTest(
248+
'Garbage Collection tags unused ecr images',
249+
withoutBootstrap(async (fixture) => {
250+
const toolkitStackName = fixture.bootstrapStackName;
251+
252+
await fixture.cdkBootstrapModern({
253+
toolkitStackName,
254+
});
255+
256+
const repoName = await fixture.bootstrapRepoName();
257+
258+
await fixture.cdkDeploy('docker-in-use', {
259+
options: [
260+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
261+
'--toolkit-stack-name', toolkitStackName,
262+
'--force',
263+
],
264+
});
265+
fixture.log('Setup complete!');
266+
267+
await fixture.cdkDestroy('docker-in-use', {
268+
options: [
269+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
270+
'--toolkit-stack-name', toolkitStackName,
271+
'--force',
272+
],
273+
});
274+
275+
await fixture.cdkGarbageCollect({
276+
rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them)
277+
type: 'ecr',
278+
bootstrapStackName: toolkitStackName,
279+
});
280+
fixture.log('Garbage collection complete!');
281+
282+
await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName }))
283+
.then((result) => {
284+
expect(result.imageIds).toHaveLength(2); // the second tag comes in as a second 'id'
285+
});
145286
}),
146287
);
147288

148289
integTest(
149-
'Garbage Collection untags in-use assets',
290+
'Garbage Collection untags in-use s3 objects',
150291
withoutBootstrap(async (fixture) => {
151292
const toolkitStackName = fixture.bootstrapStackName;
152293
const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`;
@@ -175,7 +316,7 @@ integTest(
175316
Key: key,
176317
Tagging: {
177318
TagSet: [{
178-
Key: 'aws-cdk:isolated',
319+
Key: S3_ISOLATED_TAG,
179320
Value: '12345',
180321
}, {
181322
Key: 'bogus',
@@ -200,3 +341,52 @@ integTest(
200341
}]);
201342
}),
202343
);
344+
345+
integTest(
346+
'Garbage Collection untags in-use ecr images',
347+
withoutBootstrap(async (fixture) => {
348+
const toolkitStackName = fixture.bootstrapStackName;
349+
350+
await fixture.cdkBootstrapModern({
351+
toolkitStackName,
352+
});
353+
354+
const repoName = await fixture.bootstrapRepoName();
355+
356+
await fixture.cdkDeploy('docker-in-use', {
357+
options: [
358+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
359+
'--toolkit-stack-name', toolkitStackName,
360+
'--force',
361+
],
362+
});
363+
fixture.log('Setup complete!');
364+
365+
// Artificially add tagging to the asset in the bootstrap bucket
366+
const imageIds = await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName }));
367+
const digest = imageIds.imageIds![0].imageDigest;
368+
const imageManifests = await fixture.aws.ecr.send(new BatchGetImageCommand({ repositoryName: repoName, imageIds: [{ imageDigest: digest }] }));
369+
const manifest = imageManifests.images![0].imageManifest;
370+
await fixture.aws.ecr.send(new PutImageCommand({ repositoryName: repoName, imageManifest: manifest, imageDigest: digest, imageTag: `0-${ECR_ISOLATED_TAG}-12345` }));
371+
372+
await fixture.cdkGarbageCollect({
373+
rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them)
374+
type: 'ecr',
375+
bootstrapStackName: toolkitStackName,
376+
});
377+
fixture.log('Garbage collection complete!');
378+
379+
await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName }))
380+
.then((result) => {
381+
expect(result.imageIds).toHaveLength(1); // the second tag has been removed
382+
});
383+
384+
await fixture.cdkDestroy('docker-in-use', {
385+
options: [
386+
'--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`,
387+
'--toolkit-stack-name', toolkitStackName,
388+
'--force',
389+
],
390+
});
391+
}),
392+
);

packages/aws-cdk/README.md

+20-11
Original file line numberDiff line numberDiff line change
@@ -896,28 +896,37 @@ CDK Garbage Collection.
896896

897897
> [!CAUTION]
898898
> CDK Garbage Collection is under development and therefore must be opted in via the `--unstable` flag: `cdk gc --unstable=gc`.
899-
>
900-
> [!WARNING]
901-
> `cdk gc` currently only supports garbage collecting S3 Assets. You must specify `cdk gc --unstable=gc --type=s3` as ECR asset garbage collection has not yet been implemented.
902899
903-
`cdk gc` garbage collects unused S3 assets from your bootstrap bucket via the following mechanism:
900+
`cdk gc` garbage collects unused assets from your bootstrap bucket via the following mechanism:
904901

905902
- for each object in the bootstrap S3 Bucket, check to see if it is referenced in any existing CloudFormation templates
906903
- if not, it is treated as unused and gc will either tag it or delete it, depending on your configuration.
907904

905+
The high-level mechanism works identically for unused assets in bootstrapped ECR Repositories.
906+
908907
The most basic usage looks like this:
909908

909+
```console
910+
cdk gc --unstable=gc
911+
```
912+
913+
This will garbage collect all unused assets in all environments of the existing CDK App.
914+
915+
To specify one type of asset, use the `type` option (options are `all`, `s3`, `ecr`):
916+
910917
```console
911918
cdk gc --unstable=gc --type=s3
912919
```
913920

914-
This will garbage collect S3 assets from the current bootstrapped environment(s) and immediately delete them. Note that, since the default bootstrap S3 Bucket is versioned, object deletion will be handled by the lifecycle
921+
Otherwise `cdk gc` defaults to collecting assets in both the bootstrapped S3 Bucket and ECR Repository.
922+
923+
`cdk gc` will garbage collect S3 and ECR assets from the current bootstrapped environment(s) and immediately delete them. Note that, since the default bootstrap S3 Bucket is versioned, object deletion will be handled by the lifecycle
915924
policy on the bucket.
916925

917926
Before we begin to delete your assets, you will be prompted:
918927

919928
```console
920-
cdk gc --unstable=gc --type=s3
929+
cdk gc --unstable=gc
921930

922931
Found X objects to delete based off of the following criteria:
923932
- objects have been isolated for > 0 days
@@ -926,11 +935,11 @@ Found X objects to delete based off of the following criteria:
926935
Delete this batch (yes/no/delete-all)?
927936
```
928937

929-
Since it's quite possible that the bootstrap bucket has many objects, we work in batches of 1000 objects. To skip the
930-
prompt either reply with `delete-all`, or use the `--confirm=false` option.
938+
Since it's quite possible that the bootstrap bucket has many objects, we work in batches of 1000 objects or 100 images.
939+
To skip the prompt either reply with `delete-all`, or use the `--confirm=false` option.
931940

932941
```console
933-
cdk gc --unstable=gc --type=s3 --confirm=false
942+
cdk gc --unstable=gc --confirm=false
934943
```
935944

936945
If you are concerned about deleting assets too aggressively, there are multiple levers you can configure:
@@ -946,7 +955,7 @@ When using `created-buffer-days`, we simply filter out any assets that have not
946955
of days.
947956

948957
```console
949-
cdk gc --unstable=gc --type=s3 --rollback-buffer-days=30 --created-buffer-days=1
958+
cdk gc --unstable=gc --rollback-buffer-days=30 --created-buffer-days=1
950959
```
951960

952961
You can also configure the scope that `cdk gc` performs via the `--action` option. By default, all actions
@@ -957,7 +966,7 @@ are performed, but you can specify `print`, `tag`, or `delete-tagged`.
957966
- `delete-tagged` deletes assets that have been tagged for longer than the buffer days, but does not tag newly unused assets.
958967

959968
```console
960-
cdk gc --unstable=gc --type=s3 --action=delete-tagged --rollback-buffer-days=30
969+
cdk gc --unstable=gc --action=delete-tagged --rollback-buffer-days=30
961970
```
962971

963972
This will delete assets that have been unused for >30 days, but will not tag additional assets.

packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Tag } from '../../cdk-toolkit';
22

33
export const BUCKET_NAME_OUTPUT = 'BucketName';
4-
export const REPOSITORY_NAME_OUTPUT = 'RepositoryName';
4+
export const REPOSITORY_NAME_OUTPUT = 'ImageRepositoryName';
55
export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName';
66
export const BOOTSTRAP_VERSION_OUTPUT = 'BootstrapVersion';
77
export const BOOTSTRAP_VERSION_RESOURCE = 'CdkBootstrapVersion';

0 commit comments

Comments
 (0)