
Build and test a six-node Debian HPC lab with kubeadm, Kubernetes, Slinky Slurm, NFS, Prometheus, Grafana, Helm, and Terraform.
July 26, 2026
Disposable CPU lab
This build runs Kubernetes and Slurm, submits real jobs, and checks real shared state. It is still a private libvirt lab. Its bootstrap shortcut and missing production controls make it unsuitable as a trading or research production cluster.
A senior HPC infrastructure job description can compress a surprising amount of engineering into four bullets: build custom images, operate Kubernetes and a scheduler, move distributed workloads, and tune compute, storage, and scheduling. A useful build exercise should make those nouns interact instead of placing their packages on one VM.
This OpenFactory recipe creates six Debian 13 machines with separate controller, compute, login, storage, and observability roles. Five machines form a kubeadm cluster. SchedMD Slinky then places a real Slurm controller, LoginSet, REST API, and two-node CPU partition on that cluster. The sixth machine exports NFS and never joins Kubernetes.
The build passes only when a real two-node srun, a real batch job, shared storage, Slurm controller restart persistence, and monitoring placement all behave as specified. There are no HTTP stand-ins for sbatch, generated green metrics, or scheduler-shaped text files.
The result is a six-VM Debian 13 lab on one private subnet. Five VMs form Kubernetes; two of those become Slurm compute nodes. A separate storage VM serves controller state and shared home directories, while the observability VM carries the main Prometheus, Grafana, Alertmanager, operator, and kube-state-metrics pods.
| Node | Static IP | RAM | vCPU | Placement |
|---|---|---|---|---|
| controller | 10.103.0.10 | 4 GB | 2 | Kubernetes control plane; Slinky operator, slurmctld, and REST API |
| compute-1 | 10.103.0.21 | 4 GB | 1 | Kubernetes worker; CPU DaemonSet NodeSet |
| compute-2 | 10.103.0.22 | 4 GB | 1 | Kubernetes worker; CPU DaemonSet NodeSet |
| login-ops | 10.103.0.30 | 4 GB | 1 | Kubernetes worker; LoginSet, kubectl, Helm, and Terraform |
| storage | 10.103.0.40 | 4 GB | 1 | Outside Kubernetes; NFS server on a serial-matched data disk |
| observability | 10.103.0.50 | 4 GB | 1 | Kubernetes worker; Prometheus, Grafana, and Alertmanager |
The minimum guest allocation is 24 GB of RAM and seven vCPUs. The storage node also receives one generated 20 GB virtio disk with serial OPENFACTORY_HPC_DATA. Its first-boot service refuses to format anything when that serial is missing, duplicated, attached to the root disk, or already carries an unexpected filesystem.
All six images can boot together, but their services advance through explicit gates. Storage identifies and exports its dedicated disk. The controller initializes kubeadm and Flannel. Four workers join with one short-lived token. Only after five nodes are Ready does the controller revoke that token and install Slinky plus monitoring.
OpenFactory puts a secret-free intent object in the plan: controller, workers, Kubernetes version, pod CIDR, service CIDR, CNI, and a 900-second maximum token lifetime. The backend generates one token for the build group and injects the role-specific first-boot services. It does not ask the model to invent a token or leave one in recipe JSON.
The VM subnet 10.103.0.0/24, pod subnet 10.244.0.0/16, and service subnet 10.112.0.0/16 must not overlap. This catches a mundane but expensive class of cluster failure before any image is built.
The convenience boundary is narrow and visible: one random kubeadm token per group, a maximum lifetime of 15 minutes, unsafe CA discovery permitted only on the RFC1918 lab subnet, redaction from user-facing responses, and mandatory deletion after all five Kubernetes members report Ready.
Kubeadm documents unsafe CA discovery as a weaker bootstrap mode because a stolen token can help an attacker impersonate the control plane. That trade permits parallel first boot without preloading a CA hash into every image, but it only belongs on the isolated lab network.
The controller uses the normal kubeadm token lifecycle. The acceptance gate checks the negative condition too: after the five-node Ready count is exact, kubeadm token list must contain no live bootstrap token. Production images need CA-pinned discovery and a protected join-material delivery path.
Slinky turns Kubernetes resources into the Slurm service boundary. The operator, controller, and REST API stay on controller; LoginSet stays on login-ops; a DaemonSet NodeSet lands only on the two compute hosts. Static NFS volumes preserve controller state and give both compute nodes the same shared home directory.
SchedMD's Slinky installation guide describes the operator and Slurm chart split. Its NodeSet operations guide covers the compute-node resource model. Here the default CPU partition must contain exactly the two DaemonSet members, not every Kubernetes worker.
Slurm controller state and shared /home use static NFS PersistentVolumes and claims. That design lets a replacement slurmctld pod recover known jobs and lets a batch job write once for both compute nodes. Kubernetes documents that PersistentVolumes separate storage implementation from consumption, which is the exact separation this failure test exercises.
Commands on the login host are wrappers around the actual Slinky LoginSet. sinfo, squeue, srun, sbatch, scontrol, and scancel execute inside that service. The Slurm 26.05 command manuals remain the behavior contract.
The enabled group scenario checks behavior across machine boundaries, not package presence. It waits up to 30 minutes where cluster startup needs it, fails on empty command output, uses strict shell settings, and requires exact counts for Kubernetes members, Slurm nodes, NFS exports, tool versions, and monitoring placement.
| Gate | Required evidence |
|---|---|
| 1. Cluster bootstrap | Exactly five Kubernetes nodes are Ready at v1.36.3, Flannel is Ready, and kubeadm lists no live bootstrap token. |
| 2. Slurm inventory | sinfo reports exactly two nodes in the CPU partition, matching compute-1 and compute-2. |
| 3. Distributed execution | srun -N2 -n2 hostname returns two distinct compute hostnames. |
| 4. Batch and shared home | A real sbatch --wait job writes one file under /home, then both compute nodes read identical content through srun. |
| 5. Controller persistence | A running job remains known after the slurmctld pod is deleted and replaced, proving that controller state survived on the NFS PVC. |
| 6. Monitoring placement | The Slurm ServiceMonitor exists, monitoring workloads are Ready, and the main Prometheus and Grafana pods run on observability. |
| 7. Operator tools | The login host reports Helm v4.2.3 and Terraform v1.15.8. |
| 8. Storage identity | The mounted filesystem belongs to OPENFACTORY_HPC_DATA, the expected three exports are active, and NFS is serving. |
A second scenario records fio, iperf3, kubeadm duration, and scheduler latency commands, but it is disabled and non-gating. A single-host virtual lab is useful for comparing builds; it cannot establish production storage or network service levels.
Version ownership sits in feature generators, not ad hoc package hooks written by the planner. Kubernetes, Flannel, Slinky, Slurm, monitoring, Helm, and Terraform have fixed versions. Chart archives, installers, and configuration enter the images with checksum checks, while the running cluster still needs egress for its pinned OCI images.
| Component | Pin | Primary source |
|---|---|---|
| Kubernetes | v1.36.3 | Kubernetes release |
| Flannel | v0.28.8 | Flannel release |
| Slinky Slurm Operator | v1.2.0 | Slinky installation guide |
| Slurm | 26.05 | SchedMD 26.05 manuals |
| kube-prometheus-stack | 86.0.0 | Prometheus chart release |
| Helm | v4.2.3 | Helm release |
| Terraform | v1.15.8 | HashiCorp install page |
The storage generator is equally strict about physical identity. A size or bus match is not enough; the disk serial must be exact. That turns a destructive format step into a fail-closed operation tied to the topology declaration.
This prompt is the executable specification used by the article's Deploy button and the OpenFactory regression campaign. It fixes every address, role, placement rule, version, storage guard, test, warning, and exclusion. Editing away an awkward constraint may make planning easier, but it also changes what the result can honestly prove.
Build a real, disposable six-node HPC lab named `slurm-hpc`.
This fixture must exercise OpenFactory's actual multi-machine build and libvirt
test path. Do not create Python compatibility listeners, fake `/sbatch` HTTP
endpoints, mock metrics, or placeholder Slurm configuration. The running system
must be Kubernetes v1.36.3 bootstrapped with kubeadm, Flannel v0.28.8, SchedMD
Slinky Slurm Operator chart v1.2.0 / Slurm 26.05, and
`kube-prometheus-stack` 86.0.0.
This is intentionally a zero-touch lab image set. It uses one backend-generated
kubeadm token with a 15-minute maximum lifetime and unsafe CA discovery only on
the private RFC1918 lab network. The controller must revoke the token after all
five Kubernetes nodes are Ready. Make the warnings say plainly that these are
lab images, not secure production runtime bundles.
## Six-node topology
Use one network:
```json
{
"name": "hpc-lab",
"cidr": "10.103.0.0/24",
"gateway": "10.103.0.1",
"dns_servers": ["1.1.1.1", "8.8.8.8"],
"is_default_route": true
}
```
Every node is a `linux-build` on `debian-trixie`, has one static interface on
`hpc-lab`, and has 4 GB minimum RAM. Use this exact inventory:
| node_id | IP | role | min CPU | Kubernetes/Slinky placement |
|---|---:|---|---:|---|
| `controller` | `10.103.0.10` | `controller` | 2 | control plane, Slinky operator/controller/restapi |
| `compute-1` | `10.103.0.21` | `compute` | 1 | worker, CPU DaemonSet NodeSet |
| `compute-2` | `10.103.0.22` | `compute` | 1 | worker, CPU DaemonSet NodeSet |
| `login-ops` | `10.103.0.30` | `login` | 1 | worker, LoginSet, kubectl/Helm/Terraform |
| `storage` | `10.103.0.40` | `storage` | 1 | outside Kubernetes, NFS server |
| `observability` | `10.103.0.50` | `observability` | 1 | worker, Prometheus/Grafana placement |
Use canvas-pixel coordinates with no overlap: controller `(320, 100)`,
compute-1 `(120, 340)`, compute-2 `(520, 340)`, login-ops `(120, 580)`,
storage `(520, 580)`, observability `(320, 820)`. Connect the controller to
all four Kubernetes workers, connect storage to controller, both computes, and
login-ops, and connect observability to controller.
The `storage` topology node must declare exactly this generated data disk:
```json
{
"id": "hpc-data",
"kind": "disk",
"size_gb": 20,
"bus": "virtio",
"serial": "OPENFACTORY_HPC_DATA"
}
```
The storage image may format only the runtime disk whose serial is exactly
`OPENFACTORY_HPC_DATA`; it must refuse zero matches, multiple matches, the root
disk, and an unexpected existing filesystem.
## Kubernetes intent
Include this secret-free object at `plan.kubernetes`. Never include a token:
```json
{
"mode": "kubeadm-lab",
"version": "v1.36.3",
"controller": "controller",
"workers": ["compute-1", "compute-2", "login-ops", "observability"],
"pod_cidr": "10.244.0.0/16",
"service_cidr": "10.112.0.0/16",
"cni": "flannel",
"bootstrap_token_ttl_seconds": 900
}
```
The VM, pod, and service CIDRs must remain non-overlapping. The storage node is
not a Kubernetes member, so the live acceptance count is five Ready nodes.
## Recipe contract
Every recipe must use `base_image: "debian-trixie"`, set
`hardware.min_memory_gb: 4`, set the CPU minimum from the table, and keep only
role-appropriate features/packages. Every node has a normal `ops` user with
sudo and SSH for test access. Do not put secrets in the prompt beyond the
fixture's ordinary lab login password.
Features by role:
- `controller`: `headless`, `ssh`, `kubeadm-lab`, `slinky-slurm`, `helm`
- `compute-1`, `compute-2`: `headless`, `ssh`, `kubeadm-lab`, `slinky-slurm`
- `login-ops`: `headless`, `ssh`, `kubeadm-lab`, `slinky-slurm`, `helm`,
`terraform`
- `storage`: `headless`, `ssh`, `hpc-nfs`
- `observability`: `headless`, `ssh`, `kubeadm-lab`, `slinky-slurm`
Do not list `helm`, `terraform`, `kubeadm`, `kubectl`, or `kubelet` as
additional apt packages and do not add ad hoc upstream repository hooks. The
declared OpenFactory features own those pinned installations.
The backend injects each Kubernetes member's
`openfactory-kubeadm-lab` service from `plan.kubernetes`. Do not handcraft that
service or a bootstrap token.
Every Kubernetes member must declare an enabled service named
`openfactory-slinky-slurm` with this shared config and its own correct `role`
(`controller`, `compute`, `login`, or `observability`):
```json
{
"controller_node_id": "controller",
"compute_node_ids": ["compute-1", "compute-2"],
"login_node_id": "login-ops",
"observability_node_id": "observability",
"nfs_server": "10.103.0.40",
"slinky_version": "1.2.0",
"slurm_version": "26.05",
"prometheus_stack_version": "86.0.0"
}
```
The storage recipe must declare an enabled service named
`openfactory-hpc-nfs`:
```json
{
"disk_serial": "OPENFACTORY_HPC_DATA",
"export_network": "10.103.0.0/24"
}
```
Do not add startup scripts that duplicate feature-generator behavior.
## Required running behavior
Slinky accounting stays disabled. Use NFS-backed static PV/PVC storage for
Slurm controller state and NFS-backed shared `/home` in LoginSet and NodeSet.
The controller, operator, and restapi run on `controller`; the LoginSet runs on
`login-ops`; a DaemonSet NodeSet runs only on `compute-1` and `compute-2`; and
the default CPU partition contains exactly those two Slurm nodes. Enable
controller metrics and its ServiceMonitor. Put the main Prometheus, Grafana,
Alertmanager, operator, and kube-state-metrics pods on `observability`.
The login-ops host commands `sinfo`, `squeue`, `srun`, `sbatch`, `scontrol`,
and `scancel` must execute the real command inside the Slinky LoginSet. They
must not emulate scheduler responses.
Runtime egress is allowed for the pinned Slinky container images. The Slinky
and monitoring charts, Kubernetes/Flannel configuration, and Helm/Terraform CLI
installers must be baked into the images with fixed versions and checksums.
## Gating acceptance scenario
Create one enabled group scenario named `real-hpc-acceptance`. Use
`command_succeeds` or `command_output` assertions with
`params.timeout_seconds` up to 1800. The scenario must verify:
1. On `controller`, exactly five Kubernetes nodes report Ready, kubeadm reports
Kubernetes v1.36.3, Flannel pods are Ready, and `kubeadm token list` has no
live bootstrap token.
2. On `login-ops`, `sinfo` reports exactly two Slurm nodes in the CPU partition.
3. On `login-ops`, `srun -N2 -n2 hostname` produces two distinct hostnames.
4. On `login-ops`, a real `sbatch --wait` job writes
`/home/openfactory-hpc-acceptance.txt`, then `srun -N2 -n2 cat` reads the
same content from both compute nodes.
5. From `controller`, submit a running job through the real LoginSet, delete the
slurmctld pod, wait for its replacement, and verify the job remains known.
This proves controller state survived a pod restart on the NFS PVC.
6. On `controller`, the Slinky controller ServiceMonitor exists; Prometheus and
Grafana workloads are Ready in `monitoring`; their main pods are assigned to
`observability`.
7. On `login-ops`, `helm version --short` contains `v4.2.3` and
`terraform version` contains `v1.15.8`.
8. On `storage`, the mounted filesystem comes from the disk with serial
`OPENFACTORY_HPC_DATA`, all three exports are active, and NFS is serving.
Avoid shell checks that pass on empty output. Every command must use
`set -euo pipefail`, explicit numeric/string assertions, and bounded waits.
## Non-gating measurements
Add a second scenario named `hpc-lab-measurements` with `enabled: false`. It
documents commands for NFS `fio`, VM-to-VM `iperf3`, kubeadm bootstrap duration,
and Slurm queue/scheduler latency. These are reported measurements, not pass/fail
SLOs for the first CPU lab.
## Warnings and scope
Preserve all of these warnings:
- Lab-only bootstrap uses unsafe CA discovery on a private network and is not a
secure production runtime bundle.
- Production identity, multi-user UID/GID management, TLS, secrets rotation,
network policy, backup/restore, and hardened RBAC are not included.
- SlurmDBD/accounting, fair-share/QoS/account hierarchies, HTCondor, and Ray are
not included.
- GPU/GRES and RDMA/InfiniBand are render-tested extension hooks only; this
acceptance run is CPU-only and does not validate hardware drivers or devices.
- Cloud image publishing and an on-prem-to-cloud migration workflow are not
included in this six-VM libvirt lab.
- Runtime egress is required for pinned OCI images; chart archives are baked
and checksum-verified at image build time.The raw prompt is available as plain text for diffing and code review.
Start from the Deploy button above, inspect the generated six-node plan and its lab warnings, then build the role images as one group. OpenFactory provisions the private libvirt topology and runs the enabled acceptance scenario. Treat any missing node, skipped assertion, or softened warning as a failed build.
plan.kubernetes object.real-hpc-acceptance to pass. Use the disabled measurements only as observations for this host, not as performance promises.The lab turns several resume nouns into inspectable evidence: role-specific Linux images, kubeadm orchestration, operator-managed Slurm, distributed jobs, persistent controller state, shared storage, placement-aware monitoring, and CI assertions. It also marks cloud migration as unimplemented, which is more useful than implying that an installed Terraform binary constitutes a migration.
| Role language | Evidence in this build |
|---|---|
| Terraform, Kubernetes, and Slurm | The login node carries pinned Terraform and Helm CLIs; kubeadm forms a five-node cluster; Slinky deploys and exercises Slurm 26.05. |
| HPC and distributed workloads | srun spans both compute nodes, while sbatch and shared NFS state prove that work crosses machine boundaries. |
| Custom cloud images and CI/CD | Six role-specific Debian images are generated from one plan, with a CI workflow that checks generators, plan repair, and the real acceptance scenario. |
| Compute, storage, and scheduling | Node placement is explicit, the CPU partition has exactly two members, and the storage service accepts only its serial-matched data disk. |
| Cloud migration | Not included. Terraform is present as an operator tool, but cloud modules, image publication, data movement, and migration rehearsal remain future work. |
For an interview or design review, the interesting discussion is in the failure choices: why storage stays outside Kubernetes, why a scheduler node count is exact, why controller persistence is tested by deletion, why a bootstrap token needs a negative assertion, and why empty shell output is never a pass.
The recipe stops before the controls that define a production HPC service. There is no SlurmDBD or accounting, fair-share policy, QoS, account hierarchy, production identity, TLS lifecycle, secrets rotation, hardened RBAC, backup, network policy, cloud publication, HTCondor, Ray, validated GPU stack, or validated RDMA fabric.
A production design should keep the acceptance style and replace the shortcuts: use authenticated CA-pinned node enrollment, real identity and account policy, highly available control services, backed-up accounting, fit-for-purpose data storage, segmented networks, signed image promotion, hardware qualification, and rehearsed cloud or recovery moves.
That is the useful division of labor. The lab makes the base mechanisms repeatable and gives every green claim a command behind it. Production engineering then adds the controls, capacity data, and failure evidence that a real research or trading workload demands.
Yes. Slinky deploys Slurm 26.05, and the login host forwards sinfo, squeue, srun, sbatch, scontrol, and scancel to the actual LoginSet. The gate rejects fake scheduler output and proves a two-node srun plus an sbatch job that shares a file through NFS.
Kubernetes supplies the control plane for the Slinky operator and its Slurm workloads. Slinky places the controller, REST API, login service, and compute NodeSet as Kubernetes resources. This is one useful operating model, not a claim that every Slurm site should add Kubernetes.
No. It is a disposable private-network lab. The bootstrap flow uses unsafe CA discovery for up to 15 minutes, and the build omits production identity, hardened RBAC, TLS operations, secrets rotation, backup, network policy, Slurm accounting, high availability, and multi-user UID/GID management.
No. GPU/GRES and RDMA/InfiniBand are disabled render-test hooks. The enabled acceptance path is CPU-only and does not claim that drivers, firmware, devices, topology, GPUDirect, or high-speed fabrics work.
No. Terraform 1.15.8 is installed on the login and operations node, but the recipe contains no AWS, Google Cloud, or Azure modules, no cloud image publishing, and no data-migration workflow. Those are explicit follow-on engineering tasks.
The declared floor is 24 GB of guest RAM and seven guest vCPUs: 4 GB on each of six VMs, two vCPUs for the controller, and one for every other node. The host also needs room for six system disks, a 20 GB data disk, image builds, Kubernetes pods, and hypervisor overhead.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.