Provisioning & Infrastructure-as-Code (Terraform)
How a new tenant is stood up - the real pipeline behind the control plane's provisioning wizard.
How a new tenant is stood up — the real pipeline behind the control plane's provisioning wizard (product §17). This is "create a tenant → Cloud Run + Postgres + secrets" made concrete.
Principle: every tenant is Terraform
A tenant's entire stack is a Terraform module (infra/terraform/modules/tenant/) instantiated per customer. Provisioning = render the module with the tenant's variables + terraform apply. This gives consistency, drift detection, and a clean teardown. IaC defines databases, compute, IAM, secrets, and network — no click-ops.
module "tenant" {
source = "../../modules/tenant"
slug = "caledonia-ports"
display_name = "Caledonia Ports Authority"
region = "europe-west1"
plan = "pro" # sizes Cloud Run + Cloud SQL + Atlas tier
blueprint = "maritime"
owner_email = "f.macleod@caledonia-ports.uk"
}
# module creates: project, APIs, VPC+connector, Cloud SQL PG (RLS-ready),
# Atlas DB, GCS bucket (CMEK), Secret Manager secrets, KMS key ring,
# Cloud Run service, domain mapping, service account + IAM.Each tenant has its own Terraform state (workspace or state prefix) so applies are isolated.
The provisioning pipeline (maps 1:1 to the §17 wizard steps)
1. project + APIs gcloud projects create insigz-<slug>; enable run, sqladmin, secretmanager, …
2. network terraform: VPC + serverless connector + private ranges
3. postgres terraform: Cloud SQL PG16 (tier by plan, HA, CMEK); create db; RLS on
4. secrets generate db-password, jwt-key, provenance-key; put in Secret Manager (KMS)
5. deploy gcloud run deploy insigz-<slug> --image app:<ver> --set-secrets … --vpc-connector …
6. migrate + seed insigzctl migrate --tenant=<slug> --to=latest; insigzctl seed --blueprint=<bp>
7. dns + tls + register run domain-mapping; insigzctl tenant register --owner=… --plan=…
8. verify + invite curl /healthz; insigzctl invite --email=<owner> --role=ownerThe control plane's wizard streams exactly this; the friendly steps and the raw log in §17 are this pipeline. It is idempotent — a failed step (e.g. the Error-409 cooldown demoed in the UI) is safe to retry without dangling resources, because terraform apply reconciles to desired state and insigzctl operations are upsert-style.
The orchestrator
The control-plane backend (62) runs provisioning as a durable workflow (Cloud Workflows, decided D8), persisting each step's status so the UI can show live progress and so a crash resumes rather than restarts. It does not hold tenant DB credentials — it creates them in Secret Manager and the tenant's own service account reads them at boot.
insigzctl
A CLI (also callable as a library) for the app-level steps Terraform shouldn't own:
insigzctl migrate --tenant=<slug> --to=latest
insigzctl seed --tenant=<slug> --blueprint=<bp>
insigzctl tenant register|suspend|reinstate|decommission --slug=<slug>
insigzctl secret rotate --slug=<slug> --kind=db|jwt|provenance
insigzctl invite --tenant=<slug> --email=<e> --role=ownerDecommission performs the §17 danger-zone flow: 30-day retention snapshot (PG + Mongo + bucket) → terraform destroy → secret/KMS cleanup → audit.
Lifecycle states
provisioning → live → (degraded) → suspended ⇄ live → decommissioned. Suspend = scale Cloud Run to zero + block sign-in (data retained); reinstate reverses it. All transitions are control-plane actions, gated + audited (§17).
Gaps to close
- (Orchestrator decided: Cloud Workflows, D8.) Define the per-step status schema it persists (see
83). - Decide Atlas provisioning (Atlas Terraform provider) integration.
- Define the Terraform state backend (GCS) + locking, and per-tenant workspace naming.
- Write the idempotency/rollback contract per step + the decommission snapshot format.