--- # AWS Cleanup Playbook - Comprehensive version with robust VPC removal - name: Clean up AWS resources hosts: localhost gather_facts: false connection: local vars_files: - vars.yaml vars: aws_region: "{{ aws_region | default(aws_region_choices | random) }}" confirm_cleanup: "{{ confirm_cleanup | default(true) }}" deployment_id: "{{ deployment_id | default('') }}" redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}" c2_name: "{{ c2_name | default('s-' + deployment_id) }}" tracker_name: "{{ tracker_name | default('t-' + deployment_id) }}" cleanup_summary: {} tasks: # Confirmation step (if enabled) - name: Confirm cleanup pause: prompt: "Are you sure you want to delete all AWS resources for deployment ID {{ deployment_id }}? This action cannot be undone. Type 'yes' to confirm" register: cleanup_confirmation when: confirm_cleanup | bool - name: Check confirmation assert: that: - cleanup_confirmation.user_input | default('yes') == 'yes' fail_msg: "Cleanup cancelled by user" when: confirm_cleanup | bool # STEP 1: Find all instances by deployment ID - name: Find all EC2 instances for this deployment amazon.aws.ec2_instance_info: region: "{{ aws_region }}" filters: "tag:deployment_id": "{{ deployment_id }}" register: deployment_instances - name: Set fact for instances found set_fact: cleanup_summary: "{{ cleanup_summary | combine({'instances_found': deployment_instances.instances | length}) }}" # STEP 2: Terminate all instances with proper tagging - name: Terminate all instances for this deployment amazon.aws.ec2_instance: instance_ids: "{{ item.instance_id }}" region: "{{ aws_region }}" state: absent loop: "{{ deployment_instances.instances }}" register: terminated_instances when: deployment_instances.instances | length > 0 - name: Wait for instances to be terminated pause: seconds: 30 when: deployment_instances.instances | length > 0 # STEP 3: Find all security groups by deployment ID - name: Find all security groups for this deployment amazon.aws.ec2_security_group_info: region: "{{ aws_region }}" filters: "tag:deployment_id": "{{ deployment_id }}" register: deployment_sgs # Also find SGs by name pattern - name: Find security groups by name pattern amazon.aws.ec2_security_group_info: region: "{{ aws_region }}" register: all_sgs - name: Filter SGs by name pattern set_fact: named_sgs: "{{ all_sgs.security_groups | selectattr('group_name', 'search', redirector_name + '-sg|' + c2_name + '-sg') | list }}" - name: Combine all security groups to delete set_fact: all_sgs_to_delete: "{{ deployment_sgs.security_groups + named_sgs }}" cleanup_summary: "{{ cleanup_summary | combine({'security_groups_found': (deployment_sgs.security_groups + named_sgs) | length}) }}" # STEP 4: Delete all security groups - name: Delete security groups amazon.aws.ec2_security_group: group_id: "{{ item.group_id }}" region: "{{ aws_region }}" state: absent loop: "{{ all_sgs_to_delete }}" when: all_sgs_to_delete | length > 0 ignore_errors: yes register: deleted_sgs # STEP 5: Find and delete all ENIs - name: Find network interfaces by tag amazon.aws.ec2_eni_info: region: "{{ aws_region }}" filters: "tag:deployment_id": "{{ deployment_id }}" register: deployment_enis - name: Delete ENIs amazon.aws.ec2_eni: region: "{{ aws_region }}" eni_id: "{{ item.id }}" state: absent force_detach: true loop: "{{ deployment_enis.network_interfaces }}" ignore_errors: yes register: deleted_enis when: deployment_enis.network_interfaces | length > 0 - name: Set ENIs count in summary set_fact: cleanup_summary: "{{ cleanup_summary | combine({'enis_found': deployment_enis.network_interfaces | length}) }}" # STEP 6: Find all VPCs by deployment ID - name: Find all VPCs for this deployment amazon.aws.ec2_vpc_net_info: region: "{{ aws_region }}" filters: "tag:deployment_id": "{{ deployment_id }}" register: deployment_vpcs # STEP 7: Find VPCs by name pattern as fallback - name: Find all VPCs by name pattern amazon.aws.ec2_vpc_net_info: region: "{{ aws_region }}" register: all_vpcs - name: Filter VPCs by name pattern set_fact: named_vpcs: "{{ all_vpcs.vpcs | selectattr('tags', 'defined') | selectattr('tags.Name', 'defined') | selectattr('tags.Name', 'search', redirector_name + '-vpc|' + c2_name + '-vpc') | list }}" - name: Combine all VPCs to delete set_fact: all_vpcs_to_delete: "{{ deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id') }}" cleanup_summary: "{{ cleanup_summary | combine({'vpcs_found': (deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id')) | length}) }}" # STEP 8: Find and delete NAT Gateways for each VPC separately - name: Find NAT gateways in each VPC amazon.aws.ec2_vpc_nat_gateway_info: region: "{{ aws_region }}" filters: vpc-id: "{{ item.vpc_id }}" register: natgw_results loop: "{{ all_vpcs_to_delete }}" when: all_vpcs_to_delete | length > 0 - name: Delete NAT gateways amazon.aws.ec2_vpc_nat_gateway: region: "{{ aws_region }}" nat_gateway_id: "{{ item.1.nat_gateway_id }}" state: absent release_eip: true loop: "{{ natgw_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('nat_gateways', 'defined') | subelements('nat_gateways') }}" ignore_errors: yes register: deleted_natgws when: natgw_results.results is defined - name: Wait after NAT deletion pause: seconds: 15 when: deleted_natgws.results is defined and deleted_natgws.results | length > 0 # STEP 9: Find and delete Internet Gateways - name: Find internet gateways for each VPC amazon.aws.ec2_vpc_igw_info: region: "{{ aws_region }}" filters: attachment.vpc-id: "{{ item.vpc_id }}" register: igw_results loop: "{{ all_vpcs_to_delete }}" when: all_vpcs_to_delete | length > 0 - name: Detach and delete internet gateways amazon.aws.ec2_vpc_igw: internet_gateway_id: "{{ item.1.internet_gateway_id }}" state: absent region: "{{ aws_region }}" loop: "{{ igw_results.results | default([]) | subelements('internet_gateways') }}" ignore_errors: yes register: deleted_igws - name: Wait after IGW deletion pause: seconds: 15 when: deleted_igws.results is defined and deleted_igws.results | length > 0 # STEP 10: Find and delete Route Tables - name: Find route tables for each VPC amazon.aws.ec2_vpc_route_table_info: region: "{{ aws_region }}" filters: vpc-id: "{{ item.vpc_id }}" register: rtb_results loop: "{{ all_vpcs_to_delete }}" when: all_vpcs_to_delete | length > 0 - name: Delete non-main route tables amazon.aws.ec2_vpc_route_table: region: "{{ aws_region }}" route_table_id: "{{ item.1.id }}" lookup: id state: absent loop: "{{ rtb_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('route_tables', 'defined') | subelements('route_tables') }}" when: not item.1.associations[0].main | default(false) ignore_errors: yes register: deleted_rtbs # Add this after your existing route table deletion - name: Delete main route tables with AWS CLI shell: | for rtb in $(aws ec2 describe-route-tables --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --query 'RouteTables[?Associations[?Main==`true`]].RouteTableId' --output text); do aws ec2 delete-route --route-table-id $rtb --destination-cidr-block 0.0.0.0/0 --region {{ aws_region }} || true done environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" loop: "{{ all_vpcs_to_delete }}" ignore_errors: yes when: all_vpcs_to_delete | length > 0 # STEP 11: Find and delete Subnets - name: Find subnets for each VPC amazon.aws.ec2_vpc_subnet_info: region: "{{ aws_region }}" filters: vpc-id: "{{ item.vpc_id }}" loop: "{{ all_vpcs_to_delete }}" register: subnet_results - name: Delete subnets amazon.aws.ec2_vpc_subnet: region: "{{ aws_region }}" vpc_id: "{{ item.1.vpc_id }}" cidr: "{{ item.1.cidr_block }}" state: absent loop: "{{ subnet_results.results | default([]) | subelements('subnets') }}" ignore_errors: yes register: deleted_subnets when: subnet_results.results is defined # STEP 12: Find and delete VPC Endpoints - name: Find VPC endpoints for each VPC amazon.aws.ec2_vpc_endpoint_info: region: "{{ aws_region }}" filters: vpc-id: "{{ item.vpc_id }}" register: endpoint_results loop: "{{ all_vpcs_to_delete }}" when: all_vpcs_to_delete | length > 0 - name: Delete VPC endpoints amazon.aws.ec2_vpc_endpoint: region: "{{ aws_region }}" vpc_endpoint_id: "{{ item.1.vpc_endpoint_id }}" state: absent loop: "{{ endpoint_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('vpc_endpoints', 'defined') | subelements('vpc_endpoints') }}" ignore_errors: yes register: deleted_endpoints when: endpoint_results.results is defined # Add before STEP 13 - name: Check for remaining VPC dependencies shell: | aws ec2 describe-network-interfaces --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --output json environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" register: remaining_deps loop: "{{ all_vpcs_to_delete }}" when: all_vpcs_to_delete | length > 0 - name: Display any remaining dependencies debug: msg: "VPC {{ item.item.vpc_id }} still has dependencies that need to be removed" loop: "{{ remaining_deps.results }}" when: item.stdout | from_json | json_query('NetworkInterfaces') | length > 0 # Add this before the force delete of network interfaces - name: Detach remaining network interfaces shell: | aws ec2 detach-network-interface --attachment-id $(aws ec2 describe-network-interfaces --network-interface-ids {{ item.1 }} --query 'NetworkInterfaces[0].Attachment.AttachmentId' --output text) --region {{ aws_region }} --force environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | map('attr', 'stdout') | map('from_json') | map('json_query', 'NetworkInterfaces[?Status==`in-use`].NetworkInterfaceId') | zip(remaining_deps.results | map('attr', 'item')) | list }}" ignore_errors: yes when: item.0 | length > 0 - name: Force delete any remaining network interfaces shell: | aws ec2 delete-network-interface --network-interface-id {{ item.1 }} --region {{ aws_region }} environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') | map('attr', 'stdout') | map('from_json') | map('json_query', 'NetworkInterfaces[].NetworkInterfaceId') | zip(remaining_deps.results | map('attr', 'item')) | list }}" ignore_errors: yes when: item.0 | length > 0 # STEP 13: Final VPC deletion with multiple retries - name: Wait for all dependencies to clear pause: seconds: 20 when: all_vpcs_to_delete | length > 0 # First attempt with normal module - with error display - name: Delete all VPCs (first attempt) amazon.aws.ec2_vpc_net: vpc_id: "{{ item.vpc_id }}" region: "{{ aws_region }}" state: absent loop: "{{ all_vpcs_to_delete }}" register: vpc_deletion when: all_vpcs_to_delete | length > 0 ignore_errors: yes - name: Display VPC deletion errors debug: msg: "Failed to delete VPC {{ item.item.vpc_id }}: {{ item.msg }}" loop: "{{ vpc_deletion.results | default([]) }}" when: item.failed is defined and item.failed # Direct API call for any VPCs that failed - name: Find which VPCs still exist amazon.aws.ec2_vpc_net_info: region: "{{ aws_region }}" vpc_ids: "{{ all_vpcs_to_delete | map(attribute='vpc_id') | list }}" register: remaining_vpcs when: all_vpcs_to_delete | length > 0 # Forcibly delete with direct AWS CLI command - name: Force delete remaining VPCs with CLI shell: | aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" loop: "{{ remaining_vpcs.vpcs }}" ignore_errors: yes when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 register: force_vpc_delete # Add after the VPC deletion attempts - more aggressive approach - name: Force delete remaining VPCs with AWS CLI and debug output shell: | aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} 2>&1 || echo "Failed with: $?" environment: AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" loop: "{{ remaining_vpcs.vpcs }}" register: force_vpc_delete_debug when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0 - name: Display debug output from force delete debug: msg: "{{ item.stdout }}" loop: "{{ force_vpc_delete_debug.results | default([]) }}" when: item.stdout is defined and item.stdout | trim != "" # Track deleted VPCs in summary - name: Set VPC deletion results in summary set_fact: cleanup_summary: "{{ cleanup_summary | combine({ 'vpcs_deleted': ((vpc_deletion.results | default([]) | selectattr('failed', 'undefined') | list | length) + (force_vpc_delete.results | default([]) | selectattr('failed', 'undefined') | list | length))}) }}" when: all_vpcs_to_delete | length > 0 # Add these tasks to confirm VPC deletion - name: Verify VPC deletion amazon.aws.ec2_vpc_net_info: region: "{{ aws_region }}" filters: "tag:deployment_id": "{{ deployment_id }}" register: vpc_check - name: Display cleanup summary debug: msg: - "Cleanup Summary:" - "Redirector instance deleted: {{ redirector_deleted | default('N/A') }}" - "C2 instance deleted: {{ c2_deleted | default('N/A') }}" - "VPC resources deleted: {{ vpc_check.vpcs | length == 0 }}" when: not disable_summary | default(false) # STEP 14: Delete key pairs - name: Find key pairs by name patterns amazon.aws.ec2_key: name: "{{ item }}" region: "{{ aws_region }}" state: present register: keys_check ignore_errors: yes with_items: - "{{ redirector_name }}" - "{{ c2_name }}" - "{{ tracker_name }}" - name: Delete key pairs amazon.aws.ec2_key: name: "{{ item.invocation.module_args.name }}" region: "{{ aws_region }}" state: absent loop: "{{ keys_check.results }}" when: keys_check.results | length > 0 and item.failed is not defined and item.key is defined register: deleted_keys - name: Count deleted keys set_fact: cleanup_summary: "{{ cleanup_summary | combine({ 'keypairs_deleted': (deleted_keys.results | default([]) | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}" # Add this after your existing key pair finding task - name: Find c2deploy key pairs amazon.aws.ec2_key: name: "c2deploy_{{ deployment_id }}" region: "{{ aws_region }}" state: present register: c2deploy_key_check ignore_errors: yes - name: Delete c2deploy key pairs amazon.aws.ec2_key: name: "c2deploy_{{ deployment_id }}" region: "{{ aws_region }}" state: absent when: not c2deploy_key_check.failed | default(true) register: deleted_c2deploy_key # STEP 15: Delete SSH key files - name: Delete SSH key files file: path: "{{ item }}" state: absent with_items: - "~/.ssh/{{ redirector_name }}.pem" - "~/.ssh/{{ c2_name }}.pem" - "~/.ssh/{{ tracker_name }}.pem" - "~/.ssh/c2deploy_{{ deployment_id }}.pem" # Fix: add .pem extension - "~/.ssh/c2deploy_{{ deployment_id }}.pub" ignore_errors: yes register: deleted_ssh_files - name: Count deleted SSH files set_fact: cleanup_summary: "{{ cleanup_summary | combine({ 'ssh_files_deleted': (deleted_ssh_files.results | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}" # Remove infrastructure state file - fix path to include deployment_id - name: Remove infrastructure state file file: path: "infrastructure_state_{{ deployment_id }}.json" state: absent ignore_errors: yes register: infra_file # STEP 16: Enhanced and Accurate Cleanup Summary - name: Enhanced cleanup summary debug: msg: - "==========================================================" - " AWS CLEANUP SUMMARY: {{ deployment_id }} " - "==========================================================" - "EC2 Instances: {{ cleanup_summary.instances_found | default(0) }} found, {{ terminated_instances.results | default([]) | length }} terminated" - "Security Groups: {{ cleanup_summary.security_groups_found | default(0) }} found, {{ deleted_sgs.results | default([]) | length }} deleted" - "Network Interfaces: {{ cleanup_summary.enis_found | default(0) }} found, {{ deleted_enis.results | default([]) | length }} deleted" - "VPCs: {{ cleanup_summary.vpcs_found | default(0) }} found, {{ cleanup_summary.vpcs_deleted | default(0) }} deleted" - "Key Pairs: {{ cleanup_summary.keypairs_deleted | default(0) }} deleted" - "SSH Key Files: {{ cleanup_summary.ssh_files_deleted | default(0) }} deleted" - "Infrastructure file: {{ 'Removed' if infra_file.changed else 'Not found' }}" - "==========================================================" - "CLEANUP {{ 'COMPLETED' if (cleanup_summary.vpcs_deleted | default(0) == cleanup_summary.vpcs_found | default(0)) else 'PARTIAL - SOME RESOURCES MAY REMAIN' }}" - "========================================================="