@better-compute/provider-hetzner
v0.3.0
Published
Hetzner providers for better-compute: Hetzner Cloud servers as VPS instances and offers.
Maintainers
Readme
@better-compute/provider-hetzner
Hetzner providers for better-compute: Hetzner Cloud servers as VPS instances, and Hetzner Robot dedicated servers bootstrapped into bcd nodes.
Install
npm install @better-compute/provider-hetznerIt is not part of the better-compute umbrella: install it next to better-compute (or the @better-compute/* packages it works with). It depends on @better-compute/core and @better-compute/vps.
Usage
import { betterCompute } from 'better-compute';
import { vps } from 'better-compute/vps';
import { hetznerCloud } from '@better-compute/provider-hetzner';
betterCompute({ /* database, iam, secret, baseURL, … */ modules: [vps({ providers: [hetznerCloud] })] });
// An account is data, validated by the provider's manifest. The token is a SecretRef, never a value.
await client.providerAccounts.create({
tenantId,
provider: 'hetzner-cloud',
name: 'main',
config: { token: 'vault://kv/hcloud', location: 'fsn1' },
});hetznerCloud serves Hetzner Cloud servers as the instances of class: 'provider' pools. hetznerRobotProvider({ ssh }) turns dedicated servers into bcd nodes of metal-host or vps-host pools through vps's providerAccounts.bootstrap.
Hetzner Cloud
- Config is
{ token: SecretRef, location: 'fsn1' | 'nbg1' | 'hel1' | 'ash' | 'hil' | 'sin' }. The token is resolved through the host's secret resolver on each request. - Idempotent create. The Hetzner API has no idempotency keys. Instead, each server carries
better-compute.io/idempotency-key=<instance id>andbetter-compute.io/managed=truelabels.createfirst looks the key up. If a concurrent create wins the unique server name, it returns the winner. - Server names are hostname-safe (
inst_01…→inst-01…, at most 63 characters). - Offers come from
/server_typesand/datacenters, both paginated.- They include gross hourly prices in EUR for the account's location, or the queried
region. availablecomes from that location's datacenters.- Deprecated types are skipped.
- GPU and metal queries return nothing, since Hetzner Cloud has neither.
- They include gross hourly prices in EUR for the account's location, or the queried
- Power:
on→poweron,off→poweroff,graceful-off→shutdown,reset/cycle→reset,graceful-restart→reboot. - Other operations:
reinstall→rebuild.listyields only the servers this provider manages. - Errors
- A 404 is absent:
getanswersundefined, anddeleteandpowerthrowNOT_FOUND. - A 429 is
RATE_LIMITED, withretryAfterMstaken fromRateLimit-Reset. - Everything else is
PROVIDER_ERROR, withdetails: { status, code }holding Hetzner's error code.
- A 404 is absent:
- Transport:
hetznerCloudProvider({ fetch, endpoint })swaps the transport. The tests use an in-memory emulator of the API, and the same hook can record or replay real traffic.
The package passes providerConformanceCases() from @better-compute/vps.
Hetzner Robot
hetznerRobotProvider({ ssh }) (provides: ['metal']) turns the dedicated servers an account already has into metal-host nodes that run bcd ("platform-owned provider machines"). hetznerRobot is the same provider without an SSH executor: offers, claims, power and rescue, but not identify or bootstrap.
Config:
{ user, password: SecretRef, rescueKeyFingerprint, rescueKey: SecretRef, image }. The fingerprint names an SSH key stored in Robot, andrescueKeyis its private key.imagedefaults toUbuntu-2404-noble-amd64-base.tar.gz.Offers are the servers with an empty name that are not cancelled.
- Specs come from
robotProducts(GEX44, GEX130, AX41-NVMe, AX52, AX102, EX44) oroptions.products, because Robot's API reports only product names. An unknown product reports 1 vCPU and 1 GiB until you configure it. availablemeans Robot says the server isready.
- Specs come from
createclaims instead of ordering. It names the serverbc-<idempotency key>, and a retry finds the same claim. A server someone has named for another use is never offered or claimed (CONFLICT).deletereleases the claim (clears the name and deactivates rescue).- Ordering and cancelling servers are contractual actions, left to people.
gettreats unclaimed servers as absent.
Power maps to Robot resets:
reset/cycle→hw,graceful-restart→sw,graceful-off/on→power,off→power_long. Robot cannot report power state.rescueactivates Linux rescue with the account's key, then hardware-resets into it.identify(machine)boots rescue (unless it already answers) and reads/sys/class/dmi/id/product_uuidover SSH, so the bootstrap token can be bound to the machine (expect.productUuid; theprovisionedattestor refuses unbound tokens). It leaves the server in rescue forbootstrap.bootstrap(machine, { joinToken, bcdUrl, sha256, controlPlaneUrl, pins })runs in these steps:- It reuses a rescue system that already answers, else activates rescue and waits for SSH, through the injected
SshExecutor(one hardware reset for identify + bootstrap). - It writes the single-use
provisionedjoin token over SSH stdin into aumask 077file. - It runs
bootstrapScript, which callsinstallimagewith software RAID 1 over the NVMe disks, else the SATA disks. - The post-install script, built in rescue, places the token at
/etc/bcd/join-token(0600). It downloads bcd over https and checks its sha256. It installsbcd-join.service, a oneshot that runsrobotJoinArgv(controlPlaneUrl, pins)while the token file exists, andbcd.service(bcd node), then shreds itself. - The server reboots and rescue is deactivated. On first boot
bcd join --server … --attestor provisioned --serving-ca-pin … --join-signing-pin … --join-encryption-pin …reads the gateway from the control plane's well-known document, refuses it if its pins differ from the ones on the command line, joins, writes/etc/bcd/config.tomland removes the token. Re-runs on a joined node succeed without a token. bcd's own tests parse these command lines with bcd's flags, so the two sides cannot drift.
The token never appears in user data, in argv or in the script sent over SSH.
- It reuses a rescue system that already answers, else activates rescue and waits for SSH, through the injected
bcd is not released as a binary yet, so bcdUrl and sha256 must point at a bcd build you host. The Robot provider's tests run against an emulated Robot API, and can also syntax-check the bootstrap script with a POSIX shell.
