commonware_deployer/ec2/
destroy.rs

1//! `destroy` subcommand for `ec2`
2
3use crate::ec2::{
4    aws::*,
5    deployer_directory,
6    s3::{
7        create_s3_client, delete_prefix, is_no_such_bucket_error, Region, S3_BUCKET_NAME,
8        S3_DEPLOYMENTS_PREFIX,
9    },
10    Config, Error, DESTROYED_FILE_NAME, LOGS_PORT, MONITORING_REGION, PROFILES_PORT, TRACES_PORT,
11};
12use futures::future::try_join_all;
13use std::{collections::HashSet, fs::File, path::PathBuf};
14use tracing::{info, warn};
15
16/// Tears down all resources associated with the deployment tag
17pub async fn destroy(config: &PathBuf) -> Result<(), Error> {
18    // Load configuration
19    let config: Config = {
20        let config_file = File::open(config)?;
21        serde_yaml::from_reader(config_file)?
22    };
23    let tag = &config.tag;
24    info!(tag = tag.as_str(), "loaded configuration");
25
26    // Ensure deployment directory exists
27    let tag_directory = deployer_directory(tag);
28    if !tag_directory.exists() {
29        return Err(Error::DeploymentDoesNotExist(tag.clone()));
30    }
31
32    // Ensure not already destroyed
33    let destroyed_file = tag_directory.join(DESTROYED_FILE_NAME);
34    if destroyed_file.exists() {
35        warn!("infrastructure already destroyed");
36        return Ok(());
37    }
38
39    // Clean up S3 deployment data (preserves cached tools)
40    info!(bucket = S3_BUCKET_NAME, "cleaning up S3 deployment data");
41    let s3_client = create_s3_client(Region::new(MONITORING_REGION)).await;
42    let deployment_prefix = format!("{}/{}/", S3_DEPLOYMENTS_PREFIX, tag);
43    match delete_prefix(&s3_client, S3_BUCKET_NAME, &deployment_prefix).await {
44        Ok(()) => {
45            info!(
46                bucket = S3_BUCKET_NAME,
47                prefix = deployment_prefix.as_str(),
48                "deleted S3 deployment data"
49            );
50        }
51        Err(e) => {
52            if is_no_such_bucket_error(&e) {
53                info!(
54                    bucket = S3_BUCKET_NAME,
55                    "bucket does not exist, skipping S3 cleanup"
56                );
57            } else {
58                warn!(bucket = S3_BUCKET_NAME, %e, "failed to delete S3 deployment data, continuing with destroy");
59            }
60        }
61    }
62
63    // Determine all regions involved
64    let mut all_regions = HashSet::new();
65    all_regions.insert(MONITORING_REGION.to_string());
66    for instance in &config.instances {
67        all_regions.insert(instance.region.clone());
68    }
69
70    // First pass: Delete instances, security groups, subnets, route tables, peering, IGWs, and key pairs
71    info!(regions=?all_regions, "removing resources");
72    let jobs = all_regions.iter().map(|region| {
73        let region = region.clone();
74        let tag = tag.clone();
75        async move {
76            let ec2_client = create_ec2_client(Region::new(region.clone())).await;
77            info!(region = region.as_str(), "created EC2 client");
78
79            let instance_ids = find_instances_by_tag(&ec2_client, &tag).await?;
80            if !instance_ids.is_empty() {
81                terminate_instances(&ec2_client, &instance_ids).await?;
82                wait_for_instances_terminated(&ec2_client, &instance_ids).await?;
83                info!(
84                    region = region.as_str(),
85                    ?instance_ids,
86                    "terminated instances"
87                );
88            }
89
90            // If in the monitoring region, we need to revoke the ingress rule
91            let security_groups = find_security_groups_by_tag(&ec2_client, &tag).await?;
92            let has_monitoring_sg = security_groups
93                .iter()
94                .any(|sg| sg.group_name() == Some(&tag));
95            let has_binary_sg = security_groups
96                .iter()
97                .any(|sg| sg.group_name() == Some(&format!("{tag}-binary")));
98            if region == MONITORING_REGION && has_monitoring_sg && has_binary_sg {
99                // Find the monitoring security group (named `tag`)
100                let monitoring_sg = security_groups
101                    .iter()
102                    .find(|sg| sg.group_name() == Some(&tag))
103                    .expect("Monitoring security group not found")
104                    .group_id()
105                    .unwrap();
106
107                // Find the binary security group (named `{tag}-binary`)
108                let binary_sg = security_groups
109                    .iter()
110                    .find(|sg| sg.group_name() == Some(&format!("{tag}-binary")))
111                    .expect("Regular security group not found")
112                    .group_id()
113                    .unwrap();
114
115                // Revoke ingress rule from monitoring security group to binary security group
116                let logging_permission = IpPermission::builder()
117                    .ip_protocol("tcp")
118                    .from_port(LOGS_PORT as i32)
119                    .to_port(LOGS_PORT as i32)
120                    .user_id_group_pairs(UserIdGroupPair::builder().group_id(binary_sg).build())
121                    .build();
122                if let Err(e) = ec2_client
123                    .revoke_security_group_ingress()
124                    .group_id(monitoring_sg)
125                    .ip_permissions(logging_permission)
126                    .send()
127                    .await
128                {
129                    warn!(%e, "failed to revoke logs ingress rule between monitoring and binary security groups");
130                } else {
131                    info!(
132                        monitoring_sg,
133                        binary_sg,
134                        "revoked logs ingress rule between monitoring and binary security groups"
135                    );
136                }
137
138                // Revoke ingress rule from monitoring security group to binary security group
139                let profiling_permission = IpPermission::builder()
140                    .ip_protocol("tcp")
141                    .from_port(PROFILES_PORT as i32)
142                    .to_port(PROFILES_PORT as i32)
143                    .user_id_group_pairs(UserIdGroupPair::builder().group_id(binary_sg).build())
144                    .build();
145                if let Err(e) = ec2_client
146                    .revoke_security_group_ingress()
147                    .group_id(monitoring_sg)
148                    .ip_permissions(profiling_permission)
149                    .send()
150                    .await
151                {
152                    warn!(%e, "failed to revoke profiles ingress rule between monitoring and binary security groups");
153                } else {
154                    info!(
155                        monitoring_sg,
156                        binary_sg,
157                        "revoked profiles ingress rule between monitoring and binary security groups"
158                    );
159                }
160
161                // Revoke ingress rule from monitoring security group to binary security group
162                let tracing_permission = IpPermission::builder()
163                    .ip_protocol("tcp")
164                    .from_port(TRACES_PORT as i32)
165                    .to_port(TRACES_PORT as i32)
166                    .user_id_group_pairs(UserIdGroupPair::builder().group_id(binary_sg).build())
167                    .build();
168                if let Err(e) = ec2_client
169                    .revoke_security_group_ingress()
170                    .group_id(monitoring_sg)
171                    .ip_permissions(tracing_permission)
172                    .send()
173                    .await
174                {
175                    warn!(%e, "failed to revoke traces ingress rule between monitoring and binary security groups");
176                } else {
177                    info!(
178                        monitoring_sg,
179                        binary_sg,
180                        "revoked traces ingress rule between monitoring and binary security groups"
181                    );
182                }
183            }
184
185            // Remove network resources
186            let sgs = find_security_groups_by_tag(&ec2_client, &tag).await?;
187            for sg in sgs {
188                let sg_id = sg.group_id().unwrap();
189                wait_for_enis_deleted(&ec2_client, sg_id).await?;
190                info!(
191                    region = region.as_str(),
192                    sg_id, "ENIs deleted from security group"
193                );
194                delete_security_group(&ec2_client, sg_id).await?;
195                info!(region = region.as_str(), sg_id, "deleted security group");
196            }
197
198            let subnet_ids = find_subnets_by_tag(&ec2_client, &tag).await?;
199            for subnet_id in subnet_ids {
200                delete_subnet(&ec2_client, &subnet_id).await?;
201                info!(region = region.as_str(), subnet_id, "deleted subnet");
202            }
203
204            let route_table_ids = find_route_tables_by_tag(&ec2_client, &tag).await?;
205            for rt_id in route_table_ids {
206                delete_route_table(&ec2_client, &rt_id).await?;
207                info!(region = region.as_str(), rt_id, "deleted route table");
208            }
209
210            let peering_ids = find_vpc_peering_by_tag(&ec2_client, &tag).await?;
211            for peering_id in peering_ids {
212                delete_vpc_peering(&ec2_client, &peering_id).await?;
213                wait_for_vpc_peering_deletion(&ec2_client, &peering_id).await?;
214                info!(
215                    region = region.as_str(),
216                    peering_id, "deleted VPC peering connection"
217                );
218            }
219
220            let igw_ids = find_igws_by_tag(&ec2_client, &tag).await?;
221            for igw_id in igw_ids {
222                let vpc_id = find_vpc_by_igw(&ec2_client, &igw_id).await?;
223                detach_igw(&ec2_client, &igw_id, &vpc_id).await?;
224                info!(
225                    region = region.as_str(),
226                    igw_id, vpc_id, "detached internet gateway"
227                );
228                delete_igw(&ec2_client, &igw_id).await?;
229                info!(region = region.as_str(), igw_id, "deleted internet gateway");
230            }
231
232            let key_name = format!("deployer-{tag}");
233            delete_key_pair(&ec2_client, &key_name).await?;
234            info!(region = region.as_str(), key_name, "deleted key pair");
235            Ok::<(), Error>(())
236        }
237    });
238    try_join_all(jobs).await?;
239
240    // Second pass: Delete VPCs after dependencies are removed
241    for region in &all_regions {
242        let ec2_client = create_ec2_client(Region::new(region.clone())).await;
243        info!(region = region.as_str(), "created EC2 client");
244        let vpc_ids = find_vpcs_by_tag(&ec2_client, tag).await?;
245        for vpc_id in vpc_ids {
246            delete_vpc(&ec2_client, &vpc_id).await?;
247            info!(region = region.as_str(), vpc_id, "deleted VPC");
248        }
249    }
250    info!(regions = ?all_regions, "resources removed");
251
252    // Write destruction file
253    File::create(destroyed_file)?;
254
255    // We don't delete the temporary directory to prevent re-deployment of the same tag
256    info!(tag = tag.as_str(), "destruction complete");
257    Ok(())
258}