| 1 | #!/usr/bin/env bun |
| 2 | |
| 3 | // @ts-nocheck |
| 4 | |
| 5 | import * as Credentials from "@distilled.cloud/aws/Credentials"; |
| 6 | import * as ec2 from "@distilled.cloud/aws/ec2"; |
| 7 | import { Region } from "@distilled.cloud/aws/Region"; |
| 8 | import { NodeServices } from "@effect/platform-node"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as FetchHttpClient from "effect/http/FetchHttpClient"; |
| 12 | import { parseArgs } from "node:util"; |
| 13 | |
| 14 | const { values } = parseArgs({ |
| 15 | args: Bun.argv.slice(2), |
| 16 | options: { |
| 17 | region: { type: "string", short: "r" }, |
| 18 | profile: { type: "string", short: "p" }, |
| 19 | execute: { type: "boolean" }, |
| 20 | "include-default-vpcs": { type: "boolean" }, |
| 21 | passes: { type: "string" }, |
| 22 | help: { type: "boolean", short: "h" }, |
| 23 | }, |
| 24 | strict: true, |
| 25 | }); |
| 26 | |
| 27 | const usage = ` |
| 28 | Best-effort EC2 regional cleanup using @distilled.cloud/aws. |
| 29 | |
| 30 | Usage: |
| 31 | bun alchemy/scripts/clean-aws.ts --region us-east-1 [--profile dev] |
| 32 | bun alchemy/scripts/clean-aws.ts --region us-east-1 --execute |
| 33 | |
| 34 | Options: |
| 35 | --region, -r AWS region to clean |
| 36 | --profile, -p Optional AWS profile |
| 37 | --execute Perform deletions (default is dry-run) |
| 38 | --include-default-vpcs Also attempt to delete default VPC resources |
| 39 | --passes Cleanup passes to run (default: 1 dry-run, 4 execute) |
| 40 | --help, -h Show this help |
| 41 | `; |
| 42 | |
| 43 | if (values.help) { |
| 44 | console.log(usage.trim()); |
| 45 | process.exit(0); |
| 46 | } |
| 47 | |
| 48 | const region = |
| 49 | values.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION; |
| 50 | if (!region) { |
| 51 | console.error("Missing --region. Set it explicitly or via AWS_REGION."); |
| 52 | console.error(usage.trim()); |
| 53 | process.exit(1); |
| 54 | } |
| 55 | |
| 56 | if (values.profile) { |
| 57 | process.env.AWS_PROFILE = values.profile; |
| 58 | } |
| 59 | |
| 60 | const execute = values.execute ?? false; |
| 61 | const includeDefaultVpcs = values["include-default-vpcs"] ?? false; |
| 62 | const maxPasses = Number(values.passes ?? (execute ? "4" : "1")); |
| 63 | |
| 64 | if (!Number.isFinite(maxPasses) || maxPasses < 1) { |
| 65 | console.error("--passes must be a positive integer."); |
| 66 | process.exit(1); |
| 67 | } |
| 68 | |
| 69 | const credentialsLayer = values.profile |
| 70 | ? Layer.provideMerge( |
| 71 | Credentials.fromSSO(values.profile), |
| 72 | Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer), |
| 73 | ) |
| 74 | : Credentials.fromChain(); |
| 75 | |
| 76 | const runtime = Layer.mergeAll( |
| 77 | NodeServices.layer, |
| 78 | FetchHttpClient.layer, |
| 79 | credentialsLayer, |
| 80 | Layer.succeed(Region, region), |
| 81 | ); |
| 82 | |
| 83 | const formatError = (error: unknown) => { |
| 84 | if (typeof error === "object" && error && "_tag" in error) { |
| 85 | const tagged = error as { _tag: string; message?: string }; |
| 86 | return tagged.message ? `${tagged._tag}: ${tagged.message}` : tagged._tag; |
| 87 | } |
| 88 | if (error instanceof Error) { |
| 89 | return error.message; |
| 90 | } |
| 91 | try { |
| 92 | return JSON.stringify(error); |
| 93 | } catch { |
| 94 | return String(error); |
| 95 | } |
| 96 | }; |
| 97 | |
| 98 | const log = (message: string) => Effect.sync(() => console.log(message)); |
| 99 | const warn = (message: string) => Effect.sync(() => console.warn(message)); |
| 100 | |
| 101 | const isNotFoundError = (error: unknown) => |
| 102 | typeof error === "object" && |
| 103 | error !== null && |
| 104 | "_tag" in error && |
| 105 | String((error as { _tag: string })._tag).includes("NotFound"); |
| 106 | |
| 107 | const uniq = <T>(items: ReadonlyArray<T | null | undefined>) => [ |
| 108 | ...new Set( |
| 109 | items.filter((item): item is T => item !== null && item !== undefined), |
| 110 | ), |
| 111 | ]; |
| 112 | |
| 113 | const chunk = <T>(items: ReadonlyArray<T>, size: number) => { |
| 114 | const groups: T[][] = []; |
| 115 | for (let index = 0; index < items.length; index += size) { |
| 116 | groups.push(items.slice(index, index + size)); |
| 117 | } |
| 118 | return groups; |
| 119 | }; |
| 120 | |
| 121 | const preview = (label: string, ids: ReadonlyArray<string>) => |
| 122 | ids.length === 0 |
| 123 | ? Effect.void |
| 124 | : log(`Found ${ids.length} ${label}: ${ids.join(", ")}`); |
| 125 | |
| 126 | const ignoreFailure = <A, E, R>( |
| 127 | label: string, |
| 128 | effect: Effect.Effect<A, E, R>, |
| 129 | ): Effect.Effect<void, E, R> => |
| 130 | effect.pipe( |
| 131 | Effect.asVoid, |
| 132 | Effect.catch((error) => |
| 133 | isNotFoundError(error) |
| 134 | ? warn(`Already gone ${label}: ${formatError(error)}`) |
| 135 | : Effect.fail(error as E), |
| 136 | ), |
| 137 | ); |
| 138 | |
| 139 | const deleteEach = <R>( |
| 140 | label: string, |
| 141 | ids: ReadonlyArray<string>, |
| 142 | remove: (id: string) => Effect.Effect<any, any, R>, |
| 143 | ) => |
| 144 | Effect.gen(function* () { |
| 145 | if (ids.length === 0) { |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | yield* preview(label, ids); |
| 150 | if (!execute) { |
| 151 | return; |
| 152 | } |
| 153 | |
| 154 | yield* Effect.forEach( |
| 155 | ids, |
| 156 | (id) => ignoreFailure(`${label} ${id}`, remove(id)), |
| 157 | { |
| 158 | concurrency: 1, |
| 159 | discard: true, |
| 160 | }, |
| 161 | ); |
| 162 | }); |
| 163 | |
| 164 | const repeatUntil = <A, E, R>( |
| 165 | label: string, |
| 166 | effect: Effect.Effect<A, E, R>, |
| 167 | done: (value: A) => boolean, |
| 168 | attempts: number, |
| 169 | delayMs: number, |
| 170 | ): Effect.Effect<void, E, R> => |
| 171 | Effect.gen(function* () { |
| 172 | const value = yield* effect; |
| 173 | if (done(value)) { |
| 174 | return; |
| 175 | } |
| 176 | if (attempts <= 1) { |
| 177 | yield* warn(`Timed out waiting for ${label}.`); |
| 178 | return; |
| 179 | } |
| 180 | yield* log(`Waiting for ${label}...`); |
| 181 | yield* Effect.sleep(delayMs); |
| 182 | yield* repeatUntil(label, effect, done, attempts - 1, delayMs); |
| 183 | }); |
| 184 | |
| 185 | const terminateInstances = Effect.gen(function* () { |
| 186 | const response = yield* ec2.describeInstances({} as any); |
| 187 | const ids = uniq( |
| 188 | (response.Reservations ?? []).flatMap((reservation: any) => |
| 189 | (reservation.Instances ?? []) |
| 190 | .filter((instance: any) => { |
| 191 | const state = instance.State?.Name; |
| 192 | return state !== "shutting-down" && state !== "terminated"; |
| 193 | }) |
| 194 | .map((instance: any) => instance.InstanceId), |
| 195 | ), |
| 196 | ); |
| 197 | |
| 198 | if (ids.length === 0) { |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | yield* preview("instance(s)", ids); |
| 203 | if (!execute) { |
| 204 | return; |
| 205 | } |
| 206 | |
| 207 | for (const group of chunk(ids, 100)) { |
| 208 | yield* ignoreFailure( |
| 209 | `instances ${group.join(", ")}`, |
| 210 | ec2.terminateInstances({ InstanceIds: group } as any), |
| 211 | ); |
| 212 | } |
| 213 | |
| 214 | yield* repeatUntil( |
| 215 | "instances to terminate", |
| 216 | ec2 |
| 217 | .describeInstances({} as any) |
| 218 | .pipe( |
| 219 | Effect.map((result) => |
| 220 | uniq( |
| 221 | (result.Reservations ?? []).flatMap((reservation: any) => |
| 222 | (reservation.Instances ?? []) |
| 223 | .filter((instance: any) => ids.includes(instance.InstanceId)) |
| 224 | .map((instance: any) => instance.State?.Name), |
| 225 | ), |
| 226 | ), |
| 227 | ), |
| 228 | ), |
| 229 | (states) => states.every((state) => state === "terminated"), |
| 230 | 40, |
| 231 | 5_000, |
| 232 | ); |
| 233 | }); |
| 234 | |
| 235 | const deleteVpcEndpoints = Effect.gen(function* () { |
| 236 | const ids = uniq( |
| 237 | (yield* ec2.describeVpcEndpoints({} as any)).VpcEndpoints?.map( |
| 238 | (endpoint: any) => endpoint.VpcEndpointId, |
| 239 | ), |
| 240 | ); |
| 241 | |
| 242 | if (ids.length === 0) { |
| 243 | return; |
| 244 | } |
| 245 | |
| 246 | yield* preview("VPC endpoint(s)", ids); |
| 247 | if (!execute) { |
| 248 | return; |
| 249 | } |
| 250 | |
| 251 | for (const group of chunk(ids, 25)) { |
| 252 | yield* ignoreFailure( |
| 253 | `VPC endpoints ${group.join(", ")}`, |
| 254 | ec2.deleteVpcEndpoints({ VpcEndpointIds: group } as any), |
| 255 | ); |
| 256 | } |
| 257 | |
| 258 | yield* repeatUntil( |
| 259 | "VPC endpoints to delete", |
| 260 | ec2.describeVpcEndpoints({ VpcEndpointIds: ids } as any).pipe( |
| 261 | Effect.map((result) => |
| 262 | uniq( |
| 263 | (result.VpcEndpoints ?? []).map( |
| 264 | (endpoint: any) => endpoint.VpcEndpointId, |
| 265 | ), |
| 266 | ), |
| 267 | ), |
| 268 | Effect.catch((error) => |
| 269 | isNotFoundError(error) |
| 270 | ? Effect.succeed([] as string[]) |
| 271 | : Effect.fail(error), |
| 272 | ), |
| 273 | ), |
| 274 | (remainingIds) => remainingIds.length === 0, |
| 275 | 30, |
| 276 | 5_000, |
| 277 | ); |
| 278 | }); |
| 279 | |
| 280 | const deleteNatGateways = Effect.gen(function* () { |
| 281 | const ids = uniq( |
| 282 | (yield* ec2.describeNatGateways({} as any)).NatGateways?.filter( |
| 283 | (natGateway: any) => natGateway.State !== "deleted", |
| 284 | ).map((natGateway: any) => natGateway.NatGatewayId), |
| 285 | ); |
| 286 | |
| 287 | if (ids.length === 0) { |
| 288 | return; |
| 289 | } |
| 290 | |
| 291 | yield* preview("NAT gateway(s)", ids); |
| 292 | if (!execute) { |
| 293 | return; |
| 294 | } |
| 295 | |
| 296 | yield* Effect.forEach( |
| 297 | ids, |
| 298 | (id) => |
| 299 | ignoreFailure( |
| 300 | `NAT gateway ${id}`, |
| 301 | ec2.deleteNatGateway({ NatGatewayId: id } as any), |
| 302 | ), |
| 303 | { concurrency: 1, discard: true }, |
| 304 | ); |
| 305 | |
| 306 | yield* repeatUntil( |
| 307 | "NAT gateways to delete", |
| 308 | ec2 |
| 309 | .describeNatGateways({} as any) |
| 310 | .pipe( |
| 311 | Effect.map((result) => |
| 312 | uniq( |
| 313 | (result.NatGateways ?? []) |
| 314 | .filter((natGateway: any) => |
| 315 | ids.includes(natGateway.NatGatewayId), |
| 316 | ) |
| 317 | .map((natGateway: any) => natGateway.State), |
| 318 | ), |
| 319 | ), |
| 320 | ), |
| 321 | (states) => states.every((state) => state === "deleted"), |
| 322 | 30, |
| 323 | 10_000, |
| 324 | ); |
| 325 | }); |
| 326 | |
| 327 | const deleteNetworkInterfaces = Effect.gen(function* () { |
| 328 | const interfaces = |
| 329 | (yield* ec2.describeNetworkInterfaces({} as any)).NetworkInterfaces ?? []; |
| 330 | |
| 331 | const blocked = interfaces.filter( |
| 332 | (networkInterface: any) => |
| 333 | networkInterface.Attachment || |
| 334 | networkInterface.RequesterManaged || |
| 335 | networkInterface.Status !== "available", |
| 336 | ); |
| 337 | |
| 338 | if (blocked.length > 0) { |
| 339 | return yield* Effect.fail( |
| 340 | new Error( |
| 341 | `Network interfaces still in use: ${blocked |
| 342 | .map( |
| 343 | (networkInterface: any) => |
| 344 | `${networkInterface.NetworkInterfaceId}(status=${networkInterface.Status ?? "unknown"}, requesterManaged=${networkInterface.RequesterManaged ?? false}, attachment=${networkInterface.Attachment?.AttachmentId ?? "none"})`, |
| 345 | ) |
| 346 | .join(", ")}`, |
| 347 | ), |
| 348 | ); |
| 349 | } |
| 350 | |
| 351 | const ids = uniq( |
| 352 | interfaces.map( |
| 353 | (networkInterface: any) => networkInterface.NetworkInterfaceId, |
| 354 | ), |
| 355 | ); |
| 356 | |
| 357 | yield* deleteEach("network interface(s)", ids, (id) => |
| 358 | ec2.deleteNetworkInterface({ NetworkInterfaceId: id } as any), |
| 359 | ); |
| 360 | }); |
| 361 | |
| 362 | const releaseElasticIps = Effect.gen(function* () { |
| 363 | const addresses = (yield* ec2.describeAddresses({} as any)).Addresses ?? []; |
| 364 | const ids = uniq( |
| 365 | addresses.map((address: any) => address.AllocationId ?? address.PublicIp), |
| 366 | ); |
| 367 | |
| 368 | if (ids.length === 0) { |
| 369 | return; |
| 370 | } |
| 371 | |
| 372 | yield* preview("elastic IP(s)", ids); |
| 373 | if (!execute) { |
| 374 | return; |
| 375 | } |
| 376 | |
| 377 | yield* Effect.forEach( |
| 378 | addresses, |
| 379 | (address: any) => |
| 380 | Effect.gen(function* () { |
| 381 | if (address.AssociationId) { |
| 382 | yield* ignoreFailure( |
| 383 | `elastic IP association ${address.AssociationId}`, |
| 384 | ec2.disassociateAddress({ |
| 385 | AssociationId: address.AssociationId, |
| 386 | } as any), |
| 387 | ); |
| 388 | } |
| 389 | yield* ignoreFailure( |
| 390 | `elastic IP ${address.AllocationId ?? address.PublicIp}`, |
| 391 | ec2.releaseAddress( |
| 392 | address.AllocationId |
| 393 | ? ({ AllocationId: address.AllocationId } as any) |
| 394 | : ({ PublicIp: address.PublicIp } as any), |
| 395 | ), |
| 396 | ); |
| 397 | }), |
| 398 | { concurrency: 1, discard: true }, |
| 399 | ); |
| 400 | }); |
| 401 | |
| 402 | const deleteVpcPeeringConnections = Effect.gen(function* () { |
| 403 | const ids = uniq( |
| 404 | (yield* ec2.describeVpcPeeringConnections( |
| 405 | {} as any, |
| 406 | )).VpcPeeringConnections?.filter( |
| 407 | (connection: any) => connection.Status?.Code !== "deleted", |
| 408 | ).map((connection: any) => connection.VpcPeeringConnectionId), |
| 409 | ); |
| 410 | |
| 411 | yield* deleteEach("VPC peering connection(s)", ids, (id) => |
| 412 | ec2.deleteVpcPeeringConnection({ VpcPeeringConnectionId: id } as any), |
| 413 | ); |
| 414 | }); |
| 415 | |
| 416 | const deleteVpnConnections = Effect.gen(function* () { |
| 417 | const ids = uniq( |
| 418 | (yield* ec2.describeVpnConnections({} as any)).VpnConnections?.filter( |
| 419 | (connection: any) => connection.State !== "deleted", |
| 420 | ).map((connection: any) => connection.VpnConnectionId), |
| 421 | ); |
| 422 | |
| 423 | yield* deleteEach("VPN connection(s)", ids, (id) => |
| 424 | ec2.deleteVpnConnection({ VpnConnectionId: id } as any), |
| 425 | ); |
| 426 | }); |
| 427 | |
| 428 | const deleteVpnGateways = Effect.gen(function* () { |
| 429 | const gateways = |
| 430 | (yield* ec2.describeVpnGateways({} as any)).VpnGateways ?? []; |
| 431 | const ids = uniq( |
| 432 | gateways |
| 433 | .filter((gateway: any) => gateway.State !== "deleted") |
| 434 | .map((gateway: any) => gateway.VpnGatewayId), |
| 435 | ); |
| 436 | |
| 437 | if (ids.length === 0) { |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | yield* preview("VPN gateway(s)", ids); |
| 442 | if (!execute) { |
| 443 | return; |
| 444 | } |
| 445 | |
| 446 | yield* Effect.forEach( |
| 447 | gateways, |
| 448 | (gateway: any) => |
| 449 | Effect.gen(function* () { |
| 450 | for (const attachment of gateway.VpcAttachments ?? []) { |
| 451 | if (attachment.VpcId && attachment.State !== "detached") { |
| 452 | yield* ignoreFailure( |
| 453 | `VPN gateway attachment ${gateway.VpnGatewayId}`, |
| 454 | ec2.detachVpnGateway({ |
| 455 | VpcId: attachment.VpcId, |
| 456 | VpnGatewayId: gateway.VpnGatewayId, |
| 457 | } as any), |
| 458 | ); |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | yield* ignoreFailure( |
| 463 | `VPN gateway ${gateway.VpnGatewayId}`, |
| 464 | ec2.deleteVpnGateway({ VpnGatewayId: gateway.VpnGatewayId } as any), |
| 465 | ); |
| 466 | }), |
| 467 | { concurrency: 1, discard: true }, |
| 468 | ); |
| 469 | }); |
| 470 | |
| 471 | const deleteCustomerGateways = Effect.gen(function* () { |
| 472 | const ids = uniq( |
| 473 | (yield* ec2.describeCustomerGateways({} as any)).CustomerGateways?.map( |
| 474 | (gateway: any) => gateway.CustomerGatewayId, |
| 475 | ), |
| 476 | ); |
| 477 | |
| 478 | yield* deleteEach("customer gateway(s)", ids, (id) => |
| 479 | ec2.deleteCustomerGateway({ CustomerGatewayId: id } as any), |
| 480 | ); |
| 481 | }); |
| 482 | |
| 483 | const deleteInternetGateways = Effect.gen(function* () { |
| 484 | const gateways = |
| 485 | (yield* ec2.describeInternetGateways({} as any)).InternetGateways ?? []; |
| 486 | const ids = uniq(gateways.map((gateway: any) => gateway.InternetGatewayId)); |
| 487 | |
| 488 | if (ids.length === 0) { |
| 489 | return; |
| 490 | } |
| 491 | |
| 492 | yield* preview("internet gateway(s)", ids); |
| 493 | if (!execute) { |
| 494 | return; |
| 495 | } |
| 496 | |
| 497 | yield* Effect.forEach( |
| 498 | gateways, |
| 499 | (gateway: any) => |
| 500 | Effect.gen(function* () { |
| 501 | for (const attachment of gateway.Attachments ?? []) { |
| 502 | if (attachment.VpcId) { |
| 503 | yield* ignoreFailure( |
| 504 | `internet gateway attachment ${gateway.InternetGatewayId}`, |
| 505 | ec2.detachInternetGateway({ |
| 506 | InternetGatewayId: gateway.InternetGatewayId, |
| 507 | VpcId: attachment.VpcId, |
| 508 | } as any), |
| 509 | ); |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | yield* ignoreFailure( |
| 514 | `internet gateway ${gateway.InternetGatewayId}`, |
| 515 | ec2.deleteInternetGateway({ |
| 516 | InternetGatewayId: gateway.InternetGatewayId, |
| 517 | } as any), |
| 518 | ); |
| 519 | }), |
| 520 | { concurrency: 1, discard: true }, |
| 521 | ); |
| 522 | }); |
| 523 | |
| 524 | const deleteEgressOnlyInternetGateways = Effect.gen(function* () { |
| 525 | const gateways = |
| 526 | (yield* ec2.describeEgressOnlyInternetGateways({} as any)) |
| 527 | .EgressOnlyInternetGateways ?? []; |
| 528 | const ids = uniq( |
| 529 | gateways.map((gateway: any) => gateway.EgressOnlyInternetGatewayId), |
| 530 | ); |
| 531 | |
| 532 | yield* deleteEach("egress-only internet gateway(s)", ids, (id) => |
| 533 | ec2.deleteEgressOnlyInternetGateway({ |
| 534 | EgressOnlyInternetGatewayId: id, |
| 535 | } as any), |
| 536 | ); |
| 537 | }); |
| 538 | |
| 539 | const deleteRouteTables = Effect.gen(function* () { |
| 540 | const routeTables = |
| 541 | (yield* ec2.describeRouteTables({} as any)).RouteTables?.filter( |
| 542 | (routeTable: any) => |
| 543 | !(routeTable.Associations ?? []).some( |
| 544 | (association: any) => association.Main, |
| 545 | ), |
| 546 | ) ?? []; |
| 547 | |
| 548 | const ids = uniq( |
| 549 | routeTables.map((routeTable: any) => routeTable.RouteTableId), |
| 550 | ); |
| 551 | |
| 552 | if (ids.length === 0) { |
| 553 | return; |
| 554 | } |
| 555 | |
| 556 | yield* preview("non-main route table(s)", ids); |
| 557 | if (!execute) { |
| 558 | return; |
| 559 | } |
| 560 | |
| 561 | yield* Effect.forEach( |
| 562 | routeTables, |
| 563 | (routeTable: any) => |
| 564 | Effect.gen(function* () { |
| 565 | for (const association of routeTable.Associations ?? []) { |
| 566 | if (!association.Main && association.RouteTableAssociationId) { |
| 567 | yield* ignoreFailure( |
| 568 | `route table association ${association.RouteTableAssociationId}`, |
| 569 | ec2.disassociateRouteTable({ |
| 570 | AssociationId: association.RouteTableAssociationId, |
| 571 | } as any), |
| 572 | ); |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | yield* ignoreFailure( |
| 577 | `route table ${routeTable.RouteTableId}`, |
| 578 | ec2.deleteRouteTable({ |
| 579 | RouteTableId: routeTable.RouteTableId, |
| 580 | } as any), |
| 581 | ); |
| 582 | }), |
| 583 | { concurrency: 1, discard: true }, |
| 584 | ); |
| 585 | }); |
| 586 | |
| 587 | const deleteSubnets = Effect.gen(function* () { |
| 588 | const ids = uniq( |
| 589 | (yield* ec2.describeSubnets({} as any)).Subnets?.filter((subnet: any) => |
| 590 | includeDefaultVpcs ? true : !subnet.DefaultForAz, |
| 591 | ).map((subnet: any) => subnet.SubnetId), |
| 592 | ); |
| 593 | |
| 594 | yield* deleteEach("subnet(s)", ids, (id) => |
| 595 | ec2.deleteSubnet({ SubnetId: id } as any), |
| 596 | ); |
| 597 | }); |
| 598 | |
| 599 | const deleteSecurityGroups = Effect.gen(function* () { |
| 600 | const ids = uniq( |
| 601 | (yield* ec2.describeSecurityGroups({} as any)).SecurityGroups?.filter( |
| 602 | (group: any) => group.GroupName !== "default", |
| 603 | ).map((group: any) => group.GroupId), |
| 604 | ); |
| 605 | |
| 606 | yield* deleteEach("security group(s)", ids, (id) => |
| 607 | ec2.deleteSecurityGroup({ GroupId: id } as any), |
| 608 | ); |
| 609 | }); |
| 610 | |
| 611 | const deleteNetworkAcls = Effect.gen(function* () { |
| 612 | const ids = uniq( |
| 613 | (yield* ec2.describeNetworkAcls({} as any)).NetworkAcls?.filter( |
| 614 | (acl: any) => !acl.IsDefault, |
| 615 | ).map((acl: any) => acl.NetworkAclId), |
| 616 | ); |
| 617 | |
| 618 | yield* deleteEach("network ACL(s)", ids, (id) => |
| 619 | ec2.deleteNetworkAcl({ NetworkAclId: id } as any), |
| 620 | ); |
| 621 | }); |
| 622 | |
| 623 | const deleteAvailableVolumes = Effect.gen(function* () { |
| 624 | const ids = uniq( |
| 625 | (yield* ec2.describeVolumes({} as any)).Volumes?.filter( |
| 626 | (volume: any) => volume.State === "available", |
| 627 | ).map((volume: any) => volume.VolumeId), |
| 628 | ); |
| 629 | |
| 630 | yield* deleteEach("available volume(s)", ids, (id) => |
| 631 | ec2.deleteVolume({ VolumeId: id } as any), |
| 632 | ); |
| 633 | }); |
| 634 | |
| 635 | const deregisterImages = Effect.gen(function* () { |
| 636 | const ids = uniq( |
| 637 | (yield* ec2.describeImages({ Owners: ["self"] } as any)).Images?.map( |
| 638 | (image: any) => image.ImageId, |
| 639 | ), |
| 640 | ); |
| 641 | |
| 642 | yield* deleteEach("AMI(s)", ids, (id) => |
| 643 | ec2.deregisterImage({ ImageId: id } as any), |
| 644 | ); |
| 645 | }); |
| 646 | |
| 647 | const deleteSnapshots = Effect.gen(function* () { |
| 648 | const ids = uniq( |
| 649 | (yield* ec2.describeSnapshots({ |
| 650 | OwnerIds: ["self"], |
| 651 | } as any)).Snapshots?.map((snapshot: any) => snapshot.SnapshotId), |
| 652 | ); |
| 653 | |
| 654 | yield* deleteEach("snapshot(s)", ids, (id) => |
| 655 | ec2.deleteSnapshot({ SnapshotId: id } as any), |
| 656 | ); |
| 657 | }); |
| 658 | |
| 659 | const deleteLaunchTemplates = Effect.gen(function* () { |
| 660 | const ids = uniq( |
| 661 | (yield* ec2.describeLaunchTemplates({} as any)).LaunchTemplates?.map( |
| 662 | (template: any) => template.LaunchTemplateId, |
| 663 | ), |
| 664 | ); |
| 665 | |
| 666 | yield* deleteEach("launch template(s)", ids, (id) => |
| 667 | ec2.deleteLaunchTemplate({ LaunchTemplateId: id } as any), |
| 668 | ); |
| 669 | }); |
| 670 | |
| 671 | const deletePlacementGroups = Effect.gen(function* () { |
| 672 | const ids = uniq( |
| 673 | (yield* ec2.describePlacementGroups({} as any)).PlacementGroups?.map( |
| 674 | (group: any) => group.GroupName, |
| 675 | ), |
| 676 | ); |
| 677 | |
| 678 | yield* deleteEach("placement group(s)", ids, (id) => |
| 679 | ec2.deletePlacementGroup({ GroupName: id } as any), |
| 680 | ); |
| 681 | }); |
| 682 | |
| 683 | const deleteKeyPairs = Effect.gen(function* () { |
| 684 | const ids = uniq( |
| 685 | (yield* ec2.describeKeyPairs({} as any)).KeyPairs?.map( |
| 686 | (keyPair: any) => keyPair.KeyPairId, |
| 687 | ), |
| 688 | ); |
| 689 | |
| 690 | yield* deleteEach("key pair(s)", ids, (id) => |
| 691 | ec2.deleteKeyPair({ KeyPairId: id } as any), |
| 692 | ); |
| 693 | }); |
| 694 | |
| 695 | const deleteVpcs = Effect.gen(function* () { |
| 696 | const ids = uniq( |
| 697 | (yield* ec2.describeVpcs({} as any)).Vpcs?.filter((vpc: any) => |
| 698 | includeDefaultVpcs ? true : !vpc.IsDefault, |
| 699 | ).map((vpc: any) => vpc.VpcId), |
| 700 | ); |
| 701 | |
| 702 | yield* deleteEach("VPC(s)", ids, (id) => ec2.deleteVpc({ VpcId: id } as any)); |
| 703 | }); |
| 704 | |
| 705 | const deleteDhcpOptions = Effect.gen(function* () { |
| 706 | const associatedDhcpOptionsIds = new Set( |
| 707 | uniq( |
| 708 | (yield* ec2.describeVpcs({} as any)).Vpcs?.map( |
| 709 | (vpc: any) => vpc.DhcpOptionsId, |
| 710 | ), |
| 711 | ), |
| 712 | ); |
| 713 | |
| 714 | const ids = uniq( |
| 715 | (yield* ec2.describeDhcpOptions({} as any)).DhcpOptions?.map( |
| 716 | (dhcpOptions: any) => dhcpOptions.DhcpOptionsId, |
| 717 | ).filter( |
| 718 | (id: string) => id !== "default" && !associatedDhcpOptionsIds.has(id), |
| 719 | ), |
| 720 | ); |
| 721 | |
| 722 | yield* deleteEach("DHCP options set(s)", ids, (id) => |
| 723 | ec2.deleteDhcpOptions({ DhcpOptionsId: id } as any), |
| 724 | ); |
| 725 | }); |
| 726 | |
| 727 | const main = Effect.gen(function* () { |
| 728 | yield* log( |
| 729 | [ |
| 730 | `Mode=${execute ? "execute" : "dry-run"}`, |
| 731 | `Region=${region}`, |
| 732 | values.profile ? `Profile=${values.profile}` : undefined, |
| 733 | includeDefaultVpcs ? "Including default VPCs" : "Skipping default VPCs", |
| 734 | ] |
| 735 | .filter((value): value is string => !!value) |
| 736 | .join(" | "), |
| 737 | ); |
| 738 | |
| 739 | for (let pass = 1; pass <= maxPasses; pass++) { |
| 740 | yield* log(`\n=== Cleanup pass ${pass}/${maxPasses} ===`); |
| 741 | |
| 742 | yield* terminateInstances; |
| 743 | yield* deleteVpcEndpoints; |
| 744 | yield* deleteNatGateways; |
| 745 | yield* deleteNetworkInterfaces; |
| 746 | yield* releaseElasticIps; |
| 747 | yield* deleteVpnConnections; |
| 748 | yield* deleteVpnGateways; |
| 749 | yield* deleteCustomerGateways; |
| 750 | yield* deleteVpcPeeringConnections; |
| 751 | yield* deleteInternetGateways; |
| 752 | yield* deleteEgressOnlyInternetGateways; |
| 753 | yield* deleteRouteTables; |
| 754 | yield* deleteSubnets; |
| 755 | yield* deleteSecurityGroups; |
| 756 | yield* deleteNetworkAcls; |
| 757 | yield* deleteAvailableVolumes; |
| 758 | yield* deregisterImages; |
| 759 | yield* deleteSnapshots; |
| 760 | yield* deleteLaunchTemplates; |
| 761 | yield* deletePlacementGroups; |
| 762 | yield* deleteKeyPairs; |
| 763 | yield* deleteVpcs; |
| 764 | yield* deleteDhcpOptions; |
| 765 | |
| 766 | if (!execute) { |
| 767 | yield* log( |
| 768 | "\nDry-run complete. Re-run with --execute to perform deletions.", |
| 769 | ); |
| 770 | return; |
| 771 | } |
| 772 | } |
| 773 | |
| 774 | yield* log("\nCleanup passes complete."); |
| 775 | }); |
| 776 | |
| 777 | Effect.runPromise(main.pipe(Effect.provide(runtime))).catch((error) => { |
| 778 | console.error(formatError(error)); |
| 779 | process.exit(1); |
| 780 | }); |