Service-level architecture on Google Cloud
Every Google Cloud product in the deployment, named · drawn from
numen-gcp-sandbox, 2026-09-07
The other page shows the shape of the system. This one names the parts. It is written for someone reviewing the design rather than using the product: which Google Cloud services are in the path, what each is actually doing, what it costs when nobody is working, and which services are deliberately absent. Everything here was read from the live project, not from a design document.
1 · The services, and where they sit
Colour is meaning, not decoration: blue is Numen's own code, violet is a managed Google service, green is compute a researcher's work runs on, and amber is storage.
2 · What each service is doing, and what it costs
| Service | Doing what, here | Cost when idle |
|---|---|---|
| Cloud Load Balancing | Global external ALB with a Google-managed certificate. The only public entry point. | ~$18/mo |
| Compute Engine (webapp) | One e2-standard-2 (since 7 September; e2-small's shared cores
saturated under the console's polling) running nginx and gunicorn with four
threaded workers. Stateless — it rebuilds itself from a startup script and
an app bundle in Cloud Storage on every boot. | ~$50/mo |
| Cloud SQL | MySQL 8.0 db-f1-micro, private IP only, holding Slurm job
accounting. Deliberately outlives clusters. | ~$8–10/mo |
| Identity-Aware Proxy | The only route to a shell on any VM. No bastion, no public SSH. | $0 |
| Cloud NAT + Router | Egress for machines with no external address. | Low, usage-based |
| Infrastructure Manager | Runs Terraform server-side for single-node launches, so the webapp holds no state and needs no Terraform binary. | $0 |
| Cloud Build | Every cluster action the console takes — create, add a compute group,
destroy — is one build: fetch the cluster's launch record, run
terraform 1.12.2 against the packaged blueprint (Infra Manager's
Terraform is too old for the toolkit), sweep orphaned compute nodes on
destroy, and wait for a filesystem that is being created with the cluster.
Also runs the nightly assistant check. | $0 — per build minute |
| Cloud Scheduler | Fires the nightly golden run at 02:00 ET. | ~$0 |
| Cloud Batch | Ephemeral job VMs in our own subnet. No queue, no compute environment to keep warm. | $0 |
| GKE Autopilot | One regional cluster, private nodes, public endpoint. The webapp submits
Kubernetes Jobs through the API server with its own IAM token
(roles/container.developer); pods run as
numen/numen-runtime, bound by Workload Identity to the runtime
service account, and mount the outputs bucket with the GCS FUSE CSI driver.
Applications with a maintained image only. No kubectl or kubeconfig anywhere. |
~$73/mo cluster fee while the lane exists; pods per second |
| Cluster Toolkit (Slurm on GCP v6) | Three blueprints rendered once by gcluster and packaged in Cloud
Storage: head node, no login node, eight compute-group slots (CPU or GPU,
filled per cluster by the console), Slurm accounting to Cloud SQL, and in the
Lustre variant a /scratch mount on every node. Compute nodes are
created per job and deleted five minutes after idle. | $0 until deployed; head node ~$0.13/hr while a cluster exists |
| Managed Lustre | Shared /scratch on a single node or across every cluster node.
Created from the admin console or with a cluster (the build waits for it);
never deleted with a cluster. Users write under /scratch/shared
or /scratch/users/<user>. | $0 — but $7.40/hr while it exists |
| Filestore | Managed NFS as the cheaper shared filesystem, same picker and mount path as Lustre. Admin-only create with the cost stated. | $0 — but ~$5.39/day per 1 TiB share while it exists |
| Cloud Storage | One bucket per user, plus assets, outputs and Terraform state. | Pennies |
| Secret Manager | Slurm database password, the QA account's password for the nightly run, OIDC client secret. Read at boot into 0600 systemd drop-ins — never in instance metadata. | ~$0 |
| Vertex AI | Gemini 2.5 Flash in production, tool-calling over live pricing, the live catalogue, live instance state, the capacity planner and the cost report. Claude on Vertex is retained and switchable; the fallback is off by default so one conversation is one model. | $0 — per token |
| Cloud Billing Catalog API | Every price the product shows. ~32,000 SKUs, cached six hours. | $0 |
| Recommender API | Google's own recommendations per project — idle VMs, machine types, idle disks and addresses, commitments, IAM — read in parallel across the region's zones and shown with the monthly saving. Viewer only; nothing applies one. | $0 |
| Compute Engine serial console | The bootstrap log without SSH: the startup script's output is mirrored to serial port 1, and the diagnosis endpoint reads it with the same permission that reads guest attributes. Matched against known failure signatures. | $0 |
| Lab projects | A second project per lab (numen-gcp-lab-a today) with its own VPC,
NAT, Terraform and runtime service accounts, created by a Terraform module from a
map of labs. The platform's webapp reaches it with the lab's own accounts. |
NAT + whatever runs there |
| Compute Engine quota API | Regional limits and usage, so a launch that cannot succeed is refused before it starts rather than at apply time. | $0 |
| Cloud Logging / Monitoring | Audit trail, a billing budget alert, and the alert that fires when the nightly assistant check fails. | Free tier |
| OS Login | How researchers and operators reach a cluster's head node over IAP as themselves, and how the webapp reads job state from it. No SSH keys are stored by the application. | $0 |
3 · Three paths worth following end to end
A researcher launches a machine
Browser → Cloud Load Balancing → nginx → Django. Django prices the selection from the Cloud Billing Catalog, checks regional quota, then hands a Terraform configuration to Infrastructure Manager, which creates a Compute Engine VM running as that user's own service account. The VM has no external address; its startup script mounts that user's Cloud Storage bucket with Cloud Storage FUSE and installs the selected applications. Progress is reported back through guest attributes. Idle-stop eventually returns it to zero.
A researcher builds a cluster and runs a job
New cluster in the console: a name, a head node, one to eight compute groups (each a
machine type and a node cap, GPU shapes included) and optionally a shared filesystem.
Django validates the shape, writes a launch record to Cloud Storage and submits a
Cloud Build that runs terraform apply as sa-terraform
against the packaged Cluster Toolkit blueprint, state in the tfstate bucket; the
dialog polls the build. About three minutes later the head node is up and Django lists
the cluster by the toolkit's own slurm_cluster_name label through the
Compute Engine API. The researcher logs in to the head node over IAP with
OS Login and runs sbatch; slurmctld creates compute nodes that
did not exist a moment earlier (about two minutes to running), deletes them five minutes
after the job ends, and slurmdbd writes the record to Cloud SQL. Adding a
compute group later is another build against the same state; so is destroy, which also
sweeps any compute node Terraform never knew about.
A filesystem is created with a cluster
Choosing “Create a new Lustre filesystem with this cluster” asks
Managed Lustre for the filesystem through the same guarded path the admin console
uses, then submits the build with an extra lustre-wait step that polls until
the filesystem is ACTIVE (10–20 minutes), writes its mount spec into the launch
record, and only then lets Terraform run. The filesystem outlives the cluster on purpose;
deleting it is a separate, priced, admin action.
A job's history survives its cluster
This is the path that is easy to miss and is the reason the accounting database is platform-level. Cluster Toolkit treats accounting as optional and defaults it to null, so Slurm records nothing unless it is wired up; a cluster destroyed at the end of a session takes its entire job history with it. Here slurmdbd writes to a Cloud SQL instance that no cluster owns. Destroy the cluster and the rows remain, and the optimisation engine reads them back into DuckDB later. Demonstrated across two clusters that no longer exist.
4 · Services deliberately absent
Listing what is not used is as informative as listing what is, and it stops the diagram being read as a wish.
| Not used | Why |
|---|---|
| Parallelstore | Cheaper than Lustre at about $2.30/hour and this project is not allowlisted to create one. Google is also retiring the cluster-toolkit module for it. Managed Lustre is what ships. |
| VPC Service Controls | Not deployed, and worth being precise about: it is a perimeter, not a partition. It stops data leaving a boundary and does nothing to isolate two tenants inside one. It is a complement to a tenancy model, not a substitute. |
| Workload Identity Federation | Nothing federates an external identity yet. It attaches to whatever a tenant turns out to be, so it waits on that decision. |
| BigQuery | Billing export to BigQuery for FinOps reconciliation is planned, not built. Every price shown today is a live Catalog lookup, and spend to date is runtime × list price per instance. |
| Cloud DNS, Cloud Armor, Cloud Run | The domain is managed outside Google Cloud; there is no WAF in front of the load balancer yet; the webapp is one VM by design (stateless, rebuilt from a startup script). None of these is a gap a demo depends on. |
| Cloud Composer, Dataproc, Bigtable | No workload here needs them. Naming a service the product does not provision is how a capability claim gets made by accident. |
A researcher runs a container job
- Picks an application with a maintained image and "GKE — Kubernetes job" as the infrastructure. The webapp builds a Kubernetes Job: image, CPU and memory from the catalogue's minimums (inside Autopilot's CPU:memory ratio), the outputs bucket as a GCS FUSE volume, labels naming the user and the app.
- Submits it to the API server over HTTPS with the cluster's CA and its own IAM access token. The namespace and the Kubernetes service account are created on first use.
- Autopilot provisions a node for exactly that request (one to three minutes), the pod
pulls the image, the workload runs and writes under
/outputs/<job>, which is the bucket. - My instances shows the job as lane GKE; its panel reads the pod, the conditions and the container log back through the API. A job that succeeds is recorded once as job evidence with its measured hours.
A launch fails and nobody opens a shell
- The Diagnose button (or the assistant's tool) calls one endpoint with the launch name.
- The endpoint reads the Infrastructure Manager deployment and its Terraform error, the VM, the bootstrap's guest attributes and the serial console, in parallel.
- Nineteen signatures — zone capacity, quota, permission, GPU driver, Lustre client, filesystem mount, a pipeline that installed but does not import, packages missing on apt or dnf, no egress, disk full, out of memory, conda solve, verification failed, idle stop, Spot preemption — become findings with the evidence line and a fix.
- The panel shows the verdict, the findings and the log tail; the assistant, asked why, explains those findings and is told not to invent a cause they do not contain.
5 · Identities, and what each may do
| Service account | Attached to | May do |
|---|---|---|
sa-numen-controller | The webapp VM | Read compute, monitoring and logging; start and stop instances through a
custom role rather than instanceAdmin; log in to cluster
login nodes via OS Login; read three named secrets. It can also read every
user's bucket, because the File Manager serves all of them — per-user
separation above that line is enforced in the request path. |
sa-numen-u-<id> | That user's launched VMs | Object access to that one user's bucket, and nothing else. The credential on the machine cannot reach anyone else's data. |
sa-numen-runtime | Batch job VMs, and GKE pods through Workload
Identity (numen/numen-runtime) |
Write results to the outputs bucket; report to the Batch agent. Deliberately not granted access to user buckets, because it is shared. |
sa-terraform / sa-numen-runtime per lab |
That lab project's launches and jobs | The same two roles inside the lab, created with it: an org policy forbids using a service account across projects, so each lab carries its own pair. |
sa-numen-slurm | Cluster hosts | Run the cluster and read its own database password. |
sa-terraform | Infra Manager deployments and Cloud Build cluster builds | Project Editor, plus a narrowly conditioned grant so it can give a cluster access to its own secret. Scoping this down is open work. |
sa-numen-qa / sa-numen-qa-scheduler | The nightly golden run | Run one Cloud Build, read the QA password, write results to
qa/golden/ in the assets bucket. |
/scratch is one filesystem (per-user directories under
/scratch/users are created by the job prolog, but the head node is still
common ground). The console no longer submits jobs as the platform identity —
that path was removed — but per-user isolation still stops at the cluster
boundary. Fine for a single tenant; not fine once two customers could share a
cluster.
Numen on Google Cloud · Clovertex · read from the deployed sandbox, 2026-09-07 · back to the architecture overview