This repo holds bash scripts I used to report on ECR images and then delete them. They're a bit crude, but get the job done!
- AWS CLI tool - I used
brew install awsfor Bluefin - Suite of CLI utils, mainly
jq - AWS account that you are logged into and has access to ECR
To get a report of all images, call:
./aws_ecr_stats.shThis will generate two cache directories cache_private and cache_private. From the JSON files in there, it will output one table for PRIVATE and one for PUBLIC image stats in your ECR repo:
PUBLIC_REPOSITORY SIZE IMAGE_COUNT TAGGED/UNTAGGED
bar-quux-nouveau <no-images> 0 0/0
bar-smang-healthcheck <no-images> 0 0/0
foo/bar-healthcheck <no-images> 0 0/0
foo/curator 40.82MB 1 1/0
foo/http-redirector 53.81MB 1 1/0
foo/bar-mcp-server 53.86MB 1 1/0
foo/ci-images 78.62MB 1 1/0
bar-interop/mediator 88.78MB 1 1/0
foo/auto-ssh 232.85MB 6 3/3
foo/volume-monitor 354.26MB 1 1/0
foo/corge 405.79MB 2 2/0
foo/bar-smang/healthcheck 406.86MB 9 9/0
bar-interop/configurator 474.85MB 1 1/0
foo/devops 532.01MB 1 1/0
foo/bar-listener 609.64MB 1 1/0
foo/foo-impact 852.95MB 4 2/2
foo/bar-upgrade-service 2.68GB 19 17/2
foo/grault 3.65GB 32 32/0
foo/baz 7.10GB 9 1/8
foo/moh-quuux-dashboard 12.14GB 40 38/2
foo/foo-os 14.27GB 95 81/14
foo/bar-quux-nouveau 25.62GB 177 59/118
foo/bar 446.87GB 25176 12245/12931
foo/bar-smang-healthcheck 677.46GB 25229 12292/12937
foo/bar-quux 2.82TB 25373 12269/13104
foo/bar-sentinel 3.10TB 25472 12306/13166
foo/bar-api 3.38TB 25494 12312/13182
foo/bar-smang 3.99TB 25267 12323/12944
----------------------------- -------- -------- --------
TOTAL 14.45TB 152413 74000/78413If for any reason this process gets interrupted and you have empty cache files, be sure to delete all the files in cache_private and cache_private and re-run the script.
Now that you have the cache_private and cache_private filled with JSON files, you can explore them with grep and jq. For example, I wanted generate a CSV list of all images that were created more than a month ago. Today was 2026-06-11, so this is aprox 30 days:
jq -r \
'.imageDetails[] | select(.imageTags == null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-smang.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' After verifying this is correct, I can then save this CSV for further processing and run it for a number or repos saving to the same file:
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-smang.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' >> tagged.images.to.delete.csv
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-api.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' >> tagged.images.to.delete.csv
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-quux.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' >> tagged.images.to.delete.csvOptionally, you may want to keep images that are not a branch. For those that follow SemVer, this regex should work [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}. Adding it in the jq mix per above, it looks like:
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-smang.json \
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' \
|grep -vE '"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{3,20}"' \
>> tagged.images.to.delete.csv
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-api.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' \
| grep -vE '"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{3,20}"' \
>> tagged.images.to.delete.csv
jq -r \
'.imageDetails[] | select(.imageTags != null) | [.imagePushedAt, .imageDigest, .repositoryName, .imageTags[]?]|@csv' foo_bar-quux.json\
| sort | grep -vE '2026-06-|2026-05-3|2026-05-2' \
| grep -vE '"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{3,20}"' \
>> tagged.images.to.delete.csvNB - I found that I would get an error when deleting untagged images that were used in tagged images (and couldn't easy tell when there was a dependency):
{
"imageIds": [],
"failures": [
{
"imageId": {
"imageDigest": "sha256:0008d08a3b8903e403c15474b77b16e1c2cd7068c17b5a8e1b49c83517c3eee2"
},
"failureCode": "ImageReferencedByManifestList",
"failureReason": "Requested image referenced by manifest list: [sha256:2c2163ff123133cc36f7ab5d1df2e30b39feccf20d42fb823ed468b35ac2d629]"
}
]
}To get around this I generated a list of tagged images select(.imageTags != null) first and then a list of untagged images select(.imageTags == null). I could then delete all the tagged images first, and then delete all the untagged images.
To backup your releases, you should hard code all your repositories and release numbers into the batch.delete.images.sh script and run it. It will call docker pull for each of your repositories and releases and cache it locally. You'll need to have enough disk space to do this.
You'll likely get errors from pulling too many images too fast, so log into ECR so docker can make authenticated calls to avoid the rate limitting errors:
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/medicDelete the cached credentials when you're done backing up - they're stored in plaintext.
Be sure to verify the CSV files first! - Even though you backed up your releases, be sure to verify the images and CSV and hashes are correct before deleting.
After you have verified and backed up, you can delete the images with the delete script. It's dependent on CSV files with the format from the jq calls above which looks like this:
"2022-11-03T15:44:49-07:00","sha256:eee7ea954a41ab93fe5f66f7006f39ab4d94955a31d9a3df180cd64cb120c6d8","foo/bar-quux"
"2022-11-08T09:56:50-08:00","sha256:9871dced96f088ffcfdcaa5dd1d4e0405359990bd75aba8961c0014bf84c2394","foo/bar-quux"
"2022-11-08T21:43:05-08:00","sha256:92c2759c660cd33108dbbdb7151586994851c42108e8f59de9b83bc85e617e2d","foo/bar-quux"The script accepts the name of the CSV file and public or private as input:
./batch.delete.images.sh CSV_FILE_NAME PUBLIC_PRIVATE > LOG_FILEFor example:
./batch.delete.images.sh tagged.images.to.delete.csv private > tagged.images.to.delete.logBe sure to > the output to a log file, as the aws ecr batch-delete-image command outputs a bunch of JSON that you should save in case images fail to delete. That way you'll have the imageDigest hashes to debug and re-process as needed:
If you think the repo exists, but you're getting RepositoryNotFoundException errors (see below), check that you called with the correct public or private flag.
aws: [ERROR]: An error occurred (RepositoryNotFoundException) when calling the BatchDeleteImage operation: The repository with name 'cht-api' does not exist in the registry with id '123456789012345'If you get an error like this when deleting tagged images:
{
"imageIds": [
{
"imageDigest": "sha256:404719106333411ddb04ffb33e27fb8c515e6d58da09026ea50f3f25cc1c821c"
}
],
"failures": [
{
"imageId": {
"imageDigest": "sha256:79ef40190ca521dbff988da81608db3e187add13eebacab0f5f1f28b92bdc226"
},
"failureCode": "ImageReferencedByManifestList",
"failureReason": "Requested image referenced by manifest list: [sha256:3046381e02fbf5cd1d7c59ba8d9c0ef0d604926369b6b2bee2964b02b56526cb]"
},
{
"imageId": {
"imageDigest": "sha256:1e71a852ee8a12f6fcbc564d0d39c7da8a3e5a5c16178e2a7f7be57fed68a673"
},
"failureCode": "ImageReferencedByManifestList",
"failureReason": "Requested image referenced by manifest list: [sha256:3046381e02fbf5cd1d7c59ba8d9c0ef0d604926369b6b2bee2964b02b56526cb]"
}
]
}You can try to re-run the script in hopes that the image manifests using this image were deleted. Alternately, you can call aws ecr batch-get-image to research which image it is. Again, this is why it's critical to save the output of the delete command in a file.
To check how many images you delete/space you cleared up, you can move the cached JSON files mv cache_private cache_privateBAK and mv cache_private cache_privateBAK and rerun the ./aws_ecr_stats.sh script. Compare the output to what you ran before deleting the images.