01 / 09

Adapter-based deploy control plane · apps/spindrift

Spindrift

A deployment is an artifact, a place, and a version of its config.

Pin those three and almost everything people build deploy tooling for stops being a feature. Every slide here reads in one line — open a band if you want the argument.

Four contracts 3 parts to the first 5 adapter seams Skim in 90 seconds

02 / 09

The problem

“I wrote a thing. Put it somewhere with a URL.”

Nothing on that list is unsolved — clusters, registries, GitOps engines and free certificates all exist. What nobody hands you is the assembly, and the assembly is the work, in full, every single time.

The seven decisions, made by hand
  1. Write a Dockerfile
  2. Author a Helm release or a Kustomization
  3. Find a hostname
  4. Wire a route
  5. Add a secret
  6. Open a PR, wait for CI, wait for Flux
  7. Read pod logs to find out why it did not come up

Seven decisions, made by whoever is holding it. Each one is a place to be quietly wrong for a month.

No amount of infrastructure you already own removes any of it. Owning the cluster is what makes the assembly your problem rather than somebody's product.

03 / 09

Contract one · what a deployment is

A deploy is three things, pinned

Artifact, Target, Config — each named by value rather than by reference, so no part of a deployment is allowed to mean “latest”.

The triple, and the lineage that makes it possible
Artifactwhat runs — by digest
Targetwhere it runs
Configwhat it reads — by digest

deploy 2 · artifact files·sha256:1d7ea… · target bluenose/static · configVersion sha256:4f53cda1…

A real row off the running installation — config_version is the column the digest is stored in. That line is the deployment, not a description of one and not a pointer to one. Reproducing it needs nothing that could have moved since.

Sourceone staged bundle, by digest
Buildcomponent · commit · target-shape
Artifactimage | files, by digest
Deploythe triple

One Build → one Artifact → many Deploys

Identity is kept separate from placement. What was built never learns where it went — and that one separation is what the rest of this deck is downstream of.

Two authored nouns

A human writes an App and a Component. Build, Artifact and Deploy are produced, never typed.

04 / 09

Consequences, not features

Four things nobody had to build

Rollback, two runtimes, safe concurrency and config that cannot drift are not features here. Each one falls out of having named the three parts by value.

What each one costs, which is nothing
Rollback

An older triple

Not a rebuild. Retention depth is 10, which is therefore also rollback depth.

Two runtimes

No new concept

An artifact is image or files. Kubernetes, Cloud Run and static hosting are the same triple with a different where.

Concurrency

Impossible, not racy

The triple is taken under a locking read against the transactional Component@Target.desired row.

Config

Cannot drift underneath

Scoped to (Component, Target) and pinned by digest into the deploy — so a running deployment reads what it was deployed with.

05 / 09

Contract two · where the far sides plug in

Five seams, and none of them is the architecture

Every outside thing is swappable without editing at the place it is used — and the proof one is real arrived unannounced: a build route that dials in rather than out landed as one row in a list.

The five seams, and their adapters today

A seam is a place behaviour can be swapped without editing at that place. One implementation would leave a seam hypothetical; every one of these has at least two.

SeamWhat varies across itAdapters today
Buildwhere the artifact is producedGitHub Actions · Cloud Build · microVM pool · in-cluster Job
Deploywhat runtime receives itKubernetes · Cloud Run · static hosting
Deliverywhich operator drives itFlux HelmRelease · Argo Application
Config storewhere the value of record livesSecret Manager · 1Password · HTTP
Datastorewhere a Target's state livescloud project · cluster

Flux is an adapter, not the architecture

A Target declares which operator drives it, and Spindrift writes that operator's own object. Both flavours end at the same status type and nothing above them branches — so the GitOps engine is a choice a Target makes, never a thing the platform is built out of.

apply · observe · destroy · run · executions · tail · inspect → seven verbs, and “deploy anything, anywhere” is behind them

