insigz docs
Engineering

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=owner

The 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=owner

Decommission 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.