COMMITMENTS IN THIS DOCUMENT
Each is a separate yes/no. Silence on any one of them means the corresponding step does not happen, and this document is not a go for any of them.- Argo CD becomes a permanent cluster component of
lke590708— the production cluster, today running 47 pods invexa-productionwith no GitOps controller of any kind. Installing it is not reversible in practice: once prod is adopted, Argo is the deploy path. Yes / no. - Kyverno becomes a permanent admitting webhook on
lke590708— a failing or misconfigured admission webhook can block pod creation cluster-wide, including runtime-spawned bot pods. Yes / no. - The prod pin set becomes a signed, published channel entry built from digests currently observable only in the live cluster (§ 3). Publishing it makes those digests a durable artifact we can be held to. Yes / no.
- The Helm release
vexa-platforminvexa-production(currently revision 138) is retired as the deploy mechanism after adoption —helm upgradeagainst it stops being a supported action and thedrift-detectorCronJob that reads it is turned off (§ 6.7). Yes / no. - A precedent: prod deploys become pin moves. This is the first environment we operate this way; whatever shape it takes becomes how we do it and how we describe it to subscribers. Yes / no.
Why this is a document and not a RUNBOOK section
RUNBOOK.md § 4 and § 5
are cranks — the per-release internal-channel crank and the station crank, repeated
every release, identical each time. This is a one-time, dated, ceremony-gated
transition with a before-state that will never recur, and it is specific to our own
estate (namespaces, release names, volume ids) rather than to any subscriber. Putting it
in the RUNBOOK would either bloat the crank with a step nobody runs twice, or shrink the
plan to a size that hides its hazards. It composes with the RUNBOOK by reference: § 4
publishes the entry this plan pins, § 5 installs the station this plan subscribes.
RUNBOOK.md § 4 carries a one-line pointer here.
1 · What vexa-production is today — verified 2026-08-25, read-only
Every prod resource carries Helm’s ownership metadata:
vexa-staging
(Helm release vexa-platform rev 203, status failed, zero pods — the stage hold),
vexa-v012-staging (release vexa rev 10), channel-registry, monitoring,
platform-ops, shared-data.
2 · The three hazards that decide whether adoption is safe
These are the reasons this cannot be a five-line “point Argo at it” instruction. All three are properties of prod as it actually stands, verified above.2.1 The instance label is inside 13 immutable selectors
13 of 13 Deployments carryapp.kubernetes.io/instance: vexa-platform inside
.spec.selector.matchLabels, which Kubernetes forbids changing after creation. Argo
CD’s default resource-tracking method is to stamp its own Application name into
app.kubernetes.io/instance. The ApplicationSet in
kit/argocd/applicationset.yaml
names its Applications vexa-{{ .env }}. Left at the default, adoption would attempt to
rewrite that label to vexa-enterprise-prod on every workload — rejected on the selector,
and where it lands on pod templates it orphans running pods.
Required before the prod Application exists: Argo CD configured with annotation-based
tracking — application.resourceTrackingMethod: annotation in argocd-cm, so ownership
is recorded in the argocd.argoproj.io/tracking-id annotation and no existing label is
touched. This is the single setting that makes “the diff is ownership metadata only” true
rather than aspirational. Verify it on the staging rehearsal before prod, and verify it
again on prod with kubectl -n argocd get cm argocd-cm -o yaml before the first diff.
2.2 The release name must match or Argo builds a parallel copy of production
kit/argocd/applicationset.yaml hardcodes helm: releaseName: vexa. Prod’s resources are
named from the release vexa-platform (vexa-platform-vexa-meeting-api,
vexa-platform-caddy, …). Applied as written, Argo would not adopt anything — it would
create a second, parallel set of workloads named vexa-* beside the live ones, in the
live namespace, on the live database. The prod element’s releaseName must be
vexa-platform, and the rendered names must be compared to the live names before the
first sync (§ 6.4).
2.3 An in-cluster job is watching for exactly what we are about to do
The CronJobvexa-platform-drift-detector runs every 30 minutes, does
helm get manifest vexa-platform -n vexa-production and kubectl diff, and reports drift.
After adoption the Helm release stops being the source of truth, so this job reports
permanent drift, or errors, forever — and its alerting becomes noise precisely when we
need signal. It must be turned off, or repointed at the Argo Application, as part of the
migration, not after it. (It is already failing intermittently — the run 10 minutes before
this document was written exited non-zero.)
Two neighbours worth knowing but not hazards: vexa-platform-bot-reaper deletes pods
matching runtime.managed=true, which are runtime-spawned and never chart-managed, so
Argo will not track or prune them; and vexa-platform-capacity-resize /
collector-watchdog / system-host-labeler mutate node labels and a capacity-reserve
Deployment’s replica count every 5 minutes. capacity-reserve replicas are therefore a
live-mutated field — with selfHeal: true Argo would fight the CronJob. Prod’s element
in the ApplicationSet is automated: "false" (no selfHeal, no prune) precisely because
prod moves only on a pin, which happens to also resolve this. Keep it that way, and if
automation is ever wanted on prod, add
ignoreDifferences on deployment/vexa-platform-capacity-reserve .spec.replicas first.
3 · Where the pin comes from — the problem to solve before step 1
The digest set that is production exists only in the live cluster.~/dev/vexa-platform/release/registry.yamlis an evidence-check registry (126 checks: type, script, modes, access, proves). It contains no digest pins. It has never been the pin store and nothing else in that repo is.- The repo carries tags:
values-production.yamlsetsvexa.imageTag: 0.12.4-260714-hc7and the comment beside it still reads “prod is NOT deployed yet (human-gated)”. - The live release’s stored values contain no
vexa.imageTagat all, yet every one of the 47 pods runs a@sha256:reference. The mapping from repo values to running digests is not reconstructible from the release object. - The repo declares subchart
vexa0.12.4; prod’s live labels sayvexa-0.12.16.
- The pin must be minted from the cluster, once, deliberately. Snapshot the full
image → digestmap (/tmp/dogfood/prod-images.txt, captured 2026-08-25, is the shape: 21 entries, everyvexaai/*and every third-party base image already resolved to a digest). - That snapshot becomes a
vexa-internalcandidate entry through RUNBOOK § 4 — built, signed, pushed, with the platform chart rendered againstvalues-production.yamland the digests baked in. Until that entry exists, there is nothing to pin to, and every later step in this document is blocked on it. - The chart the entry carries must be the platform chart, not the OSS chart. The
channel today carries
vexa; prod runsvexa-platform(billing workers, analytics, transcription-gateway, webapp, dashboard). This is gap 1 of the founder’s 2026-08-25 pivot chain and it is a prerequisite, not a detail.
vexa-internal at prod’s position should be built to
reproduce what is already running, not to change it. A migration that also upgrades is
two experiments in one namespace.
4 · Prerequisites, with the inertness requirement stated
Inertness requirement, stated explicitly because it is the whole safety story of P2 and
P3: installing Argo CD and Kyverno must not change one byte of
vexa-production.
Concretely — Argo installs with no Application selecting that namespace; Kyverno installs
with every policy in Audit mode and namespaceSelector excluding vexa-production;
neither is given a cluster-wide mutating webhook. The acceptance test is a diff of the
before/after generation numbers of all 13 Deployments (prod-gens-BEFORE.txt is that
baseline — e.g. vexa-platform-vexa-meeting-api generation 125, webapp 82,
caddy 33). Any generation that moves during P2 or P3 is a stop.
Cluster-scoped installs are owned by whoever holds the cluster-install custody for this
work; this plan neither performs them nor claims them.
5 · Subscription shape — pinned, never *
Prod’s element in the ApplicationSet, with the three corrections this plan requires:
*; production follows a pin and only a pin. Nothing on the publisher
side can advance it: publishing a new entry makes a release available to prod and does
nothing else. That asymmetry is the product claim, and prod is where we have to actually
live inside it.
The prod contract
(vexa-internal-prod-2026 — since 2026-08-25 the record is
channels/vexa-internal/contracts/internal-prod.json in the private
vexa-stations repository) already requires
{kind: station-verdict, station: vexa-staging} — so a candidate that staging has not
validated and signed cannot pass prod’s PreSync verify even if someone moves the pin. That
is a machine-enforced gate underneath the human one in § 8, not a substitute for it.
6 · The migration, step by step
Identical in mechanism to the staging rehearsal (P5) — same SSA adoption, same diff-before-sync, same reconcile-by-values loop. What differs is only that prod is notautomated and that every step below has a named stop condition.
6.1 — Freeze the other write surface. Announce that no helm upgrade, kubectl set image or hand patch touches vexa-production for the duration. Two writers on one
surface is how this environment produced its drift in the first place.
6.2 — Re-capture the before-state into the migration receipt directory: pods,
Deployment generations, the full image→digest map, helm get manifest vexa-platform -n vexa-production, and kubectl get pv. The 2026-08-25 captures under /tmp/dogfood/ are
the shape; re-take them on the day, because a generation that moved between then and now
means someone deployed.
6.3 — Create the prod Application with sync disabled. automated: "false" already
means no auto-sync. Create it and go no further; Argo now knows about prod without having
written to it. Argo reports OutOfSync — that is expected and is the input to the next
step, not a failure.
6.4 — Diff before sync. This is the gate, not a formality.
- a rendered name that does not exist live, or a live name that Argo does not render (§ 2.2 has happened);
- a diff on
.spec.selectorof any Deployment (§ 2.1 has happened); - a diff on any image digest (the pin does not reproduce what is running — go back to § 3);
- a diff on any Secret, PVC, or the
vexa-platform-caddyService’sLoadBalancerfields (a churned NodeBalancer is a public-IP change).
argocd.argoproj.io/tracking-id annotation
and nothing else. A single behavioural field left in the diff means the pin does not
describe production, and syncing would deploy a change while claiming to adopt one.
The known normalisation exception is already in the ApplicationSet —
ignoreDifferences on StatefulSet .spec.volumeClaimTemplates, where the API server
defaults fields Argo cannot normalise. Prod has zero StatefulSets, so this exception is
inert here; it matters for staging.
6.6 — Sync with ServerSideApply. syncOptions: [ServerSideApply=true] is already in
the ApplicationSet template and is what makes adoption non-destructive: Argo becomes an
additional field manager on objects that already exist, rather than replacing them.
Client-side apply is the legacy mechanism under the ownership-conflict bug class and is
not what happens here. Expected result: zero pod restarts, zero Deployment generation
changes. Verify against 6.2’s capture immediately, per-Deployment. A generation bump is
not “adoption noise” — it is a rollout, and it means 6.5 exited early.
6.7 — Retire the old write surface. Disable vexa-platform-drift-detector (§ 2.3).
Leave the Helm release secrets in place — sh.helm.release.v1.vexa-platform.v129…v138
are 10 revisions of history and cost nothing. Do not helm uninstall; with resources now
also owned by Argo, an uninstall is a deletion of production.
6.8 — Soak, then write the receipt. One full business day at minimum, covering at
least one run of every CronJob (the longest cycle is db-backup, daily at 02:00, and
slo-check at 05:30). Then a receipt under docs/receipts/ naming the pin, the entry
digest, the before/after generation table, and the PV state from § 7.
7 · The eight Retain PVs are untouched — named
No step in this plan creates, deletes, resizes, rebinds or reclaims a PersistentVolume, and none of them appears in the channel chart’s managed set. Naming them so “untouched” is checkable rather than reassuring. All 25 PVs onlke590708 use linode-block-storage-retain with
persistentVolumeReclaimPolicy: Retain. The distinct set of exactly eight is the
Released Retain PVs — orphaned by past vexa-production PVC churn, still holding
their data because the policy is Retain, still costing Linode block storage:
The three PVs that are live production storage, equally untouched:
Verification, before and after, and the result goes in the receipt:
Retain, 8 Released, the same 3 Bound in
vexa-production, no PV created, no PV deleted. Any change is a stop and a rollback
trigger, not a note.
Two things this plan deliberately does not do: it does not clean up the 8 released
volumes (that is a separate decision with its own approval, and doing it inside a migration
would make an unrelated deletion look like migration fallout), and it does not adopt the
caddy-data PVC’s certificate state into anything — Caddy re-issues on its own if it ever
loses it, which is a behaviour to know, not to change here.
8 · The ceremony
The gate is: prod-owner custody + founder approval of the exact pin string. Where it sits relative to the locks that already exist:
The asks, in the order they become real — none of them is an ask today, because the
artifacts do not exist yet:
- When the platform-chart candidate entry exists on
vexa-internal(P1): approve that entry as prod’s initial pin. What it does: makes the pin string real. Consequence if unanswered: the migration cannot start; prod stays helm-cranked. - When P2–P6 are green and § 6.5 has exited with an ownership-metadata-only diff: approve the sync. What it does: Argo becomes an owner of production. Consequence if unanswered: prod is Argo-visible and unsynced, which is a stable and reversible state (delete the Application; § 9.1).
- On every release thereafter: approve the pin move. What it does: deploys. That is the point of the whole exercise — the prod deploy becomes one approved line.
stage-own still
gates who may act; deploy no longer runs helm upgrade against vexa-production.
9 · Rollback
9.1 Before the first sync
Delete the prod Application with the non-cascading option so no managed resource is touched:deployed at
revision 138 and still authoritative. This is the free exit and it is available through
step 6.5.
9.2 After adoption — rollback is a pin move
docs/receipts/: the pin moved from, the
pin moved to, why, what the diff showed, generations before and after, and the PV check
from § 7.
What a pin move does not roll back: database schema. Convergence
(admin_api/schema/sync.py) is additive and forward-only — an upgrade that added a column
still has that column after the pin moves back. For a schema-affecting release the rollback
plan is snapshot-then-restore, and it is not this document’s mechanism.
10 · Rehearsed vs assumed — stated honestly
Rehearsed, on a throwaway cluster (2026-08-24 week-exit rehearsal): clean-room bootstrap, wrong-key refusal, install from the channel, PreSync verify proven both ways, station bundle emit/ingest, gated publish, self-upgrade in 26s with zero kubectl. Rehearsed, on the standing station: the ApplicationSetenv-rename teardown — and it
is why § 5 says choose the element name once.
To be rehearsed on vexa-staging before prod (P5): the adoption dance itself —
annotation tracking, release-name match, diff-before-sync, reconcile-by-values. Staging is
at zero pods with a failed Helm release, which makes it a weaker rehearsal than it
sounds: bringing it up under Argo is a fresh install, not an adoption. To rehearse
adoption properly, staging must first be brought up by Helm to a healthy state and then
adopted. If that is skipped, prod adoption is the first adoption we have ever done, and
this document’s § 6 is a design, not a procedure.
Assumed, not proven, at prod scale:
- that ServerSideApply adoption of 13 Deployments + 48 NetworkPolicies + 7 PDBs produces zero generation bumps;
- that the rendered platform chart reproduces prod’s 21 image digests exactly (§ 3 says the mapping is not currently reconstructible from the release object — this is the highest single risk in the plan);
- that Kyverno can be installed inert beside 47 running pods;
- that pin-move rollback works on prod-sized state.