Skip to content

Commit 620f19c

Browse files
committed
ecr retention, cleanup script, stage tag
1 parent 0ec9a3b commit 620f19c

File tree

4 files changed

+164
-41
lines changed

4 files changed

+164
-41
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ jobs:
7878
TAGS="$BASE_TAG"
7979
8080
if [[ "${{ inputs.environment }}" == "stage" ]]; then
81+
TAGS="$TAGS,${{ steps.ecr-uri.outputs.ecr-uri }}:stage"
8182
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
8283
TAGS="$TAGS,${{ steps.ecr-uri.outputs.ecr-uri }}:pr-${{ github.event.number }}"
8384
fi

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Change ecr retention, add a script to cleanup old images manually.
13+
1014
## [v17.0.6](https://github.com/lexicalunit/spellbot/releases/tag/v17.0.6) - 2025-11-07
1115

1216
### Changed

infrastructure/app/ecr.tf

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,58 +9,22 @@ resource "aws_ecr_repository" "spellbot" {
99
}
1010

1111
# ECR lifecycle policy to manage image retention
12+
# Note: We only expire untagged images to avoid accidentally deleting prod/stage/latest
13+
# Old git SHA tagged images should be cleaned up manually using scripts/cleanup-ecr.sh
14+
# Example: ./scripts/cleanup-ecr.sh 180 spellbot-app
1215
resource "aws_ecr_lifecycle_policy" "spellbot" {
1316
repository = aws_ecr_repository.spellbot.name
1417

1518
policy = jsonencode({
1619
rules = [
1720
{
1821
rulePriority = 1
19-
description = "Always retain prod images"
20-
selection = {
21-
tagStatus = "tagged"
22-
tagPatternList = ["prod"]
23-
countType = "imageCountMoreThan"
24-
countNumber = 9999
25-
}
26-
action = {
27-
type = "retain"
28-
}
29-
},
30-
{
31-
rulePriority = 2
32-
description = "Always retain stage images"
33-
selection = {
34-
tagStatus = "tagged"
35-
tagPatternList = ["stage"]
36-
countType = "imageCountMoreThan"
37-
countNumber = 9999
38-
}
39-
action = {
40-
type = "retain"
41-
}
42-
},
43-
{
44-
rulePriority = 3
45-
description = "Keep last 50 tagged images (git SHAs)"
46-
selection = {
47-
tagStatus = "tagged"
48-
countType = "imageCountMoreThan"
49-
countNumber = 50
50-
tagPatternList = ["*"]
51-
}
52-
action = {
53-
type = "expire"
54-
}
55-
},
56-
{
57-
rulePriority = 4
58-
description = "Delete untagged images older than 1 day"
22+
description = "Delete untagged images after 7 days"
5923
selection = {
6024
tagStatus = "untagged"
6125
countType = "sinceImagePushed"
6226
countUnit = "days"
63-
countNumber = 1
27+
countNumber = 7
6428
}
6529
action = {
6630
type = "expire"

scripts/cleanup-ecr.sh

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/bin/bash
2+
#
3+
# Cleanup old ECR images while preserving prod, stage, and latest tags
4+
#
5+
# This script deletes tagged images older than a specified number of days,
6+
# but always preserves images tagged with 'prod', 'stage', or 'latest'.
7+
#
8+
# Usage:
9+
# ./scripts/cleanup-ecr.sh [days] [repository-name]
10+
#
11+
# Examples:
12+
# ./scripts/cleanup-ecr.sh 180 spellbot-app # Delete images older than 180 days
13+
# ./scripts/cleanup-ecr.sh 90 # Delete images older than 90 days (default repo)
14+
#
15+
16+
set -euo pipefail
17+
18+
# Configuration
19+
DEFAULT_DAYS=180
20+
DEFAULT_REPOSITORY="spellbot-app"
21+
AWS_REGION="${AWS_REGION:-us-east-1}"
22+
23+
# Parse arguments
24+
DAYS="${1:-$DEFAULT_DAYS}"
25+
REPOSITORY="${2:-$DEFAULT_REPOSITORY}"
26+
27+
# Colors for output
28+
RED='\033[0;31m'
29+
GREEN='\033[0;32m'
30+
YELLOW='\033[1;33m'
31+
NC='\033[0m' # No Color
32+
33+
# Helper functions
34+
log() {
35+
echo -e "${GREEN}[INFO]${NC} $*"
36+
}
37+
38+
warn() {
39+
echo -e "${YELLOW}[WARN]${NC} $*"
40+
}
41+
42+
error() {
43+
echo -e "${RED}[ERROR]${NC} $*" >&2
44+
}
45+
46+
success() {
47+
echo -e "${GREEN}[SUCCESS]${NC} $*"
48+
}
49+
50+
# Validate inputs
51+
if ! [[ "$DAYS" =~ ^[0-9]+$ ]]; then
52+
error "Days must be a positive integer"
53+
exit 1
54+
fi
55+
56+
log "ECR Image Cleanup Configuration:"
57+
log " Repository: $REPOSITORY"
58+
log " Region: $AWS_REGION"
59+
log " Delete images older than: $DAYS days"
60+
log " Protected tags: prod, stage, latest"
61+
echo ""
62+
63+
# Calculate cutoff timestamp (seconds since epoch)
64+
CUTOFF_TIMESTAMP=$(date -u -v-"${DAYS}"d +%s 2>/dev/null || date -u -d "$DAYS days ago" +%s)
65+
CUTOFF_DATE=$(date -u -r "$CUTOFF_TIMESTAMP" +%Y-%m-%d 2>/dev/null || date -u -d "@$CUTOFF_TIMESTAMP" +%Y-%m-%d)
66+
67+
log "Cutoff date: $CUTOFF_DATE"
68+
echo ""
69+
70+
# Check if repository exists
71+
if ! aws ecr describe-repositories \
72+
--repository-names "$REPOSITORY" \
73+
--region "$AWS_REGION" \
74+
--output json > /dev/null 2>&1; then
75+
error "Repository '$REPOSITORY' not found in region '$AWS_REGION'"
76+
exit 1
77+
fi
78+
79+
# Get all images
80+
log "Fetching images from repository..."
81+
IMAGES=$(aws ecr describe-images \
82+
--repository-name "$REPOSITORY" \
83+
--region "$AWS_REGION" \
84+
--output json)
85+
86+
if [[ -z "$IMAGES" || "$IMAGES" == "null" ]]; then
87+
error "Failed to fetch images from repository"
88+
exit 1
89+
fi
90+
91+
# Find images to delete
92+
log "Analyzing images..."
93+
IMAGES_TO_DELETE=$(echo "$IMAGES" | jq -r --arg cutoff "$CUTOFF_TIMESTAMP" '
94+
.imageDetails[] |
95+
select(.imageTags != null) |
96+
select(
97+
(.imageTags | any(. == "prod" or . == "stage" or . == "latest")) | not
98+
) |
99+
select(
100+
(.imagePushedAt | fromdateiso8601) < ($cutoff | tonumber)
101+
) |
102+
{
103+
digest: .imageDigest,
104+
tags: (.imageTags | join(", ")),
105+
pushedAt: (.imagePushedAt | todate)
106+
}
107+
')
108+
109+
if [[ -z "$IMAGES_TO_DELETE" ]]; then
110+
success "No images found matching deletion criteria"
111+
exit 0
112+
fi
113+
114+
# Count images to delete
115+
IMAGE_COUNT=$(echo "$IMAGES_TO_DELETE" | jq -s 'length')
116+
117+
warn "Found $IMAGE_COUNT image(s) to delete:"
118+
echo ""
119+
echo "$IMAGES_TO_DELETE" | jq -r '" Tags: \(.tags)\n Pushed: \(.pushedAt)\n Digest: \(.digest)\n"'
120+
121+
# Confirm deletion
122+
read -p "Do you want to delete these images? (yes/no): " -r
123+
echo ""
124+
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
125+
log "Deletion cancelled"
126+
exit 0
127+
fi
128+
129+
# Delete images
130+
log "Deleting images..."
131+
DELETED_COUNT=0
132+
FAILED_COUNT=0
133+
134+
while IFS= read -r digest; do
135+
if aws ecr batch-delete-image \
136+
--repository-name "$REPOSITORY" \
137+
--region "$AWS_REGION" \
138+
--image-ids "imageDigest=$digest" \
139+
--output json > /dev/null 2>&1; then
140+
((DELETED_COUNT++))
141+
log "Deleted image: $digest"
142+
else
143+
((FAILED_COUNT++))
144+
error "Failed to delete image: $digest"
145+
fi
146+
done < <(echo "$IMAGES_TO_DELETE" | jq -r '.digest')
147+
148+
echo ""
149+
success "Cleanup complete!"
150+
log " Deleted: $DELETED_COUNT image(s)"
151+
if [[ $FAILED_COUNT -gt 0 ]]; then
152+
warn " Failed: $FAILED_COUNT image(s)"
153+
fi
154+

0 commit comments

Comments
 (0)