tfmodules/hpc/ovh/main.tf
gitops_test 09cc7b38e0 sync-modules: mirrored modules of release 3.3.0-F-feature-simpl-33565.773.825d7dcb
+++ hpc/README.md
+++ hpc/ovh/credentials.tf
+++ hpc/ovh/main.tf
+++ hpc/ovh/versions.tf
2026-10-08 16:15:41 +00:00

195 lines
6.7 KiB
HCL

// 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.
// The flavors/images data sources require an explicit service_name (unlike
// the resources, they do not fall back to the OVH_CLOUD_PROJECT_SERVICE
// runner variable when the attribute is an explicit null), so the project is
// resolved from the credential itself when no override is given: the
// per-account API keys of the platform are scoped to a single project.
data "ovh_cloud_projects" "all" {
count = var.project_id == null ? 1 : 0
}
locals {
service_name = var.project_id != null ? var.project_id : one(data.ovh_cloud_projects.all[0].projects[*].project_id)
}
data "ovh_cloud_project_flavors" "head" {
service_name = local.service_name
name_filter = var.head_flavor
region = var.region
}
data "ovh_cloud_project_flavors" "compute" {
service_name = local.service_name
name_filter = var.compute_flavor
region = var.region
}
data "ovh_cloud_project_images" "all" {
service_name = local.service_name
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 = local.service_name
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 = local.service_name
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 = local.service_name
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 = local.service_name
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 = local.service_name
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 = local.service_name
region_name = var.region
depends_on = [ovh_cloud_project_instance.head]
}
resource "ovh_cloud_project_instance" "compute" {
count = var.compute_node_count
service_name = local.service_name
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
])
}