tfmodules/hpc/ovh/cloudinit.tf
gitops_test 53339e50a8 sync-modules: mirrored modules of release 3.3.0-F-feature-simpl-33565.765.f7724799
+++ hpc/README.md
+++ hpc/ovh/cloudinit.tf
+++ hpc/ovh/credentials.tf
+++ hpc/ovh/main.tf
+++ hpc/ovh/outputs.tf
+++ hpc/ovh/variables.tf
+++ hpc/ovh/versions.tf
2026-10-07 15:03:53 +00:00

246 lines
8.6 KiB
HCL

// Node bootstrap. Everything local to a node is applied by cloud-init at
// first boot; the cluster forms without any manual step. Slurm runs in
// configless mode: only the head node carries slurm.conf, compute nodes
// fetch it from the controller. The munge key is generated per deployment
// and delivered to every node through its user data.
locals {
munge_key_b64 = random_bytes.munge_key.base64
// Written early by cloud-init to a staging path (the munge user does not
// exist yet); runcmd installs it with the right owner after the packages.
munge_stage_path = "/run/simpl/munge.key"
cluster_hosts = concat(
["${local.head_ip} ${local.head_hostname}"],
[for i in range(var.compute_node_count) : "${local.compute_ips[i]} ${local.compute_hostnames[i]}"],
)
slurm_conf = join("\n", concat([
"ClusterName=${lower(var.name_prefix)}-hpc",
"SlurmctldHost=${local.head_hostname}(${local.head_ip})",
"SlurmctldParameters=enable_configless",
"AuthType=auth/munge",
"SlurmUser=slurm",
"CredType=cred/munge",
"SelectType=select/cons_tres",
"SelectTypeParameters=CR_Core",
"ProctrackType=proctrack/linuxproc",
"TaskPlugin=task/none",
"ReturnToService=2",
"SlurmctldPort=6817",
"SlurmdPort=6818",
"SrunPortRange=60001-60128",
"StateSaveLocation=/var/lib/slurm/slurmctld",
"SlurmdSpoolDir=/var/lib/slurm/slurmd",
"SlurmctldLogFile=/var/log/slurm/slurmctld.log",
"SlurmdLogFile=/var/log/slurm/slurmd.log",
"MailProg=/bin/true",
], [
for i in range(var.compute_node_count) :
"NodeName=${local.compute_hostnames[i]} NodeAddr=${local.compute_ips[i]} CPUs=${var.compute_cpus} State=UNKNOWN"
], [
"PartitionName=main Nodes=ALL Default=YES MaxTime=INFINITE State=UP",
]))
hosts_runcmd = [for entry in local.cluster_hosts : "grep -qxF '${entry}' /etc/hosts || echo '${entry}' >> /etc/hosts"]
munge_runcmd = [
"install -o munge -g munge -m 0400 ${local.munge_stage_path} /etc/munge/munge.key",
"rm -f ${local.munge_stage_path}",
"systemctl restart munge",
]
// Every cluster node is on the private subnet only: the default route
// comes from DHCP (the subnet gateway) but no name servers are provided,
// so resolution is pinned to public resolvers. Runs on every boot and
// before package installation; the last entry blocks until egress through
// the gateway answers, capped at ~4 minutes.
egress_bootcmd = [
"mkdir -p /etc/systemd/resolved.conf.d",
"printf '[Resolve]\\nDNS=1.1.1.1 8.8.8.8\\n' > /etc/systemd/resolved.conf.d/90-cluster.conf",
"systemctl try-restart systemd-resolved || true",
"for i in $(seq 1 48); do timeout 3 bash -c '</dev/tcp/archive.ubuntu.com/80' 2>/dev/null && break; sleep 5; done",
]
head_cloud_config = {
hostname = local.head_hostname
prefer_fqdn_over_hostname = false
package_update = true
packages = [
"slurmctld",
"slurm-client",
"munge",
"nfs-kernel-server",
"openmpi-bin",
"chrony",
]
write_files = [
{
path = local.munge_stage_path
encoding = "b64"
content = local.munge_key_b64
permissions = "0400"
},
{
path = "/etc/slurm/slurm.conf"
content = "${local.slurm_conf}\n"
permissions = "0644"
},
{
path = "/etc/exports"
content = "/home ${var.cluster_cidr}(rw,sync,no_subtree_check,root_squash)\n"
permissions = "0644"
},
]
bootcmd = local.egress_bootcmd
runcmd = concat(local.hosts_runcmd, local.munge_runcmd, [
"mkdir -p /var/lib/slurm/slurmctld /var/log/slurm",
"chown slurm:slurm /var/lib/slurm/slurmctld /var/log/slurm",
"systemctl enable --now nfs-kernel-server",
"exportfs -ra",
"systemctl enable slurmctld",
"systemctl restart slurmctld",
])
}
compute_cloud_config = [for i in range(var.compute_node_count) : {
hostname = local.compute_hostnames[i]
prefer_fqdn_over_hostname = false
package_update = true
packages = [
"slurmd",
"munge",
"nfs-common",
"openmpi-bin",
"chrony",
]
bootcmd = local.egress_bootcmd
write_files = [
{
path = local.munge_stage_path
encoding = "b64"
content = local.munge_key_b64
permissions = "0400"
},
{
path = "/etc/default/slurmd"
content = "SLURMD_OPTIONS=\"--conf-server ${local.head_ip}:6817\"\n"
permissions = "0644"
},
{
path = "/etc/systemd/system/slurmd.service.d/90-retry.conf"
content = "[Unit]\nStartLimitIntervalSec=0\n\n[Service]\nRestart=on-failure\nRestartSec=15\n"
permissions = "0644"
},
]
mounts = [
["${local.head_ip}:/home", "/home", "nfs", "defaults,nofail,_netdev,x-systemd.automount", "0", "0"],
]
runcmd = concat(local.hosts_runcmd, local.munge_runcmd, [
"mkdir -p /var/lib/slurm/slurmd /var/log/slurm",
"chown slurm:slurm /var/lib/slurm/slurmd /var/log/slurm",
// Best effort early mount of the shared home, so jobs submitted right
// after the cluster forms never land on the local directory before the
// automount first triggers.
"systemctl start home.mount 2>/dev/null || true",
// The retry drop-in keeps slurmd re-launching until the controller
// answers: the cluster nodes boot in parallel and the first attempts
// may precede slurmctld.
"systemctl daemon-reload",
"systemctl enable slurmd",
"systemctl restart slurmd || true",
])
}]
// Consumer and management users, shared by every node. Fixed uids: Slurm
// identifies users by uid across the cluster, so the consumer must exist
// everywhere with the same one. The consumer has no sudo (it is a cluster
// user, not a machine owner) and its password exists only on the head node
// (applied over SSH, never through user data). merge directives make
// cloud-init append to the node lists instead of replacing them.
credentials_cloud_config = {
merge_how = [
{ name = "list", settings = ["append"] },
{ name = "dict", settings = ["no_replace", "recurse_list"] },
]
ssh_pwauth = true
users = [
"default",
{
name = var.username
uid = 2000
shell = "/bin/bash"
lock_passwd = false
groups = "users"
},
{
name = var.admin_username
uid = 2001
shell = "/bin/bash"
lock_passwd = true
sudo = "ALL=(ALL) NOPASSWD:ALL"
groups = "users, sudo"
ssh_authorized_keys = [trimspace(tls_private_key.admin.public_key_openssh)]
},
// The munge and slurm system users are created before the packages so
// they get the SAME uid on every node: the packages otherwise allocate
// them dynamically, and Slurm authenticates controller-to-daemon
// messages by uid ("Security violation, slurm message from uid N"
// across nodes when they diverge). Low fixed uids: the dynamic system
// range is allocated downward from 999, so the high 900s are already
// taken on the stock image. Listed last so a failure here can never
// block the login users above.
{
name = "munge"
uid = 601
system = true
shell = "/usr/sbin/nologin"
homedir = "/var/lib/munge"
no_create_home = true
lock_passwd = true
},
{
name = "slurm"
uid = 602
system = true
shell = "/usr/sbin/nologin"
homedir = "/var/lib/slurm"
no_create_home = true
lock_passwd = true
},
]
}
}
data "cloudinit_config" "head" {
gzip = false
base64_encode = true
part {
content_type = "text/cloud-config"
content = join("\n", ["#cloud-config", yamlencode(local.head_cloud_config)])
}
part {
content_type = "text/cloud-config"
content = join("\n", ["#cloud-config", yamlencode(local.credentials_cloud_config)])
}
}
data "cloudinit_config" "compute" {
count = var.compute_node_count
gzip = false
base64_encode = true
part {
content_type = "text/cloud-config"
content = join("\n", ["#cloud-config", yamlencode(local.compute_cloud_config[count.index])])
}
part {
content_type = "text/cloud-config"
content = join("\n", ["#cloud-config", yamlencode(local.credentials_cloud_config)])
}
}