Skip to content
SHC Docs

Cross-cloud convergence plan (2026-07-13 live campaign)

Five estates went fully green in one campaign (azure, openstack/OVH, vultr, exoscale, scaleway — each: 4-node cluster, apps, LE cert). Every first-live failure was fixed at one layer, but four fixes were authored independently per cloud. This plan unifies them so the same class is solved ONCE — the anti-over-fit gate before the fixes calcify as per-cloud idioms.

Class 1 — plane/secondary NIC bring-up (exoscale, scaleway; vultr immune)

Section titled “Class 1 — plane/secondary NIC bring-up (exoscale, scaleway; vultr immune)”

Findings:

  • exoscale: template netplans ONLY eth0; plane NIC (eth1) sits unmanaged. Fix authored: per-cloud networkd unit Match=eth1.
  • scaleway: hotplugged private NIC is ens6 (module derived ens5); image auto-DHCPs it via netplan regen — only the NAME fed to shc init --nic was wrong. Fix authored: derivation fix + a GENERIC fallback unit (70-shc-extra-nics.network: DHCP any unclaimed en*, RouteMetric high).
  • vultr: DataSourceVultr configures the VPC NIC natively (no fix needed).

UNIFICATION: adopt scaleway’s generic fallback shape as the SHARED pattern in EVERY module’s cloud-init (including clouds that don’t need it today):

/etc/systemd/network/70-shc-extra-nics.network
[Match] Name=en* eth* (unclaimed only -- see precedence note)
[Network] DHCP=ipv4
[DHCPv4] UseDNS=false UseNTP=false UseHostname=false RouteMetric=200

Safety: networkd applies the FIRST matching .network by lexical filename; cloud images’ own units (10-netplan-*, 50-cloud-init) win, so the fallback only claims NICs nothing else manages. Exoscale’s bespoke Match=eth1 unit is REPLACED by this generic one. The per-cloud nic_name derivation stays (the CLI needs the name) but becomes advisory: also add a root-level nic_name override to every module that lacks it.

Class 2 — image firewall posture (vultr)

Section titled “Class 2 — image firewall posture (vultr)”

Vultr’s Debian image ships UFW ENABLED (icmp accepted -> ping lies; TCP dropped -> joins time out). No other campaign image does, but the class is “image default posture varies”. UNIFICATION: the shared cloud-init prep in EVERY module gains an idempotent, harmless line:

systemctl disable --now ufw 2>/dev/null || true

Rationale: the module’s cloud-firewall/SG model IS the firewall; a host firewall the image sneaks in is always a conflict. One line, all modules, no per-cloud conditional.

Class 3 — DNS: cloudflare-parent-mix is the DEFAULT estate shape

Section titled “Class 3 — DNS: cloudflare-parent-mix is the DEFAULT estate shape”

5 of 8 estates converged on writing records straight into the Cloudflare parent zone (hetzner original + openstack/vultr/exoscale/scaleway pivots). NS-delegation of a subdomain only survives where zone hosting is free with the account (azure, gcp, do). The per-cloud reasons differ (OVH: no Designate; exoscale: paid add-on; scaleway: domain must be hosted there; vultr: refuses subdomain zones) but the SHAPE is one: a cloudflare shc_connection + dns.provider=cloudflare + hostnames under the parent.

UNIFICATION: document cloudflare-parent-mix as the default in the estate template / ops README; NS-delegation + native cloud DNS becomes the opt-in variant. The native scaleway/exoscale DNS providers stay in the registry (built + unit-tested), live-verifiable when an account gains the product.

Class 4 — readiness gate: plane-NIC probe (provider)

Section titled “Class 4 — readiness gate: plane-NIC probe (provider)”

X400992NICNOF surfaced at shc init, AFTER waitNodePreparedOverSSH passed (the gate only probes docker). The provider knows each node’s expected private_ip; extend the gate probe with:

ip -4 addr show | grep -qF "<private_ip>" (when private_ip non-null)

so a dead plane NIC fails fast at substrate-prep with a clear message on EVERY cloud, instead of surfacing per-cloud at init. (terraform/provider ssh_engine.go nodePreparedProbe — one generic change.)

  1. Wait for the podman-wiring change to land (it edits module variables.tf validators; classes 1-2 edit templates/*.tftpl + main.tf — different files, same dirs; serialize to keep diffs reviewable).
  2. Implement classes 1+2 across all 12 modules + class 4 in the provider; one commit each.
  3. Republish; re-pin estates on the converged version during the teardown+retest wave (which then validates the CONVERGED shape from empty, not the hand-fixed nodes).
  4. Class 3 is docs + estate-template only.