The seam that runs backwards

Three build adapters reach outward — a workflow dispatch, a cloud API, a Job on a cluster this process already holds a token for. The fourth is a pool of microVMs on a machine Spindrift cannot open a connection to at all. Every instinct says that needs a new concept.

It needed a row in a list. The route writes an intent to an outbox row and polls that row for a verdict — exactly the way the other three poll a status endpoint. The endpoint just happens to be its own database.

The far side

A peer project, not a subsystem

Bosun keeps a warm pool of ephemeral microVMs, each serving exactly one CI job before it is destroyed. Spindrift does not run it, install it, or know anything about it beyond a label — the host long-polls in over three shared-secret endpoints and claims what it finds.

Why it earns its place

Proven, and honestly unserved

Two of the three original routes are somebody else's service; this one owes them nothing, and it clears the same SLSA L2 bar — verified against the build hull's own builder identity before core signs the digest. No host serves the class today: the route stays declared and ranked last, so bringing a host back serves it with no configuration change, and a build that falls through to it times out at the build budget rather than hanging.

That is the test a contract passes or fails, and it is not a test you can run on paper. An interface that only admits far sides you are able to dial was never a seam; it was the shape of the three things that happened to exist when it was written. Its deck is a sibling of this one, deployed the same way through the same product: bosun-slides-web.web.app

06 / 09

Contract three · who may reach it

Reach and auth are fields, not policy

They sit in desired state beside the image and the config, so they are pinned into the deploy like everything else — and a Target that cannot authenticate a reach is excluded before a build is ever dispatched.

The three reaches, the edge, and the refusal

Not a policy bolted on afterwards, and not a wiki page about which ingress to copy. A deployment therefore records who could reach it, as a fact, at that version.

Internalcluster only
Privatean RFC1918 address — the record type is the boundary
Publicthrough the tunnel

Private is the default. Nothing becomes reachable because somebody forgot a field.

A Target declares which reaches it can authenticate, not merely which it can serve. So auth decides where you are allowed to deploy.

The App edge

A filter on the route, not a library in the app

An auth: proxy Component gets an ExternalAuth filter on its HTTPRoute, enforced by an authentication proxy the platform owns and Spindrift never installs. Nothing is linked into the application, so an App cannot opt itself out of its own edge.

The control plane

A passkey against a real origin

First run is one screen: an unclaimed installation shows enrolment, a claimed one shows sign-in, and there is no toggle. The token is consumed on use. A passkey is scoped to the origin in the address bar, which is why the hostname is chosen before anything is installed — and why no password exists to phish.

The refusal

It is allowed to disqualify a backend

Static hosting refuses auth: proxy outright — there is no non-bypassable origin to put a boundary in front of, so the placement is rejected rather than shipped with a caveat. A guarantee that quietly degrades is not a guarantee.

reach: private · auth: proxy → AUTH_UNSUPPORTED excludes every Target that cannot authenticate at that reach, before a build is ever dispatched

07 / 09

Contract four · where authority stops

It cannot create what it deploys into — so it opens a pull request

Spindrift holds delegated write access to contents and nothing that can create a cluster, a project, a VPC, a tunnel or a signing key. A missing prerequisite becomes infrastructure code on a pull request, not a resource.

The ownership boundary, drawn

A release object in one namespace, an App inside one pre-provisioned project — that is the whole of its write access. When a prerequisite is missing it writes the infrastructure code that would clear it and opens that as a pull request: a human merges, the apply bot applies, and a standing check turns the row green.

