Setting the file. One moment.
Generate Teardown · Amazon DynamoDB · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page Reference Architecture
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
scripts/generate_teardown.py
scripts/ generate_teardown.py
Python · 337 lines · 11 KB
16 for post-mortem).
17
18 Usage:
19 python3 generate_teardown.py \\
20 --manifest created_resources.json \\
21 --out teardown.sh
22 """
23 from __future__ import annotations
24
25 import argparse
26 import json
27 import sys
28 from pathlib import Path
29
30 TEMPLATE = r """ #!/usr/bin/env bash
31 # Teardown script generated by amazon-dynamodb/scripts/generate_teardown.py.
32 #
33 # Deletes every resource created by one ddb-skill-bench run. Safe by default:
34 # - Refuses unless --confirm is passed.
35 # - Refuses if aws sts get-caller-identity returns a different account than
36 # the manifest used at deploy time.
37 # - Only touches resources whose name starts with the manifest's prefix.
38 # - --dry-run lists what would be deleted without actually deleting.
39 # - --delete-logs also deletes the Lambda CloudWatch log group (off by
40 # default; logs are useful for post-mortem).
41 #
42 # Regenerate from manifest with:
43 # python3 amazon-dynamodb/scripts/generate_teardown.py \
44 # --manifest created_resources.json --out teardown.sh
45
46 set -euo pipefail
47
48 MANIFEST_ACCOUNT='{account}'
49 MANIFEST_REGION='{region}'
50 MANIFEST_PREFIX='{prefix}'
51 MANIFEST_RUN_ID='{run_id}'
52 AWS_PROFILE_HINT='{profile_hint}'
53 # Default AWS_PROFILE to the one the deploy used, so teardown is self-contained
54 # (no need to export it first). An already-set AWS_PROFILE still wins.
55 if [ -z "${{AWS_PROFILE:-}}" ] && [ -n "$AWS_PROFILE_HINT" ] ; then
56 export AWS_PROFILE=" $ AWS_PROFILE_HINT"
57 fi
58 TABLES= ( {tables_bash_array} )
59 LAMBDA_FN='{lambda_fn}'
60 LAMBDA_ROLE='{lambda_role}'
61 LAMBDA_POLICY='{lambda_policy}'
62 LAMBDA_LOG_GROUP='{lambda_log_group}'
63
64 BANNER_SEP= $( printf '=% . 0s' $( seq 1 72 ))
65
66 print_banner () {{
67 echo " $ BANNER_SEP"
68 echo "ddb-skill-bench teardown"
69 echo " Account expected ( from manifest ) : $ MANIFEST_ACCOUNT"
70 echo " Region: $ MANIFEST_REGION"
71 echo " Prefix: $ MANIFEST_PREFIX"
72 echo " Run ID: $ MANIFEST_RUN_ID"
73 echo " Tables: $ {{ #TABLES[@]}}"
74 for t in " $ {{TABLES [ @ ] }}"; do echo " - $ t"; done
75 if [ [ -n "$LAMBDA_FN" ] ]; then
76 echo " Lambda: $ LAMBDA_FN"
77 fi
78 if [ [ -n "$LAMBDA_ROLE" ] ]; then
79 echo " IAM role: $ LAMBDA_ROLE ( policy: $ LAMBDA_POLICY ) "
80 fi
81 echo " $ BANNER_SEP"
82 }}
83
84 DRY_RUN=0
85 CONFIRM=0
86 DELETE_LOGS=0
87 for arg in " $ @"; do
88 case " $ arg" in
89 --dry-run) DRY_RUN=1 ;;
90 --confirm) CONFIRM=1 ;;
91 --delete-logs) DELETE_LOGS=1 ;;
92 -h | --help)
93 echo "Usage: $ 0 --confirm [ --dry-run ] [ --delete-logs ] "
94 echo " --confirm required; actually delete resources"
95 echo " --dry-run list what would be deleted; call no delete APIs"
96 echo " --delete-logs also delete the Lambda CloudWatch log group"
97 exit 0
98 ;;
99 * ) echo "unknown arg: $ arg" >&2; exit 2 ;;
100 esac
101 done
102
103 print_banner
104
105 if [ [ $CONFIRM -ne 1 ] ]; then
106 echo "Refusing to run without --confirm . " >&2
107 echo "Pass --dry-run first to preview, then re-run with --confirm . " >&2
108 exit 2
109 fi
110
111 # --- Identity check ---
112 caller_json= $( aws sts get-caller-identity --region " $ MANIFEST_REGION" 2>&1 ) || {{
113 echo "aws sts get-caller-identity failed:" >&2
114 echo " $ caller_json" >&2
115 echo "Set AWS_PROFILE ( hint: $ AWS_PROFILE_HINT ) or run: aws sso login --profile $ AWS_PROFILE_HINT" >&2
116 exit 2
117 }}
118 caller_account= $( echo " $ caller_json" | python3 -c "import sys, json; print ( json . load ( sys . stdin )[ 'Account' ]) " )
119
120 if [ [ "$caller_account" != "$MANIFEST_ACCOUNT" ] ]; then
121 echo "Account mismatch:" >&2
122 echo " current caller identity: $ caller_account" >&2
123 echo " manifest expected: $ MANIFEST_ACCOUNT" >&2
124 echo "Refusing to delete — wrong account . " >&2
125 exit 2
126 fi
127
128 echo "Identity check passed: account $ caller_account matches manifest . "
129
130 # --- DynamoDB tables ---
131 for table in " $ {{TABLES [ @ ] }}"; do
132 case " $ table" in
133 " $ MANIFEST_PREFIX" * ) ;;
134 * )
135 echo "SKIP: $ table does not start with prefix $ MANIFEST_PREFIX" >&2
136 continue
137 ;;
138 esac
139
140 if [ [ $DRY_RUN -eq 1 ] ]; then
141 echo "DRY-RUN: would delete table $ table"
142 continue
143 fi
144
145 echo "-> table: $ table"
146 if aws dynamodb delete-table \
147 --table-name " $ table" \
148 --region " $ MANIFEST_REGION" >/dev/null 2>&1; then
149 # DeleteTable is ASYNCHRONOUS: a 200 means the delete was ACCEPTED, the
150 # table then sits in DELETING for a short while. Wait until it is actually
151 # gone before reporting success, so "deleted" matches reality and a caller
152 # who re-checks immediately doesn't see a lingering DELETING table.
153 printf " deleting"
154 for _ in $( seq 1 60 ) ; do
155 if aws dynamodb describe-table --table-name " $ table" \
156 --region " $ MANIFEST_REGION" >/dev/null 2>&1; then
157 printf " . "
158 sleep 2
159 else
160 break
161 fi
162 done
163 if aws dynamodb describe-table --table-name " $ table" --region " $ MANIFEST_REGION" >/dev/null 2>&1; then
164 echo " STILL PRESENT after wait — aborting" >&2
165 exit 1
166 fi
167 echo " deleted"
168 else
169 # ResourceNotFound is tolerable (already deleted); anything else is fatal.
170 if aws dynamodb describe-table --table-name " $ table" --region " $ MANIFEST_REGION" >/dev/null 2>&1; then
171 echo " FAILED; table still exists — aborting" >&2
172 exit 1
173 else
174 echo " already gone"
175 fi
176 fi
177 done
178
179 # --- Lambda function ---
180 if [ [ -n "$LAMBDA_FN" ] ]; then
181 case " $ LAMBDA_FN" in
182 " $ MANIFEST_PREFIX" * ) ;;
183 * )
184 echo "SKIP: lambda $ LAMBDA_FN does not start with prefix $ MANIFEST_PREFIX" >&2
185 LAMBDA_FN=""
186 ;;
187 esac
188 fi
189 if [ [ -n "$LAMBDA_FN" ] ]; then
190 if [ [ $DRY_RUN -eq 1 ] ]; then
191 echo "DRY-RUN: would delete Lambda $ LAMBDA_FN"
192 else
193 echo "-> lambda: $ LAMBDA_FN"
194 aws lambda delete-function \
195 --function-name " $ LAMBDA_FN" \
196 --region " $ MANIFEST_REGION" >/dev/null 2>&1 || echo " already gone"
197 echo " deleted"
198 fi
199 fi
200
201 # --- IAM role (inline policy first, then role) ---
202 if [ [ -n "$LAMBDA_ROLE" ] ]; then
203 case " $ LAMBDA_ROLE" in
204 " $ MANIFEST_PREFIX" * ) ;;
205 * )
206 echo "SKIP: role $ LAMBDA_ROLE does not start with prefix $ MANIFEST_PREFIX" >&2
207 LAMBDA_ROLE=""
208 ;;
209 esac
210 fi
211 if [ [ -n "$LAMBDA_ROLE" ] ]; then
212 if [ [ $DRY_RUN -eq 1 ] ]; then
213 echo "DRY-RUN: would delete IAM role $ LAMBDA_ROLE ( policy $ LAMBDA_POLICY ) "
214 else
215 echo "-> iam role: $ LAMBDA_ROLE"
216 if [ [ -n "$LAMBDA_POLICY" ] ]; then
217 aws iam delete-role-policy \
218 --role-name " $ LAMBDA_ROLE" \
219 --policy-name " $ LAMBDA_POLICY" >/dev/null 2>&1 || true
220 fi
221 aws iam delete-role --role-name " $ LAMBDA_ROLE" >/dev/null 2>&1 || echo " already gone"
222 echo " deleted"
223 fi
224 fi
225
226 # --- CloudWatch log group (optional) ---
227 if [ [ $DELETE_LOGS -eq 1 && -n "$LAMBDA_LOG_GROUP" ] ]; then
228 case " $ LAMBDA_LOG_GROUP" in
229 /aws/lambda/" $ MANIFEST_PREFIX" * ) ;;
230 * )
231 echo "SKIP: log group $ LAMBDA_LOG_GROUP does not match prefix" >&2
232 LAMBDA_LOG_GROUP=""
233 ;;
234 esac
235 fi
236 if [ [ $DELETE_LOGS -eq 1 && -n "$LAMBDA_LOG_GROUP" ] ]; then
237 if [ [ $DRY_RUN -eq 1 ] ]; then
238 echo "DRY-RUN: would delete log group $ LAMBDA_LOG_GROUP"
239 else
240 echo "-> log group: $ LAMBDA_LOG_GROUP"
241 aws logs delete-log-group \
242 --log-group-name " $ LAMBDA_LOG_GROUP" \
243 --region " $ MANIFEST_REGION" >/dev/null 2>&1 || echo " already gone"
244 echo " deleted"
245 fi
246 fi
247
248 if [ [ $DRY_RUN -eq 1 ] ]; then
249 echo "Dry run complete — no resources deleted . "
250 else
251 echo "Teardown complete . Verify in the console that no ddb-skill-bench"
252 echo "resources remain under prefix $ MANIFEST_PREFIX . "
253 fi
254 """
255
256
257 def _die (msg: str , code: int = 2 ) -> None :
258 print ( f "ERROR: { msg } " , file = sys.stderr)
259 sys.exit(code)
260
261
262 def main ():
263 p = argparse.ArgumentParser( description = __doc__ .splitlines()[ 0 ])
264 p.add_argument( "--manifest" , required = True , help = "path to created_resources.json" )
265 p.add_argument( "--out" , required = True , help = "path to write teardown.sh" )
266 args = p.parse_args()
267
268 mp = Path(args.manifest)
269 if not mp.exists():
270 _die( f "manifest not found: { mp } " )
271 manifest = json.loads(mp.read_text())
272
273 prefix = manifest.get( "prefix" )
274 if not prefix or not prefix.startswith( "ddb-skill-bench-" ):
275 _die(
276 f "manifest prefix { prefix !r} is missing or does not start with "
277 "'ddb-skill-bench-'. Refusing to generate teardown."
278 )
279
280 tables = [t.get( "name" ) for t in (manifest.get( "tables" ) or []) if t.get( "name" )]
281 bad = [t for t in tables if not t.startswith(prefix)]
282 if bad:
283 _die(
284 f "manifest contains tables whose names don't match prefix "
285 f " { prefix !r} : { bad } . Refusing to generate teardown."
286 )
287
288 lam = manifest.get( "lambda" ) or {}
289 lambda_fn = lam.get( "function_name" , "" )
290 lambda_role = lam.get( "role_name" , "" )
291 lambda_policy = lam.get( "policy_name" , "" )
292 if lambda_fn and not lambda_fn.startswith(prefix):
293 _die(
294 f "manifest lambda function_name { lambda_fn !r} does not start with "
295 f "prefix { prefix !r} . Refusing."
296 )
297 if lambda_role and not lambda_role.startswith(prefix):
298 _die(
299 f "manifest lambda role_name { lambda_role !r} does not start with "
300 f "prefix { prefix !r} . Refusing."
301 )
302 lambda_log_group = f "/aws/lambda/ { lambda_fn } " if lambda_fn else ""
303
304 tables_bash_array = " " .join( f "' { t } '" for t in tables)
305
306 # Use the AWS CLI profile recorded at deploy time so the sso-login hint
307 # in the teardown script points at the right profile, not the IAM account
308 # alias (which is a different concept).
309 content = TEMPLATE .format(
310 account = manifest.get( "account" , "" ),
311 region = manifest.get( "region" , "" ),
312 prefix = prefix,
313 run_id = manifest.get( "run_id" , "" ),
314 profile_hint = manifest.get( "aws_profile" ) or manifest.get( "alias" ) or "" ,
315 tables_bash_array = tables_bash_array,
316 lambda_fn = lambda_fn,
317 lambda_role = lambda_role,
318 lambda_policy = lambda_policy,
319 lambda_log_group = lambda_log_group,
320 )
321
322 out = Path(args.out)
323 out.write_text(content)
324 out.chmod( 0o 755 )
325 print ( f "Teardown script written to: { out } " )
326 print ()
327 print ( "Review the script, then:" )
328 print ( f " bash { out } --dry-run # preview — calls no delete APIs" )
329 print ( f " bash { out } --confirm # delete the tables, Lambda, and IAM role" )
330 print (
331 f " bash { out } --confirm --delete-logs # also delete the Lambda's "
332 "CloudWatch log group (its run logs are gone for good)"
333 )
334
335
336 if __name__ == "__main__" :
337 main()