Node-fleet management for the Internet Computer

Your fleet, as committed config.

Alpage manages Internet Computer nodes the way Terraform manages infrastructure: declare the desired state in version control, and the tool submits the NNS proposals that realise it. Every change is rehearsed against a local copy of the NNS before it can touch the live network.

Built and used by Swiss Subnet AG to run the Swiss subnet.

resources.hcl
subnet "swiss" {
  id    = "3zsyy-cnoqf-...-lqe"
  label = "Swiss Subnet"
}

node "zh1_01" {
  id       = "3wbrf-zokqb-...-nae"
  subnet   = subnet.swiss.id
  operator = node_operator.ssn.id
}

Early days. Alpage is a young project under active development. It submits real NNS proposals against real infrastructure, so treat it accordingly: the CLI surface, the config schema, and the state file format may still change between releases.

1

Edit config

Declare the change in proposals.hcl, referencing nodes and subnets from resources.hcl.

2

Dry-run

Alpage replays the exact payload against a local copy of the NNS. A payload that fails there stops here.

3

Submit

The proposal goes to live governance from your neuron, after an explicit typed confirmation.

4

Record

The proposal id, submitter, payload hash, and timestamp land in state.json, committed.

Declare the change

One proposal block per change, with a kind and a matching nested block. Node and subnet ids come from named resources, so a principal is written once and referenced by name everywhere else.

proposals.hcl
proposal "shrink-zh1" {
  kind    = "membership"
  title   = "Shrink Swiss subnet"
  summary = "Remove two nodes from the Swiss subnet."
  url     = "https://forum.dfinity.org/t/..."

  membership {
    subnet_id = subnet.swiss.id
    remove { id = node.zh1_01.id }
    remove { id = node.zh1_02.id }
  }
}

Apply it

Alpage fails fast on a wrong identity, refuses to resubmit a proposal already in state without --force, and writes state only after governance accepts the submission.

terminal
$ alp apply shrink-zh1 --identity key.pem --yes
checking neuron 12345 can submit ... ok
dry-run on PocketIC ............... ok
payload hash 9f2c1a...
type 'submit' to proceed: submit
submitted proposal 134221
state.json updated

Why it works this way

Declare, don't click

Fleet membership and replica versions live in committed HCL. Review a node removal as a diff in a pull request instead of transcribing principals into a proposal form.

Verified before it lands

Every apply first executes the exact payload against a local NNS running the real governance and registry canisters (via PocketIC). If it will not execute there, it never reaches the live network.

State you can audit

One state.json records the proposal id, submitter principal, neuron, host, payload hash, and timestamp per change. Who submitted what, when, and from which config.

Drift is a CI failure

reconcile diffs your declared resources against live on-chain state and exits nonzero, so a fleet that drifted out of band breaks the build instead of surprising you.

Commands

The CLI is alp. Five of the seven subcommands are strictly read-only; only apply submits, and only import writes state without submitting.

alp apply
Check the identity can submit, dry-run on a local NNS, submit to the live network, record state.
alp plan
Reconcile a proposal against live on-chain state before submitting. Read-only counterpart to apply's dry-run.
alp import
Adopt an already-submitted proposal into state without submitting it.
alp list
Show each declared proposal as in-sync, drifted, or not-submitted (config vs state).
alp status
Read recorded proposals back from governance for their real on-chain status and tally.
alp reconcile
Read-only diff of declared resources against live on-chain state. Exits nonzero on drift.
alp registry subnet
Query a subnet's current membership, emitted as a resources.hcl fragment.

Proposal kinds

Each kind maps to one NNS proposal payload. Adding a kind is one Action implementation plus a decode case; state, dry-run, submit, and drift detection are all kind-agnostic.

membership change_subnet_membership

Add or remove nodes on a subnet.

deploy_guestos deploy_guestos_to_all_subnet_nodes

Upgrade every node in a subnet to a replica version.

Every block and field is documented in the config reference, generated from the Go struct tags.

Built for Cloud Engines

A Cloud Engine is a subnet you configure and operate yourself: you pick the nodes, the providers, and the jurisdictions, and you change them over time. Adding nodes, removing them, and moving between operators or geographies is exactly the lifecycle Alpage exists to make routine, reviewable, and repeatable.

resources.hcl
subnet "engine" {
  id            = "..."
  label         = "Production engine"
  type          = "cloud_engine"
  cost_schedule = "free"

  # Declaring none asserts none: an admin
  # added on-chain shows up as drift.
  admins = [
    "ay4xt-...-cai",
  ]
}

Engine fields are first-class

Declare type, cost_schedule, and admins on the subnet and Alpage reconciles each against the live registry record, so console-side changes surface as drift instead of going unnoticed.

Invalid engines fail at plan time

The registry's own invariants are checked before submission: an engine must be on the free cost schedule, and admins are allowed only on an engine or a rented subnet, capped at ten. A declaration the registry would refuse errors locally, not as a failed proposal.

Membership as a reviewed diff

Rotating a provider out of an engine is a membership proposal generated from a config change, dry-run locally, and recorded with the id that executed it.

Getting Started

Alpage is not something you install once and run ad hoc: it is pinned by the repository that holds your config. Add alp as a flake input at a release tag, and every operator and CI run gets the identical binary that produced the state file.

your config repo's flake.nix
inputs.alpage = {
  url = "github:swiss-subnet/alpage?ref=refs/tags/v0.2.0";
  inputs.nixpkgs.follows = "nixpkgs";
};
terminal
$ nix develop            # alp on PATH
$ alp plan swiss-wave1   # against live state
$ alp reconcile          # drift check, CI gate
$ alp apply swiss-wave1 --identity hotkey.pem

Bump the pinned tag and run nix flake update alpage to adopt a new release. Because the version is stamped from the tag, alp version tells you exactly which binary submitted any recorded proposal.

Prefer a plain binary? Tagged releases carry prebuilt alp for linux and darwin on amd64 and arm64, with .sha256 sums, on the releases page.

To work on Alpage itself, the toolchain (Go, PocketIC, IC canisters) is pinned in its own flake: nix develop --command go test ./.... See CONTRIBUTING.md; contributions are accepted under the DCO.

Licensing

Running Alpage against your own fleet of up to 5 nodes is free, including in production, and evaluation is always free at any fleet size. Beyond 5 nodes, managing fleets for third parties, or offering a hosted service built on Alpage needs a commercial license. Each released version converts to Apache-2.0 four years after it is published.

Read the plain-language version or contact licensing@subnet.ch.