VESSEL — A CLUSTER OR A CLOUD PROJECT Prerequisites — provisioned by whatever GitOps owns that platform Kubernetes: control-plane namespace · admission policy · target namespace + RBAC · edge workload GCP: project · VPC · tunnel · signing key · attestor · policy engine Shared operators · the authentication proxy · CA bundle · SOPS Secret · OCIRepository Contents — Spindrift owns these, through delegated APIs HelmRelease resources in `spindrift-apps` App resources inside the pre-provisioned vessel project It places workloads and never removes one: deleting an App or disconnecting a Target strands what is running, deliberately. Platform GitOps Flux, on a Kubernetes Target Infrastructure as code Terraform, in the cloud project Spindrift the running controller — authority granted to it, not to a human writes never creates A cluster, namespace, project, VPC, tunnel, signing key or policy engine is never created by Spindrift. Pull request the Terraform stanza + the root it belongs in The apply bot Atlantis, on the pull request The standing check turns the row green cleared outside Terraform? the row says so instead unmet prerequisite
The contract governs contents, never the container. Spindrift places a triple inside a vessel somebody else provisioned — which is also why the authentication proxy enforcing an App edge is one Flux owns and Spindrift only routes to.
08 / 09

Making the artifact half checkable

One signature, two verifiers

Because the artifact is named by digest, it is a thing a signature can be about. Four build routes converge on one digest, one key signs it, and two independent policy engines re-check it at admission.

Source to admission, end to end
Source repo at a commit or a ZIP upload — immutable, staged Source receipt what was fetched or uploaded, by which principal signed Bundle digest sha256:… a required build parameter on every route Hosted CI GitHub Actions, SHA-pinned SLSA L2 Cloud builder Cloud Build SLSA L3 Pool — a bosun skiff a microVM host, dialling in SLSA L2 In-cluster Job offline-capable SLSA L1 Admin-ranked routes, chosen after placement. The same BuildKit program over the same staged bundle — so which route ran is a property of a Build, not a different pipeline. Artifact one digest, type image or files staged in a build project, swept at 24 h by design an L2+ Target refuses L1 the one artifact digest Verify the build backend's provenance against the Target minimum (default L2) — before signing Sign & attest KMS signer key in trusted-builds via the `provenance` attestor Kyverno ClusterPolicy on the clusters, at admission Binary Authorization the vessel's own enforcing policy one signature, two verifiers Deploy — admitted an unsigned digest is rejected; a failed check is red, with no silent fallback
Four build routes converge on one digest; one key signs it; Kyverno on the clusters and Binary Authorization in the cloud each re-check it at admission. An L2+ Target refuses an L1 artifact rather than admitting it with a note.
09 / 09

Evidence, not assertion

This page is its own acceptance evidence

Uploaded as an archive, built on the hosted route into a files artifact, served at a URL by the thing it describes — and every clause above was proven on a live installation rather than a fixture.

What a live installation found that 2,283 green tests could not

Enrolment. Target connection. Archive to URL. Repository to signed artifact. Admission on a second Target.

Not fixtures — a running installation, with the digests, build numbers and response codes written down each time. An independent confirmer re-observed the last of them from scratch.

2,283tests green at 6f0d870c
5clauses proven live
3/4Targets healthy on first probe
4defects only a live install found
  • Two Terraform roots that each need the other applied first — the runbook stated one direction.
  • A derived string carries no dependency edge, so grants ran before the account they named existed.
  • Six prerequisite rows failing with one byte-identical certificate-trust sentence.
  • A read on a URL the API does not serve, which made a second deploy collide with the site the first one created — so a static App could be published exactly once.

That last one is fixed, and this revision is the proof: these words reached you as a second deploy onto the site the first one made.

Three of the four are wrong sentences shown to an operator. No passing suite would ever have caught them.

What the contract cost, and what is still open

What the contract cost

Three fields on a row. Everything in this deck — rollback, two runtimes, safe concurrency, an auth boundary that cannot be opted out of — is downstream of deciding to name things by value.

Open

The work left before a v1 is known and unglamorous. What is still outstanding is mostly wrong sentences shown to an operator — the failure mode a green suite is worst at catching, and the one the band above is entirely about.

Artifact + Target + Config 5 seams, every one of them real Everything else is bookkeeping