diff --git a/hydro_deploy/core/src/aws.rs b/hydro_deploy/core/src/aws.rs index e7c3b72b06b9..fc24e4fbd7b1 100644 --- a/hydro_deploy/core/src/aws.rs +++ b/hydro_deploy/core/src/aws.rs @@ -40,23 +40,32 @@ impl LaunchedSshHost for LaunchedEc2Instance { } } +#[derive(Debug, Clone)] +pub struct NetworkResources { + vpc: String, + subnet: String, + security_group: String, +} + #[derive(Debug)] pub struct AwsNetwork { pub region: String, - pub existing_vpc: OnceLock, + pub existing_network_key: OnceLock, + pub existing_network_id: OnceLock, id: String, } impl AwsNetwork { - pub fn new(region: impl Into, existing_vpc: Option) -> Arc { + pub fn new(region: impl Into, existing_vpc: Option) -> Arc { Arc::new(Self { region: region.into(), - existing_vpc: existing_vpc.map(From::from).unwrap_or_default(), + existing_network_key: OnceLock::new(), + existing_network_id: existing_vpc.map(From::from).unwrap_or_default(), id: nanoid!(8, &TERRAFORM_ALPHABET), }) } - fn collect_resources(&self, resource_batch: &mut ResourceBatch) -> String { + fn collect_resources(&self, resource_batch: &mut ResourceBatch) -> NetworkResources { resource_batch .terraform .terraform @@ -77,29 +86,30 @@ impl AwsNetwork { ); let vpc_network = format!("hydro-vpc-network-{}", self.id); + let subnet_key = format!("{vpc_network}-subnet"); + let sg_key = format!("{vpc_network}-default-sg"); - if let Some(existing) = self.existing_vpc.get() { - if resource_batch - .terraform - .resource - .get("aws_vpc") - .is_some_and(|map| map.contains_key(existing)) - { - format!("aws_vpc.{existing}") - } else { + if let Some(existing) = self.existing_network_id.get() { + let mut resolve = |resource_type: &str, existing_id: &str, data_key: String| { resource_batch .terraform .data - .entry("aws_vpc".to_owned()) + .entry(resource_type.to_owned()) .or_default() - .insert( - vpc_network.clone(), - json!({ - "id": existing, - }), - ); - - format!("data.aws_vpc.{vpc_network}") + .insert(data_key.clone(), json!({ "id": existing_id })); + format!("data.{resource_type}.{data_key}") + }; + + NetworkResources { + vpc: resolve("aws_vpc", &existing.vpc, vpc_network), + subnet: resolve("aws_subnet", &existing.subnet, subnet_key), + security_group: resolve("aws_security_group", &existing.security_group, sg_key), + } + } else if let Some(existing) = self.existing_network_key.get() { + NetworkResources { + vpc: format!("aws_vpc.{}", existing.vpc), + subnet: format!("aws_subnet.{}", existing.subnet), + security_group: format!("aws_security_group.{}", existing.security_group), } } else { resource_batch @@ -137,7 +147,6 @@ impl AwsNetwork { ); // Create subnet - let subnet_key = format!("{vpc_network}-subnet"); resource_batch .terraform .resource @@ -202,14 +211,13 @@ impl AwsNetwork { ); // Create security group that allows internal communication - let sg_key = format!("{vpc_network}-default-sg"); resource_batch .terraform .resource .entry("aws_security_group".to_owned()) .or_default() .insert( - sg_key, + sg_key.clone(), json!({ "name": format!("{vpc_network}-default-allow-internal"), "description": "Allow internal communication between instances", @@ -265,9 +273,53 @@ impl AwsNetwork { }), ); - let out = format!("aws_vpc.{vpc_network}"); - self.existing_vpc.set(vpc_network).unwrap(); - out + let resources = NetworkResources { + vpc: format!("aws_vpc.{vpc_network}"), + subnet: format!("aws_subnet.{subnet_key}"), + security_group: format!("aws_security_group.{sg_key}"), + }; + + // Add outputs so we can retrieve actual AWS IDs after apply + resource_batch.terraform.output.insert( + format!("hydro-network-{}-vpc-id", self.id), + TerraformOutput { + value: format!("${{aws_vpc.{vpc_network}.id}}"), + }, + ); + resource_batch.terraform.output.insert( + format!("hydro-network-{}-subnet-id", self.id), + TerraformOutput { + value: format!("${{aws_subnet.{subnet_key}.id}}"), + }, + ); + resource_batch.terraform.output.insert( + format!("hydro-network-{}-sg-id", self.id), + TerraformOutput { + value: format!("${{aws_security_group.{sg_key}.id}}"), + }, + ); + + let _ = self.existing_network_key.set(NetworkResources { + vpc: vpc_network, + subnet: subnet_key, + security_group: sg_key, + }); + resources + } + } + + pub fn update_from_outputs(&self, resource_result: &ResourceResult) { + let outputs = &resource_result.terraform.outputs; + if let (Some(vpc), Some(subnet), Some(sg)) = ( + outputs.get(&format!("hydro-network-{}-vpc-id", self.id)), + outputs.get(&format!("hydro-network-{}-subnet-id", self.id)), + outputs.get(&format!("hydro-network-{}-sg-id", self.id)), + ) { + let _ = self.existing_network_id.set(NetworkResources { + vpc: vpc.value.clone(), + subnet: subnet.value.clone(), + security_group: sg.value.clone(), + }); } } } @@ -612,7 +664,7 @@ impl Host for AwsEc2Host { return; } - let vpc_path = self.network.collect_resources(resource_batch); + let network_resources = self.network.collect_resources(resource_batch); let iam_instance_profile = self .iam_instance_profile @@ -705,12 +757,8 @@ impl Host for AwsEc2Host { instance_name.push_str(&display_name); } - let network_id = self.network.id.clone(); - let vpc_ref = format!("${{{}.id}}", vpc_path); - let default_sg_ref = format!( - "${{aws_security_group.hydro-vpc-network-{}-default-sg.id}}", - network_id - ); + let vpc_ref = format!("${{{}.id}}", network_resources.vpc); + let default_sg_ref = format!("${{{}.id}}", network_resources.security_group); // Create additional security group for external ports if needed let mut security_groups = vec![default_sg_ref]; @@ -764,7 +812,7 @@ impl Host for AwsEc2Host { } drop(external_ports); - let subnet_ref = format!("${{aws_subnet.hydro-vpc-network-{}-subnet.id}}", network_id); + let subnet_ref = format!("${{{}.id}}", network_resources.subnet); let iam_instance_profile_ref = iam_instance_profile.map(|key| format!("${{{key}.name}}")); // Write the CloudWatch Agent config file. @@ -894,6 +942,7 @@ echo -e "{cwa_config_esc}" > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwa .get_or_init(|| { let id = self.id; + self.network.update_from_outputs(resource_result); let internal_ip = resource_result .terraform .outputs