TEN-WP-0009: make database placement portable rather than settled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
121968d65b
commit
cf7ca1a692
2 changed files with 94 additions and 17 deletions
|
|
@ -17,6 +17,7 @@
|
||||||
| workplan | TEN-WP-0006 | finished | — | workplans/TEN-WP-0006-guardrail-quota-policy.md |
|
| workplan | TEN-WP-0006 | finished | — | workplans/TEN-WP-0006-guardrail-quota-policy.md |
|
||||||
| workplan | TEN-WP-0007 | finished | — | workplans/TEN-WP-0007-guardrail-production-rollout.md |
|
| workplan | TEN-WP-0007 | finished | — | workplans/TEN-WP-0007-guardrail-production-rollout.md |
|
||||||
| workplan | TEN-WP-0008 | ready | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
| workplan | TEN-WP-0008 | ready | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
||||||
|
| workplan | TEN-WP-0009 | ready | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
| task | ADHOC-2026-07-24-T01 | done | — | workplans/ADHOC-2026-07-24.md |
|
| task | ADHOC-2026-07-24-T01 | done | — | workplans/ADHOC-2026-07-24.md |
|
||||||
| task | TEN-WP-0001-T01 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
|
| task | TEN-WP-0001-T01 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
|
||||||
| task | TEN-WP-0001-T02 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
|
| task | TEN-WP-0001-T02 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
|
||||||
|
|
@ -53,3 +54,9 @@
|
||||||
| task | TEN-WP-0008-T02 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
| task | TEN-WP-0008-T02 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
||||||
| task | TEN-WP-0008-T03 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
| task | TEN-WP-0008-T03 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
||||||
| task | TEN-WP-0008-T04 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
| task | TEN-WP-0008-T04 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
|
||||||
|
| task | TEN-WP-0009-T01 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
| task | TEN-WP-0009-T02 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
| task | TEN-WP-0009-T03 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
| task | TEN-WP-0009-T04 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
| task | TEN-WP-0009-T05 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
| task | TEN-WP-0009-T06 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ depends_on:
|
||||||
- TEN-WP-0007
|
- TEN-WP-0007
|
||||||
unblocks:
|
unblocks:
|
||||||
- TEN-WP-0008
|
- TEN-WP-0008
|
||||||
|
state_hub_workstream_id: "3bf3b4ab-a116-4914-b2d8-d3c430754956"
|
||||||
---
|
---
|
||||||
|
|
||||||
# TEN-WP-0009 - PostgreSQL production store
|
# TEN-WP-0009 - PostgreSQL production store
|
||||||
|
|
@ -57,33 +58,88 @@ So **no secret enters this repo, this workplan, or any evidence dump**, and no
|
||||||
or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an
|
or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an
|
||||||
operator-minted in-cluster secret is outside that lane.
|
operator-minted in-cluster secret is outside that lane.
|
||||||
|
|
||||||
## T01 - Decide the topology and write the cluster manifest
|
## Placement: shared by default, movable by design
|
||||||
|
|
||||||
|
Dedicated-vs-shared is **not a decision this workplan should settle once**, and
|
||||||
|
`tenant-engine` is the wrong place to settle it for the platform. The estate
|
||||||
|
has distinct elements — railiance, NetKingdom, HelixForge, Coulomb — plus
|
||||||
|
tenants layered on top, and which repo belongs to which grouping is not yet
|
||||||
|
organised. On top of that, isolation level is heading toward being a
|
||||||
|
*product* property: plans will differ in how much isolation they buy.
|
||||||
|
|
||||||
|
So the requirement here is **portability, not placement**:
|
||||||
|
|
||||||
|
- start on a **shared** cluster, because it is cheaper on a single node and
|
||||||
|
nothing yet justifies dedicated capacity;
|
||||||
|
- make moving to a dedicated cluster — or to a different shared one — an
|
||||||
|
**operational change, not a code change**;
|
||||||
|
- never let the decision leak into the application.
|
||||||
|
|
||||||
|
What that demands of this repo, and all of it is cheap if done now and
|
||||||
|
expensive to retrofit:
|
||||||
|
|
||||||
|
1. **Connect by injected URL only.** No cluster name, namespace, host, or
|
||||||
|
database name anywhere in `src/`. The application must be unable to tell
|
||||||
|
whether it is on a shared or dedicated cluster.
|
||||||
|
2. **Own a whole database, not a set of tables in someone else's.** Sharing a
|
||||||
|
*cluster* is a capacity decision; sharing a *database* would entangle
|
||||||
|
schemas and make relocation a merge rather than a move. A dedicated
|
||||||
|
database inside a shared cluster relocates with a dump and a restore.
|
||||||
|
3. **No cross-database joins or co-location assumptions**, which the repo
|
||||||
|
boundary already forbids — `tenant_id` is the only key shared with
|
||||||
|
`user-engine`. Placement must not quietly become a dependency.
|
||||||
|
4. **Idempotent schema creation**, so a fresh target comes up correct without a
|
||||||
|
hand-built database.
|
||||||
|
|
||||||
|
Then "move `tenant-engine` to dedicated" is: create the target cluster, dump,
|
||||||
|
restore, swap the `secretKeyRef`, restart. No rebuild, no release, no code
|
||||||
|
review.
|
||||||
|
|
||||||
|
**What this repo does not own:** the placement *policy* — which element or
|
||||||
|
tenant gets dedicated capacity and when. That is a platform concern, and
|
||||||
|
`tenant-engine` implementing its own would be the same boundary error as
|
||||||
|
building a fleet drift-detector because we got bitten. Raised with
|
||||||
|
`railiance-platform` separately; see T01.
|
||||||
|
|
||||||
|
## T01 - Make placement portable and provision on shared capacity
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TEN-WP-0009-T01
|
id: TEN-WP-0009-T01
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873"
|
||||||
```
|
```
|
||||||
|
|
||||||
Decide between a dedicated `tenant-engine-pg` cluster in our namespace and
|
Provision a dedicated **database** on an existing **shared** cluster, per the
|
||||||
sharing an existing one (`net-kingdom-pg` or `apps-pg` in `databases`).
|
principles above. Candidates are `net-kingdom-pg` (already exists, and
|
||||||
|
`tenant-engine` is a NetKingdom service) and `apps-pg`. Prefer the one whose
|
||||||
|
grouping matches the product family, since that is the axis a future split
|
||||||
|
would most likely follow — moving is cheap, but moving less often is cheaper.
|
||||||
|
|
||||||
Argue it rather than defaulting. Points to weigh: `user-engine` chose dedicated
|
Note the deviation from precedent and why: `user-engine` runs a dedicated
|
||||||
and is the closest precedent; a dedicated cluster keeps blast radius and backup
|
`user-engine-pg`. We are deliberately *not* copying that. Their choice is fine
|
||||||
policy per-service; a shared cluster is cheaper on a single node and
|
and ours is reversible in an afternoon; picking dedicated now would spend
|
||||||
`net-kingdom-pg` already exists for this product family. Note that
|
capacity on an isolation guarantee nothing has asked for yet.
|
||||||
`tenant-engine` is consulted by `flex-auth` on the authorization path, so its
|
|
||||||
availability envelope is not purely its own.
|
|
||||||
|
|
||||||
Write the CNPG `Cluster` manifest into `deploy/`, matching the fleet's existing
|
Record what would trigger a move to dedicated, so the reversal is a judgement
|
||||||
image and storage conventions rather than inventing new ones.
|
already made rather than one improvised under pressure. Candidate triggers:
|
||||||
|
a noisy neighbour affecting the authorization path, a compliance or residency
|
||||||
|
requirement, a plan tier that sells isolation, or the shared cluster's backup
|
||||||
|
policy no longer fitting.
|
||||||
|
|
||||||
Also decide **backups**. The `scheduledbackups.postgresql.cnpg.io` CRD is
|
Also decide **backups**: whether the shared cluster's existing
|
||||||
available; SQLite-on-a-PVC had no backup story at all, so this is a gain to
|
`scheduledbackups` policy covers us, or we need our own. SQLite-on-a-PVC had no
|
||||||
claim explicitly rather than inherit by accident.
|
backup story at all, so this is a gain to claim explicitly rather than inherit
|
||||||
|
by accident — and a shared cluster means inheriting *someone else's* retention
|
||||||
|
choice, which is worth checking rather than assuming.
|
||||||
|
|
||||||
Done when the topology decision is recorded with its reasoning and the manifest
|
Write whatever manifest the choice needs into `deploy/` — a CNPG `Database`
|
||||||
is committed.
|
resource against the shared cluster, not a new `Cluster`, if that is the
|
||||||
|
correct shape.
|
||||||
|
|
||||||
|
Done when the database exists on shared capacity, the connection reaches the
|
||||||
|
app only through an injected URL, and the move-to-dedicated trigger list and
|
||||||
|
runbook are recorded.
|
||||||
|
|
||||||
## T02 - Implement `PostgresTenantStore`
|
## T02 - Implement `PostgresTenantStore`
|
||||||
|
|
||||||
|
|
@ -91,6 +147,7 @@ is committed.
|
||||||
id: TEN-WP-0009-T02
|
id: TEN-WP-0009-T02
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "d7428bc1-2e5f-4a4c-93f5-9368ebb691d5"
|
||||||
```
|
```
|
||||||
|
|
||||||
Implement the full `TenantStore` Protocol — including
|
Implement the full `TenantStore` Protocol — including
|
||||||
|
|
@ -128,6 +185,7 @@ Done when all three backends pass the same conformance suites unchanged.
|
||||||
id: TEN-WP-0009-T03
|
id: TEN-WP-0009-T03
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "89c121c6-8bc5-4abe-80f0-d11406aeffc2"
|
||||||
```
|
```
|
||||||
|
|
||||||
`Settings` currently carries only `database_path`. Add a database URL and make
|
`Settings` currently carries only `database_path`. Add a database URL and make
|
||||||
|
|
@ -156,6 +214,7 @@ by tests.
|
||||||
id: TEN-WP-0009-T04
|
id: TEN-WP-0009-T04
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "51a83eae-9e9f-4676-bc1d-9c2d884bac0d"
|
||||||
```
|
```
|
||||||
|
|
||||||
Move the live SQLite database to PostgreSQL. Currently small — two tenants
|
Move the live SQLite database to PostgreSQL. Currently small — two tenants
|
||||||
|
|
@ -191,6 +250,7 @@ recorded here.
|
||||||
id: TEN-WP-0009-T05
|
id: TEN-WP-0009-T05
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "03aeae3b-5e0f-4c77-a763-787ae08078f5"
|
||||||
```
|
```
|
||||||
|
|
||||||
Update `deploy/tenant-engine.yaml`:
|
Update `deploy/tenant-engine.yaml`:
|
||||||
|
|
@ -223,6 +283,7 @@ service.
|
||||||
id: TEN-WP-0009-T06
|
id: TEN-WP-0009-T06
|
||||||
status: todo
|
status: todo
|
||||||
priority: medium
|
priority: medium
|
||||||
|
state_hub_task_id: "e7adbc64-ad1b-4f28-bfd0-0b07034629db"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `flex-auth`: availability characteristics of their data source changed. No
|
- `flex-auth`: availability characteristics of their data source changed. No
|
||||||
|
|
@ -243,4 +304,13 @@ Done when the notes are sent and TEN-WP-0008-T02 is updated.
|
||||||
change at the same time makes any regression ambiguous.
|
change at the same time makes any regression ambiguous.
|
||||||
- **Sharing a database with `user-engine`.** `tenant_id` is the only key the
|
- **Sharing a database with `user-engine`.** `tenant_id` is the only key the
|
||||||
two services share, and it stays that way — separate stores are the boundary,
|
two services share, and it stays that way — separate stores are the boundary,
|
||||||
not an implementation detail.
|
not an implementation detail. Sharing a *cluster* is fine; sharing a
|
||||||
|
*database* is not.
|
||||||
|
- **Fleet placement policy.** Which element or tenant gets dedicated capacity,
|
||||||
|
and how isolation maps to plan tiers, is a platform decision. This workplan
|
||||||
|
makes `tenant-engine` movable and stops there.
|
||||||
|
- **Per-tenant isolation.** A future plan tier may sell dedicated
|
||||||
|
infrastructure. `tenant-engine` already records plan assignment by id, and
|
||||||
|
`adaptive-pricing` owns what a plan *means*, so neither the tier definition
|
||||||
|
nor the placement that implements it belongs here. Worth designing for —
|
||||||
|
which the portability principles above do — not worth building for.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue