Skip to content

Apply from the laptop

nerthus-config apply is the one command that changes the estate's resources: it deletes a DNS record nobody declares, it turns a project's job logs private, it protects a branch. This runbook is the bootstrap way of running it — from the operator's own machine, with the two scoped tokens — and it is the only way there is until the site's worker takes over.

In steady state this is not the path. The estate has two (ruling 49): --from laptop at bootstrap, and --from site afterwards, where the site's worker instance applies on a merged change to Nerthus.Infra. The second is not built yet: --from site parses and refuses.

Before you start

You need three things, and the command refuses without each of them.

  • A machine that carries no role. --from laptop reads inventory/hosts.yaml and compares this machine's hostname and fully qualified name with every declared host. A match that carries any role is refused by name: a laptop is a client (ruling 31), and a host the fleet converges has a different writer.
  • The two scoped tokens in this instance's own store, under the names the inventory gives them: CLOUDFLARE_API_TOKEN and GITLAB_API_TOKEN. They are sops values that nerthus secrets set writes and nerthus-config reads; nothing prints them back. Mint the Cloudflare token says what the first one carries.
  • A clean checkout of Nerthus.Infra, on main, up to date. The command cuts a branch of its own and returns you to the one you were on; it commits nothing but state/.

The procedure

Read the plan first. It is the same plan the merge request shows and the same one the apply recomputes a minute later, so nothing is carried out that a person has not read:

cd ~/gitlab/margonem/nerthus/it/Nerthus.Infra
nerthus-config plan --against live .

Then the dry run, narrowed to what you mean to change. --only takes a kind, a kind and an id, or a prefix:

nerthus-config apply . --from laptop --only 'cloudflare_*' --dry-run

Then the apply itself. It asks before it acts; --yes answers, and --json never prompts at all:

nerthus-config apply . --from laptop --only 'cloudflare_*'

What happens, in order: a fresh plan against live, the four refusals below, the plan printed, your confirmation, then one provider call per row in the plan's own order — creates and updates first, deletes last in reverse. Every call carries one request id, printed with the run, so a log can be joined to it afterwards.

What it refuses, and why

Refusal The rule behind it
this machine is a declared host with a role ruling 49: a laptop is a client, and a host's writer is the site's worker
the declaration has findings a plan from a declaration validate refuses is a plan about an estate nobody declared
the plan carries anything unknown ruling 47: a plan with a hole in it is not one to act from; --only narrows to what a provider here can read
the plan carries a blocked row something other than apply brings such a resource into existence — a host somebody rents, an instance nerthus init makes

The first three are measured before any provider is asked at all.

What it leaves behind

state/ is what the tool last observed or made, and this command is one of its three writers (ruling 47). It writes the tree, commits it on a branch named apply/<utc>, and pushes it with GitLab's own merge_request.create push option — no API call, no second credential. You merge that merge request; the tool never pushes to main.

A remote that does not take push options still gets the branch: the push is retried without them and the answer says no merge request was opened. The estate has already been changed by then, and a record that refused to leave the machine would be the worse failure of the two.

When a row is refused mid-plan

The run stops at the first refusal and says so: how many rows were carried out, which one was refused and why, and which rows nobody attempted. What was done stays done — an estate is not a transaction — so the state tree it writes equals the prefix that really happened, and the next plan shows the remainder.

Read the refusal before re-running. A provider refuses for reasons that are usually about order: a tunnel whose DNS record still points at it is refused by name until the record goes first.

--from site, and when this page stops being the path

When it is built, the site's worker instance runs the same command with --from site: it applies on a merged change to Nerthus.Infra, through the job queue, with the tokens as ordinary vault credentials of that instance. This page stays as the bootstrap procedure and as what you do when the site is down.