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.
Node-fleet management for the Internet Computer
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.
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.
Declare the change in proposals.hcl, referencing nodes and subnets from resources.hcl.
Alpage replays the exact payload against a local copy of the NNS. A payload that fails there stops here.
The proposal goes to live governance from your neuron, after an explicit typed confirmation.
The proposal id, submitter, payload hash, and timestamp land in state.json, committed.
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.
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 }
}
}
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.
$ 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 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.
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.
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.
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.
The CLI is alp. Five of the seven subcommands are strictly read-only; only
apply submits, and only import writes state without submitting.
alp applyalp planalp importalp listalp statusalp reconcilealp registry subnet
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.
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.
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",
]
}
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.
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.
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.
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.
inputs.alpage = {
url = "github:swiss-subnet/alpage?ref=refs/tags/v0.2.0";
inputs.nixpkgs.follows = "nixpkgs";
}; $ 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.
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.