|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +usage() { |
| 5 | + cat << 'USAGE' |
| 6 | +Usage: delete-instance.sh [OPTIONS] |
| 7 | +
|
| 8 | +Delete an AWS RDS DB instance with controlled snapshot behavior. |
| 9 | +
|
| 10 | +Options: |
| 11 | + --identifier ID DB instance identifier (required) |
| 12 | + --skip-final-snapshot Skip final snapshot (dangerous) |
| 13 | + --final-snapshot-id ID Final snapshot identifier (auto-generated when omitted) |
| 14 | + --delete-automated-backups Delete retained automated backups (default) |
| 15 | + --retain-automated-backups Keep retained automated backups |
| 16 | + --wait Wait until DB instance is fully deleted |
| 17 | + --timeout SEC Wait timeout in seconds (default: 7200) |
| 18 | + --poll-interval SEC Poll interval in seconds (default: 20) |
| 19 | + --region REGION AWS region |
| 20 | + --profile PROFILE AWS CLI profile |
| 21 | + --dry-run Print AWS command without executing |
| 22 | + -h, --help Show help |
| 23 | +USAGE |
| 24 | +} |
| 25 | + |
| 26 | +die() { |
| 27 | + printf 'ERROR: %s\n' "$*" >&2 |
| 28 | + exit 2 |
| 29 | +} |
| 30 | + |
| 31 | +log() { |
| 32 | + printf '%s [delete-instance] %s\n' "$(date +"%Y-%m-%dT%H:%M:%S%z")" "$*" >&2 |
| 33 | +} |
| 34 | + |
| 35 | +command_exists() { |
| 36 | + command -v "$1" > /dev/null 2>&1 |
| 37 | +} |
| 38 | + |
| 39 | +validate_identifier() { |
| 40 | + [[ "$1" =~ ^[a-z][a-z0-9-]{0,62}$ && ! "$1" =~ -- && ! "$1" =~ -$ ]] |
| 41 | +} |
| 42 | + |
| 43 | +validate_snapshot_identifier() { |
| 44 | + [[ "$1" =~ ^[a-z][a-z0-9-]{0,254}$ && ! "$1" =~ -- && ! "$1" =~ -$ ]] |
| 45 | +} |
| 46 | + |
| 47 | +timestamp_utc() { |
| 48 | + date -u +"%Y%m%d%H%M%S" |
| 49 | +} |
| 50 | + |
| 51 | +wait_for_deleted() { |
| 52 | + local start_time elapsed out rc |
| 53 | + start_time="$(date +%s)" |
| 54 | + |
| 55 | + while true; do |
| 56 | + set +e |
| 57 | + out="$(aws "${aws_options[@]}" rds describe-db-instances \ |
| 58 | + --db-instance-identifier "$identifier" \ |
| 59 | + --query 'DBInstances[0].DBInstanceStatus' \ |
| 60 | + --output text 2>&1)" |
| 61 | + rc=$? |
| 62 | + set -e |
| 63 | + |
| 64 | + if ((rc != 0)); then |
| 65 | + if grep -q "DBInstanceNotFound" <<< "$out"; then |
| 66 | + return 0 |
| 67 | + fi |
| 68 | + die "failed while checking deletion status: $out" |
| 69 | + fi |
| 70 | + |
| 71 | + elapsed=$(($(date +%s) - start_time)) |
| 72 | + if ((elapsed >= timeout_seconds)); then |
| 73 | + die "timeout waiting for DB instance deletion: $identifier" |
| 74 | + fi |
| 75 | + |
| 76 | + sleep "$poll_interval" |
| 77 | + done |
| 78 | +} |
| 79 | + |
| 80 | +aws_options=() |
| 81 | +identifier="" |
| 82 | +skip_final_snapshot=false |
| 83 | +final_snapshot_id="" |
| 84 | +delete_automated_backups=true |
| 85 | +wait_enabled=false |
| 86 | +timeout_seconds=7200 |
| 87 | +poll_interval=20 |
| 88 | +dry_run=false |
| 89 | + |
| 90 | +while (($#)); do |
| 91 | + case "$1" in |
| 92 | + --identifier) |
| 93 | + shift |
| 94 | + (($#)) || die "--identifier requires a value" |
| 95 | + validate_identifier "$1" || die "invalid --identifier value" |
| 96 | + identifier="$1" |
| 97 | + ;; |
| 98 | + --skip-final-snapshot) |
| 99 | + skip_final_snapshot=true |
| 100 | + ;; |
| 101 | + --final-snapshot-id) |
| 102 | + shift |
| 103 | + (($#)) || die "--final-snapshot-id requires a value" |
| 104 | + validate_snapshot_identifier "$1" || die "invalid --final-snapshot-id value" |
| 105 | + final_snapshot_id="$1" |
| 106 | + ;; |
| 107 | + --delete-automated-backups) |
| 108 | + delete_automated_backups=true |
| 109 | + ;; |
| 110 | + --retain-automated-backups) |
| 111 | + delete_automated_backups=false |
| 112 | + ;; |
| 113 | + --wait) |
| 114 | + wait_enabled=true |
| 115 | + ;; |
| 116 | + --timeout) |
| 117 | + shift |
| 118 | + (($#)) || die "--timeout requires a value" |
| 119 | + [[ "$1" =~ ^[1-9][0-9]*$ ]] || die "--timeout must be a positive integer" |
| 120 | + timeout_seconds="$1" |
| 121 | + ;; |
| 122 | + --poll-interval) |
| 123 | + shift |
| 124 | + (($#)) || die "--poll-interval requires a value" |
| 125 | + [[ "$1" =~ ^[1-9][0-9]*$ ]] || die "--poll-interval must be a positive integer" |
| 126 | + poll_interval="$1" |
| 127 | + ;; |
| 128 | + --region) |
| 129 | + shift |
| 130 | + (($#)) || die "--region requires a value" |
| 131 | + aws_options+=(--region "$1") |
| 132 | + ;; |
| 133 | + --profile) |
| 134 | + shift |
| 135 | + (($#)) || die "--profile requires a value" |
| 136 | + aws_options+=(--profile "$1") |
| 137 | + ;; |
| 138 | + --dry-run) |
| 139 | + dry_run=true |
| 140 | + ;; |
| 141 | + -h | --help) |
| 142 | + usage |
| 143 | + exit 0 |
| 144 | + ;; |
| 145 | + *) |
| 146 | + die "unknown option: $1" |
| 147 | + ;; |
| 148 | + esac |
| 149 | + shift |
| 150 | +done |
| 151 | + |
| 152 | +command_exists aws || die "aws CLI is required but not found" |
| 153 | +[[ -n "$identifier" ]] || die "--identifier is required" |
| 154 | + |
| 155 | +instance_state="$(aws "${aws_options[@]}" rds describe-db-instances \ |
| 156 | + --db-instance-identifier "$identifier" \ |
| 157 | + --query 'DBInstances[0].DBInstanceStatus' \ |
| 158 | + --output text 2> /dev/null || true)" |
| 159 | + |
| 160 | +if [[ -z "$instance_state" || "$instance_state" == "None" ]]; then |
| 161 | + die "DB instance not found: $identifier" |
| 162 | +fi |
| 163 | + |
| 164 | +if [[ "$instance_state" == "deleting" ]]; then |
| 165 | + log "instance is already deleting: $identifier" |
| 166 | + if $wait_enabled; then |
| 167 | + wait_for_deleted |
| 168 | + log "instance deleted: $identifier" |
| 169 | + fi |
| 170 | + exit 0 |
| 171 | +fi |
| 172 | + |
| 173 | +if $skip_final_snapshot && [[ -n "$final_snapshot_id" ]]; then |
| 174 | + die "--final-snapshot-id cannot be used with --skip-final-snapshot" |
| 175 | +fi |
| 176 | + |
| 177 | +if ! $skip_final_snapshot && [[ -z "$final_snapshot_id" ]]; then |
| 178 | + final_snapshot_id="${identifier}-final-$(timestamp_utc)" |
| 179 | +fi |
| 180 | + |
| 181 | +delete_cmd=(aws "${aws_options[@]}" rds delete-db-instance --db-instance-identifier "$identifier") |
| 182 | + |
| 183 | +if $skip_final_snapshot; then |
| 184 | + delete_cmd+=(--skip-final-snapshot) |
| 185 | +else |
| 186 | + validate_snapshot_identifier "$final_snapshot_id" || die "generated final snapshot ID is invalid: $final_snapshot_id" |
| 187 | + delete_cmd+=(--final-db-snapshot-identifier "$final_snapshot_id") |
| 188 | +fi |
| 189 | + |
| 190 | +if $delete_automated_backups; then |
| 191 | + delete_cmd+=(--delete-automated-backups) |
| 192 | +else |
| 193 | + delete_cmd+=(--no-delete-automated-backups) |
| 194 | +fi |
| 195 | + |
| 196 | +if $dry_run; then |
| 197 | + printf 'DRY-RUN:' >&2 |
| 198 | + printf ' %q' "${delete_cmd[@]}" >&2 |
| 199 | + printf '\n' >&2 |
| 200 | + log "dry-run mode: instance not deleted" |
| 201 | + exit 0 |
| 202 | +fi |
| 203 | + |
| 204 | +"${delete_cmd[@]}" > /dev/null |
| 205 | +log "delete requested for DB instance: $identifier" |
| 206 | +if ! $skip_final_snapshot; then |
| 207 | + log "final snapshot: $final_snapshot_id" |
| 208 | +fi |
| 209 | + |
| 210 | +if $wait_enabled; then |
| 211 | + wait_for_deleted |
| 212 | + log "instance deleted: $identifier" |
| 213 | +fi |
| 214 | + |
| 215 | +exit 0 |
0 commit comments