Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env bash
- # delete_s3_buckets.sh
- # Empties and deletes all S3 buckets matching a given name prefix.
- #
- # Usage:
- # ./delete_s3_buckets.sh <bucket-name-prefix> [--dry-run]
- #
- # Examples:
- # ./delete_s3_buckets.sh my-temp-bucket
- # ./delete_s3_buckets.sh dev- --dry-run
- #
- # Requirements: aws-cli v2, jq
- set -euo pipefail
- # ── Colours ──────────────────────────────────────────────────────────────────
- RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m'
- # ── Args ─────────────────────────────────────────────────────────────────────
- if [[ $# -lt 1 ]]; then
- echo "Usage: $0 <bucket-name-prefix> [--dry-run]"
- exit 1
- fi
- PREFIX="$1"
- DRY_RUN=false
- [[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
- $DRY_RUN && echo -e "${YELLOW}[DRY-RUN] No changes will be made.${NC}"
- # ── Helpers ───────────────────────────────────────────────────────────────────
- log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
- log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
- log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
- # Delete a list of {Key, VersionId} objects in chunks of 1000 (AWS API limit).
- # Payload is written to a temp file to avoid ARG_MAX shell limits.
- delete_objects_chunked() {
- local bucket="$1"
- local all_objects_json="$2" # JSON array: [{Key:..., VersionId:...}, ...]
- local total
- total=$(echo "$all_objects_json" | jq 'length')
- [[ "$total" -eq 0 ]] && return 0
- local tmp_file
- tmp_file=$(mktemp /tmp/s3_delete_XXXXXX.json)
- # shellcheck disable=SC2064
- trap "rm -f $tmp_file" RETURN
- local offset=0
- local chunk_size=1000
- local deleted_total=0
- while [[ "$offset" -lt "$total" ]]; do
- local chunk
- chunk=$(echo "$all_objects_json" | jq --argjson o "$offset" --argjson s "$chunk_size" \
- '{ Objects: .[$o:$o+$s], Quiet: true }')
- local count
- count=$(echo "$chunk" | jq '.Objects | length')
- if $DRY_RUN; then
- log_info " [DRY-RUN] Would delete $count object versions (offset $offset)."
- else
- echo "$chunk" > "$tmp_file"
- aws s3api delete-objects \
- --bucket "$bucket" \
- --delete "file://$tmp_file" \
- --output json > /dev/null
- (( deleted_total += count )) || true
- log_info " Deleted $count object versions (offset $offset, running total: $deleted_total)."
- fi
- (( offset += chunk_size )) || true
- done
- }
- # Empty a bucket: removes all object versions and delete markers (works for
- # versioned, suspended, and non-versioned buckets).
- empty_bucket() {
- local bucket="$1"
- log_info "Emptying bucket: $bucket"
- # Remove all object versions (including delete markers)
- local has_more="true"
- local key_marker="" version_marker=""
- while [[ "$has_more" == "true" ]]; do
- local list_args=(--bucket "$bucket" --output json)
- [[ -n "$key_marker" ]] && list_args+=(--key-marker "$key_marker")
- [[ -n "$version_marker" ]] && list_args+=(--version-id-marker "$version_marker")
- local result
- result=$(aws s3api list-object-versions "${list_args[@]}" 2>/dev/null || echo '{}')
- # Combine Versions + DeleteMarkers into one array
- local objects
- objects=$(echo "$result" | jq '
- [
- ((.Versions // []) | .[] | { Key: .Key, VersionId: .VersionId }),
- ((.DeleteMarkers // []) | .[] | { Key: .Key, VersionId: .VersionId })
- ]
- ')
- delete_objects_chunked "$bucket" "$objects"
- # Pagination
- local truncated
- truncated=$(echo "$result" | jq -r '.IsTruncated // false')
- if [[ "$truncated" == "true" ]]; then
- key_marker=$(echo "$result" | jq -r '.NextKeyMarker // ""')
- version_marker=$(echo "$result" | jq -r '.NextVersionIdMarker // ""')
- else
- has_more="false"
- fi
- done
- # Also remove any remaining non-versioned objects (just in case)
- if ! $DRY_RUN; then
- aws s3 rm "s3://$bucket" --recursive --quiet 2>/dev/null || true
- fi
- log_info "Bucket $bucket is now empty."
- }
- delete_bucket() {
- local bucket="$1"
- if $DRY_RUN; then
- log_info "[DRY-RUN] Would delete bucket: $bucket"
- else
- aws s3api delete-bucket --bucket "$bucket"
- log_info "Deleted bucket: $bucket"
- fi
- }
- # ── Main ──────────────────────────────────────────────────────────────────────
- log_info "Looking for buckets with prefix: \"$PREFIX\""
- # List all buckets and filter by prefix
- BUCKETS=$(aws s3api list-buckets \
- --query "Buckets[?starts_with(Name, \`$PREFIX\`)].Name" \
- --output text)
- if [[ -z "$BUCKETS" ]]; then
- log_warn "No buckets found matching prefix \"$PREFIX\"."
- exit 0
- fi
- # Convert tab/space-separated output to array
- read -ra BUCKET_ARRAY <<< "$BUCKETS"
- echo ""
- echo -e "${YELLOW}The following buckets will be emptied and deleted:${NC}"
- for b in "${BUCKET_ARRAY[@]}"; do echo " - $b"; done
- echo ""
- if ! $DRY_RUN; then
- read -rp "Are you sure? This is IRREVERSIBLE. Type 'yes' to continue: " confirm
- [[ "$confirm" != "yes" ]] && { log_warn "Aborted."; exit 0; }
- fi
- ERRORS=0
- for BUCKET in "${BUCKET_ARRAY[@]}"; do
- echo "──────────────────────────────────────────"
- if empty_bucket "$BUCKET" && delete_bucket "$BUCKET"; then
- log_info "✓ $BUCKET processed successfully."
- else
- log_error "✗ Failed to process bucket: $BUCKET"
- (( ERRORS++ )) || true
- fi
- done
- echo "══════════════════════════════════════════"
- if [[ "$ERRORS" -eq 0 ]]; then
- log_info "All done. ${#BUCKET_ARRAY[@]} bucket(s) processed."
- else
- log_error "$ERRORS bucket(s) failed. Check the output above."
- exit 1
- fi
Advertisement