Skip to content

Instantly share code, notes, and snippets.

@sankalpmukim
Created June 29, 2025 05:17
Show Gist options
  • Save sankalpmukim/c4eb99979a149211dd7e84930df51e1d to your computer and use it in GitHub Desktop.
Save sankalpmukim/c4eb99979a149211dd7e84930df51e1d to your computer and use it in GitHub Desktop.
script to clean up non-latest images in aws ecr. made to be run inside ci/cd
#!/bin/bash
# ECR Image Cleanup Script
# This script identifies and removes old Docker images from an ECR repository
# while preserving the latest backend-* and frontend-* tagged images
set -e
# Configuration - can be overridden by environment variables or command line
ACCOUNT_ID="${ACCOUNT_ID:-012345678901}"
REGION="${REGION:-ap-south-1}"
REPOSITORY_NAME="${REPOSITORY_NAME:-my_repo}"
DRY_RUN=${DRY_RUN:-true} # Set to false to actually delete images
AUTO_CONFIRM=false # Set to true with -y flag for non-interactive mode
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-y | --yes)
AUTO_CONFIRM=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--no-dry-run)
DRY_RUN=false
shift
;;
--account-id)
ACCOUNT_ID="$2"
shift 2
;;
--region)
REGION="$2"
shift 2
;;
--repository)
REPOSITORY_NAME="$2"
shift 2
;;
-h | --help)
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -y, --yes Auto-confirm deletion (non-interactive mode)"
echo " --dry-run Run in dry-run mode (default)"
echo " --no-dry-run Actually delete images"
echo " --account-id ID AWS Account ID (default: ${ACCOUNT_ID})"
echo " --region REGION AWS Region (default: ${REGION})"
echo " --repository NAME ECR Repository name (default: ${REPOSITORY_NAME})"
echo " -h, --help Show this help message"
echo ""
echo "Environment variables:"
echo " ACCOUNT_ID AWS Account ID"
echo " REGION AWS Region"
echo " REPOSITORY_NAME ECR Repository name"
echo " DRY_RUN=false Disable dry-run mode"
echo ""
echo "Examples:"
echo " $0 # Dry run with defaults"
echo " $0 -y --no-dry-run # Delete images non-interactively"
echo " $0 --account-id 123456789 --region us-east-1 --repository myapp"
echo " ACCOUNT_ID=123456789 REGION=us-east-1 REPOSITORY_NAME=myapp $0 -y"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== ECR Image Cleanup Tool ===${NC}"
echo "Repository: ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}"
echo "Dry Run Mode: ${DRY_RUN}"
echo "Auto Confirm: ${AUTO_CONFIRM}"
echo ""
# Function to check if AWS CLI is available and configured
check_prerequisites() {
if ! command -v aws &>/dev/null; then
echo -e "${RED}Error: AWS CLI is not installed${NC}"
exit 1
fi
if ! aws sts get-caller-identity &>/dev/null; then
echo -e "${RED}Error: AWS CLI is not configured or credentials are invalid${NC}"
exit 1
fi
}
# Function to get the latest image for a given tag prefix with SHA pattern
get_latest_sha_image() {
local tag_prefix=$1
local expected_length
if [ "$tag_prefix" = "backend-" ]; then
expected_length=48 # "backend-" (8) + 40 hex chars
else
expected_length=49 # "frontend-" (9) + 40 hex chars
fi
aws ecr describe-images \
--registry-id "${ACCOUNT_ID}" \
--repository-name "${REPOSITORY_NAME}" \
--region "${REGION}" \
--query "imageDetails[?imageTags && length(imageTags[?starts_with(@, '${tag_prefix}') && length(@) == \`${expected_length}\`]) > \`0\`].{ImageDigest: imageDigest, ImageTags: imageTags, PushedAt: imagePushedAt} | sort_by(@, &PushedAt) | [-1]" \
--output json 2>/dev/null || echo "null"
}
# Function to get all SHA-pattern images matching tag patterns (for deletion)
get_sha_images_to_delete() {
local latest_backend_digest=$1
local latest_frontend_digest=$2
# Get all backend-<sha> and frontend-<sha> tagged images, excluding the latest ones
# Backend SHA pattern: exactly 48 characters (backend- + 40 hex chars)
# Frontend SHA pattern: exactly 49 characters (frontend- + 40 hex chars)
aws ecr describe-images \
--registry-id "${ACCOUNT_ID}" \
--repository-name "${REPOSITORY_NAME}" \
--region "${REGION}" \
--query "imageDetails[?imageTags && (length(imageTags[?starts_with(@, 'backend-') && length(@) == \`48\`]) > \`0\` || length(imageTags[?starts_with(@, 'frontend-') && length(@) == \`49\`]) > \`0\`) && imageDigest != '${latest_backend_digest}' && imageDigest != '${latest_frontend_digest}'].{ImageDigest: imageDigest, ImageTags: imageTags, PushedAt: imagePushedAt}" \
--output json 2>/dev/null || echo "[]"
}
# Function to get untagged images
get_untagged_images() {
aws ecr describe-images \
--registry-id "${ACCOUNT_ID}" \
--repository-name "${REPOSITORY_NAME}" \
--region "${REGION}" \
--query "imageDetails[?!imageTags || length(imageTags) == \`0\`].{ImageDigest: imageDigest, ImageTags: imageTags, PushedAt: imagePushedAt}" \
--output json 2>/dev/null || echo "[]"
}
# Function to display image info
display_image_info() {
local image_data=$1
local label=$2
local color=$3
if [ "$image_data" != "null" ] && [ "$image_data" != "" ]; then
echo -e "${color}${label}:${NC}"
echo "$image_data" | jq -r '" Digest: " + .ImageDigest + "\n Tags: " + (.ImageTags | join(", ")) + "\n Pushed: " + .PushedAt'
echo ""
else
echo -e "${YELLOW}${label}: None found${NC}"
echo ""
fi
}
# Main execution
main() {
echo -e "${BLUE}Checking prerequisites...${NC}"
check_prerequisites
echo -e "${BLUE}Analyzing repository images...${NC}"
# Find latest backend and frontend SHA-pattern images
latest_backend=$(get_latest_sha_image "backend-")
latest_frontend=$(get_latest_sha_image "frontend-")
# Extract digests for exclusion
latest_backend_digest=""
latest_frontend_digest=""
if [ "$latest_backend" != "null" ] && [ "$latest_backend" != "" ]; then
latest_backend_digest=$(echo "$latest_backend" | jq -r '.ImageDigest // ""')
fi
if [ "$latest_frontend" != "null" ] && [ "$latest_frontend" != "" ]; then
latest_frontend_digest=$(echo "$latest_frontend" | jq -r '.ImageDigest // ""')
fi
# Display what will be kept
echo -e "${GREEN}=== IMAGES TO KEEP ===${NC}"
display_image_info "$latest_backend" "Latest Backend SHA Image" "$GREEN"
display_image_info "$latest_frontend" "Latest Frontend SHA Image" "$GREEN"
# Get images to delete
sha_images_to_delete=$(get_sha_images_to_delete "$latest_backend_digest" "$latest_frontend_digest")
untagged_images=$(get_untagged_images)
# Combine deletion candidates
all_images_to_delete="[]"
if [ "$sha_images_to_delete" != "[]" ] && [ "$sha_images_to_delete" != "" ]; then
all_images_to_delete="$sha_images_to_delete"
fi
# Display what will be deleted
echo -e "${RED}=== OLD SHA-PATTERN IMAGES TO DELETE ===${NC}"
if [ "$sha_images_to_delete" == "[]" ] || [ "$sha_images_to_delete" == "" ]; then
echo -e "${GREEN}No old SHA-pattern images found to delete.${NC}"
else
echo "$sha_images_to_delete" | jq -r '.[] | " Digest: " + .ImageDigest + "\n Tags: " + (.ImageTags | join(", ")) + "\n Pushed: " + .PushedAt + "\n"'
fi
# Display untagged images
echo -e "${YELLOW}=== UNTAGGED IMAGES ===${NC}"
if [ "$untagged_images" == "[]" ] || [ "$untagged_images" == "" ]; then
echo -e "${GREEN}No untagged images found.${NC}"
else
echo "$untagged_images" | jq -r '.[] | " Digest: " + .ImageDigest + "\n Pushed: " + .PushedAt + "\n"'
echo -e "${YELLOW}Note: Untagged images are not automatically deleted by this script.${NC}"
echo -e "${YELLOW}You may want to manually review and delete them if they're not needed.${NC}"
fi
echo ""
# Count images to delete
delete_count=$(echo "$all_images_to_delete" | jq length)
if [ "$delete_count" -eq 0 ]; then
echo -e "${GREEN}No old SHA-pattern images found to delete. Repository is already clean!${NC}"
exit 0
fi
echo -e "${YELLOW}Total SHA-pattern images to delete: ${delete_count}${NC}"
echo ""
if [ "$DRY_RUN" == "true" ]; then
echo -e "${YELLOW}DRY RUN MODE - No images will be deleted${NC}"
echo -e "${YELLOW}To actually delete these images, run with: DRY_RUN=false $0${NC}"
echo -e "${YELLOW}Or use: $0 --no-dry-run${NC}"
exit 0
fi
# Confirm deletion (skip if auto-confirm is enabled)
if [ "$AUTO_CONFIRM" != "true" ]; then
echo -e "${RED}WARNING: This will permanently delete ${delete_count} images!${NC}"
read -p "Are you sure you want to continue? (type 'yes' to confirm): " confirmation
if [ "$confirmation" != "yes" ]; then
echo "Operation cancelled."
exit 0
fi
else
echo -e "${YELLOW}Auto-confirm enabled. Proceeding with deletion of ${delete_count} images...${NC}"
fi
# Perform deletion
echo -e "${BLUE}Deleting old SHA-pattern images...${NC}"
# Create image IDs for batch delete (using digests for complete deletion)
image_ids=$(echo "$all_images_to_delete" | jq -c '[.[] | {imageDigest: .ImageDigest}]')
if [ "$image_ids" != "[]" ]; then
aws ecr batch-delete-image \
--registry-id "${ACCOUNT_ID}" \
--repository-name "${REPOSITORY_NAME}" \
--region "${REGION}" \
--image-ids "$image_ids" \
--output table
echo -e "${GREEN}Successfully deleted ${delete_count} images!${NC}"
fi
}
# Run the script
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment