Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / docs/RUNBOOK_OPERATIONS.md |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a |
| Branch | deploy-and-ops |
| Path or archive member | docs/RUNBOOK_OPERATIONS.md |
| Original SHA-256 | 08d8081a5ff6791e616d01bdea4093de18ce5898f5af8df50e7a93eea7c8995c |
| Git blob SHA | a3ae79e2716db713b4fca62eb556d8f55fe92550 |
| Reading-copy SHA-256 | 08d8081a5ff6791e616d01bdea4093de18ce5898f5af8df50e7a93eea7c8995c |
Recovered source transcript
Download complete source text ↓104 lines · 6.7 KB
# Operations runbooks
Day two operations for missionMATCH under the database per tenant model. Each section is a checklist with
a verification and a rollback. New client onboarding is `docs/RUNBOOK_ONBOARDING.md`; the one time move of
the existing shared database into per tenant is `docs/MIGRATE_TO_PER_TENANT.md`.
## 1. Provisioning and deploy
Stand up or update the platform.
1. Infrastructure: `terraform -chdir=infra/terraform apply -var-file=environments/aws-govcloud.tfvars`
(operator, real credentials). See `infra/terraform/README.md`.
2. Schema: run goose against the control plane database (creates the pgvector extension, the
missionmatch_app, missionmatch_migrator, and missionmatch_compliance roles, the schema, RLS). For the
tenant fleet, `make migrate-all-tenants-check` then `make migrate-all-tenants`.
3. Identity genesis: `provision-org` for the first org and owner.
4. Build and publish the images: `docker build -f infra/docker/Dockerfile.mm-api ...` and the gateway (the
in build FIPS gate must pass), push to the registry.
5. Deploy with the pod securityContext in `infra/docker/README.md` (non root 65532, read only rootfs,
`/tmp` tmpfs, drop ALL capabilities) and the env: `MM_TENANCY=per_tenant`, `MM_CLOUD`, the three control
plane DSNs, `MM_SECRETS_BACKEND` (a government cloud store in production), the pool bounds, and the auth
secrets.
Verify: `GET /healthz` is ok, `GET /readyz` is ready (the control plane pool answers), and
`GET /healthz/fips` reports `fips_enabled: true`. Rollback: redeploy the previous image tag; infrastructure
changes roll back with the prior Terraform state.
## 2. Scaling
The API is stateless (sessions are opaque and server side in the control plane), so any pod serves any
request.
1. API throughput: increase the replica count. No sticky sessions are needed.
2. Per tenant connection budget (the load bearing limit): `MM_TENANT_MAX_POOLS` times
`MM_TENANT_POOL_MAX_CONNS`, plus the control plane and compliance pools, must stay under the database
`max_connections`. As the tenant fleet grows, watch the `/metrics` pool gauges and the database
connection count; the bounded LRU pool registry evicts idle tenants, so the live pool count is bounded,
not the tenant count. Tune the two bounds (lower MaxConns per pool before raising MaxPools).
3. Database: scale the instance class vertically, or add read replicas for read heavy reporting. The
control plane is small (identity and the directory); each tenant database scales on its own.
Verify: the pool gauges stay below the connection ceiling under load, and p95 latency holds. Rollback:
lower the replica count or the pool bounds; a pooler in front of any database MUST be in TRANSACTION mode,
never statement mode, or the per request org GUC isolation breaks.
## 3. Failover
1. Database: managed Postgres is multi AZ, so a node failure fails over automatically to the standby on
the same endpoint. The pgx pool reconnects; the per tenant pool manager Pings before serving and
returns a typed 503 (never a hang) on a still unreachable tenant, then reopens on recovery.
2. Control plane: it is a single point for identity and routing, so it must be multi AZ like the tenant
databases. While it is down, login and routing fail, but each tenant database is independent and its
data is intact; recovery is automatic on control plane failover.
3. API: a rolling deploy with the readiness probe gates traffic away from a starting or unhealthy pod.
Verify: after a forced failover, `GET /readyz` returns to ready and a tenant request serves its own data.
Rollback: failover is automatic; if a node is unhealthy, replace it from the standby.
## 4. Backup and recovery (per database)
Every database (the control plane and each tenant) is covered by the managed automated backups (the
Terraform `backup_retention_period`) plus on demand logical backups.
1. On demand backup: `scripts/backup-tenant.sh -dsn "<migrator DSN to the tenant DB>" -out FILE` (a custom
format archive of the whole database, public and eeo and the vectors; the tenant stays online). Pin the
client to PostgreSQL 16 (set `PGBIN`).
2. Recovery: take the tenant offline in the directory first
(`UPDATE tenant_directory SET status='offline' WHERE organization_id=...`, so the router refuses it with
a typed error), then
`scripts/restore-tenant.sh -admin-dsn "<superuser>" -migrator-dsn "<migrator>" -source <db> -target <db> -dump FILE`.
This is the procedure proven on M4 staging (BUILD_REPORT B4): it mirrors provision-tenant's prep,
restores, and verifies row count equality across public and eeo, a vector sample, and the isolation
invariants. On a green verify, set the status back to `active`.
Verify: the restore script's own verify must pass (row counts match, vectors present, invariants hold).
Rollback: if a restore is bad, leave the tenant offline and restore from an earlier backup; never flip a
tenant active on a failed verify.
## 5. Secret rotation
All secrets rotate without a flag day because the load bearing ones are versioned.
1. Per tenant database credential: rotate the value in the secret store under the tenant's reference. The
secrets port caches with a short TTL (`MM_SECRET_CACHE_TTL_SECONDS`) and force refreshes on an
authentication failure, so the app picks up the new credential within the TTL or on the next failed
connect and retries with backoff. Keep a separate higher privilege identity for rotation. Verify: a
request to that tenant still serves after the old credential is invalidated. Rollback: restore the prior
secret value.
2. The credential pepper (`MM_CREDENTIAL_PEPPER`): set the new pepper, increment
`MM_CREDENTIAL_PEPPER_VERSION`, and keep the prior key in `MM_CREDENTIAL_PEPPER_PREVIOUS`. Old hashes
verify under the previous version and are rehashed to the new one on the next login. Verify: existing
users can still log in; new credentials carry the new version. Rollback: revert the version and the keys.
3. The TOTP KEK (`MM_TOTP_KEK`, 32 byte AES 256): set the new key, increment `MM_TOTP_KEK_VERSION`, keep the
prior key in `MM_TOTP_KEK_PREVIOUS` so existing TOTP secrets still decrypt. Verify: enrolled users pass
a TOTP check. Rollback: revert.
4. The CSRF key (`MM_CSRF_KEY`) and the recovery lookup key (`MM_RECOVERY_LOOKUP_KEY`): rotate the value.
A CSRF key rotation invalidates in flight CSRF tokens (clients fetch a fresh one on the next unsafe
request), so rotate at a low traffic window. Verify: a login and an unsafe request succeed after
rotation. Rollback: restore the prior key.
Never log a resolved DSN, password, or secret value; the secrets port logs only the reference and the
tenant id. All keys load from the server side environment file or the cloud secret store, never the repo
or the browser bundle.