Reconcile Fly Machines
Show the whole prompt
Read https://intentius.io/chant/tutorials/fly-machines-reconcile/.
In `examples/fly-reconcile`, run `npm install` and boot mudflaps as the page shows. Run `npx chant run fly-reconcile --no-record` against it.
Run it again unchanged, then change a Machine in `src/infra.ts` and run it once more, and show me the output of each run.
Keep the local endpoint override in place and set no `FLY_API_TOKEN`. Shut mudflaps down at the end.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/tutorials/fly-machines-reconcile/.In `examples/fly-reconcile`, run `npm install` and boot mudflaps as the page shows. Run `npx chant run fly-reconcile --no-record` against it.Run it again unchanged, then change a Machine in `src/infra.ts` and run it once more, and show me the output of each run.Keep the local endpoint override in place and set no `FLY_API_TOKEN`. Shut mudflaps down at the end.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The fly-reconcile
example shows a stateless reconcile on Fly. Fly’s
Machines API has no server-side declarative apply and chant keeps no state
file, so flyApply does the reconcile itself. It diffs your typed resources
against what the platform reports, and prunes only the machines it owns.
Where local-fly boots and tears down the
emulator each run, this example points at a persistent mudflaps, so you run
the op repeatedly and watch the reconcile converge.
The infra
Section titled “The infra”src/infra.ts is one app, a persistent volume, and two machines, web (which
mounts the volume) and worker:
import { App, Machine, Volume, MachineConfig, MachineGuest, MachineMount, Fly } from "@intentius/chant-lexicon-fly";
const app = new App({ name: "fly-reconcile-demo", org_slug: Fly.OrgSlug });const data = new Volume({ name: "data", region: "iad", size_gb: 1 });const guest = new MachineGuest({ cpu_kind: "shared", cpus: 1, memory_mb: 256 });
const web = new Machine({ name: "web", region: "iad", config: new MachineConfig({ image: "flyio/hellofly:latest", guest, mounts: [new MachineMount({ volume: "data", path: "/data" })], }),});const worker = new Machine({ name: "worker", region: "iad", config: new MachineConfig({ image: "flyio/hellofly:latest", guest }) });
export { app, data, web, worker };The serializer stamps managed-by: chant into each machine’s config.metadata
on its own, as the ownership marker the owned-only prune reads back. flyApply
creates the volume before the machine that mounts it (the volume is POSTed
first), and FLY011 checks at build time that the
mount names a declared volume.
The op
Section titled “The op”ops/fly.op.ts is Build → Apply only. It leaves the emulator running, so
state persists between runs:
import { Op, phase, build } from "@intentius/chant/op";import { flyApplyStep } from "@intentius/chant-lexicon-fly";
export default Op({ name: "fly-reconcile", overview: "Reconcile the plan against a running mudflaps (create / update / prune)", taskQueue: "fly", phases: [ phase("Build", [build(".", { script: "build:fly" })]), phase("Apply", [flyApplyStep("dist/fly.json", { endpoint: "http://localhost:4280", prune: true })]), ],});Run it
Section titled “Run it”Boot mudflaps once, then run the op:
docker run -d --rm -p 4280:4280 --name mudflaps ghcr.io/intentius/mudflaps:0.4.1
cd examples/fly-reconcilenpm install
chant run fly-reconcileThe first run creates everything:
created: app/fly-reconcile-democreated: volume/fly-reconcile-demo/datacreated: machine/fly-reconcile-demo/webcreated: machine/fly-reconcile-demo/workerWatch it reconcile
Section titled “Watch it reconcile”The point is what re-running does. State lives on the platform, not in a file, so
flyApply reads it back each time.
Re-apply is a no-op. Run it again with no source change, and nothing is touched:
unchanged: app/fly-reconcile-demonoop: volume/fly-reconcile-demo/datanoop: machine/fly-reconcile-demo/webnoop: machine/fly-reconcile-demo/workerA config change updates in place. Edit web’s config.image in
src/infra.ts and re-run. flyApply leases the machine, updates it, and waits
the new instance to started. worker, unchanged, still no-ops:
updated: machine/fly-reconcile-demo/webnoop: machine/fly-reconcile-demo/workerRemoving a machine prunes it. Delete the worker machine from src/infra.ts
and re-run. Prune is on, so the machine chant no longer declares is destroyed,
since it carries the managed-by: chant marker:
noop: machine/fly-reconcile-demo/webpruned: fly-reconcile-demo/workerForeign machines survive. Create a machine directly in mudflaps, without the ownership marker, then re-run:
curl -X POST http://localhost:4280/v1/apps/fly-reconcile-demo/machines \ -H 'content-type: application/json' \ -d '{"name":"legacy","region":"iad","config":{"image":"flyio/hellofly:latest"}}'The owned-only prune leaves legacy alone. chant deletes only what it owns, so
it never touches a resource created outside chant.
Stop the emulator when done:
docker rm -f mudflapsRun it against real Fly
Section titled “Run it against real Fly”Drop the endpoint override and give it a token. The op and the code stay the same:
phase("Apply", [flyApplyStep("dist/fly.json", { prune: true })]), // no endpointexport FLY_API_TOKEN=...chant run fly-reconcileWith no endpoint, flyApply falls through to FLY_FLAPS_BASE_URL (or Fly’s
https://api.machines.dev) and authenticates with the token. The reconcile is
the same against real Fly; only the endpoint changes.