Created
June 29, 2025 05:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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