// HPC cluster: one head node (the single entry point, public SSH + private // cluster network) and N compute nodes (private network only, internet egress // through an OVH gateway for package installation). The cloud provider // credentials and the project (OVH_CLOUD_PROJECT_SERVICE) reach the provider // from the runner environment, never from module inputs. data "ovh_cloud_project_flavors" "head" { service_name = var.project_id name_filter = var.head_flavor region = var.region } data "ovh_cloud_project_flavors" "compute" { service_name = var.project_id name_filter = var.compute_flavor region = var.region } data "ovh_cloud_project_images" "all" { service_name = var.project_id region = var.region } locals { selected_image = one([for m in data.ovh_cloud_project_images.all.images : m if m.name == var.image]) selected_head_flavor = one(data.ovh_cloud_project_flavors.head.flavors) selected_compute_flavor = one(data.ovh_cloud_project_flavors.compute.flavors) head_hostname = "${var.name_prefix}-head-${var.deployment_id}" head_ip = cidrhost(var.cluster_cidr, 50) compute_hostnames = [for i in range(var.compute_node_count) : "${var.name_prefix}-compute${i + 1}-${var.deployment_id}"] compute_ips = [for i in range(var.compute_node_count) : cidrhost(var.cluster_cidr, 60 + i)] } // Private cluster network (vRack) with one subnet. Fixed addresses are // assigned outside the DHCP pool; DHCP stays enabled so the instances // configure their private interface from the OpenStack metadata. resource "ovh_cloud_project_network_private" "cluster" { service_name = var.project_id name = "${var.name_prefix}-hpc-${var.deployment_id}" regions = [var.region] vlan_id = var.vlan_id } resource "ovh_cloud_project_network_private_subnet" "cluster" { service_name = var.project_id network_id = ovh_cloud_project_network_private.cluster.id region = var.region network = var.cluster_cidr // The allocation pool must cover the fixed addresses: the OVH API refuses // an instance ip outside it ("instance ip must be in subnet allocation // pool"). The DHCP agent ports allocate from the start of the pool, so the // fixed addresses (head at 50, compute nodes from 60) stay clear of them: // an instance on a DHCP-port address is silently canceled by the cloud. start = cidrhost(var.cluster_cidr, 10) end = cidrhost(var.cluster_cidr, 200) dhcp = true } locals { openstack_network_id = one([ for r in ovh_cloud_project_network_private.cluster.regions_attributes : r.openstackid if r.region == var.region ]) } // Internet egress for the whole cluster: the gateway attaches to the // cluster subnet and DHCP distributes it as the default route. A NAT // through a dual-homed head node is not an option: the cloud's port // anti-spoofing silently drops the forwarded return traffic. resource "ovh_cloud_project_gateway" "egress" { service_name = var.project_id name = "${var.name_prefix}-hpc-gw-${var.deployment_id}" model = var.gateway_model region = var.region network_id = local.openstack_network_id subnet_id = ovh_cloud_project_network_private_subnet.cluster.id } // One project SSH key per deployment, managed by the module so it is removed // on destroy. The instance level ssh_key_create is avoided on purpose: keys it // creates outlive the instance and collide on the next deployment. resource "ovh_cloud_project_ssh_key" "admin" { service_name = var.project_id name = "${var.name_prefix}-${var.deployment_id}-key" public_key = trimspace(tls_private_key.admin.public_key_openssh) } resource "ovh_cloud_project_instance" "head" { service_name = var.project_id region = var.region billing_period = "hourly" name = local.head_hostname boot_from { image_id = local.selected_image.id } flavor { flavor_id = local.selected_head_flavor.id } ssh_key { name = ovh_cloud_project_ssh_key.admin.name } // The head node cannot sit on the public network: the cloud refuses an // instance attached to both the public network and a subnet served by a // gateway. Public reachability comes from a floating ip routed through // the gateway instead. The fixed address stays clear of the DHCP agent // ports at the start of the pool: an instance on a DHCP-port address is // silently canceled by the cloud, which also broke the floating ip // creation until the address moved. network { private { ip = local.head_ip floating_ip_create { description = "${var.name_prefix}-hpc-head-${var.deployment_id}" } network { id = local.openstack_network_id subnet_id = ovh_cloud_project_network_private_subnet.cluster.id } } } user_data = data.cloudinit_config.head.rendered depends_on = [ovh_cloud_project_gateway.egress] } // Resolves the public (floating) address of the head node: the instance // addresses only carry the fixed private ip, the floating ip is matched // through the port it is associated with. data "ovh_cloud_project_floatingips" "all" { service_name = var.project_id region_name = var.region depends_on = [ovh_cloud_project_instance.head] } resource "ovh_cloud_project_instance" "compute" { count = var.compute_node_count service_name = var.project_id region = var.region billing_period = "hourly" name = local.compute_hostnames[count.index] boot_from { image_id = local.selected_image.id } flavor { flavor_id = local.selected_compute_flavor.id } ssh_key { name = ovh_cloud_project_ssh_key.admin.name } network { private { ip = local.compute_ips[count.index] network { id = local.openstack_network_id subnet_id = ovh_cloud_project_network_private_subnet.cluster.id } } } user_data = data.cloudinit_config.compute[count.index].rendered depends_on = [ovh_cloud_project_gateway.egress] } locals { head_public_ip = one([ for f in data.ovh_cloud_project_floatingips.all.cloud_project_floatingips : f.ip if try(f.associated_entity.ip, "") == local.head_ip ]) }