Skip to content
SHC Docs

Clouds

SHC ships a first-class Terraform module per cloud (terraform/modules/shc/<cloud>) plus a copy-pasteable example root (terraform/examples/<cloud>-shc) that builds the substrate and wires the platform (cloud connection + DNS + Let’s Encrypt) through terraform-provider-shc. Each cloud also has a live smoke lane under tests/clouds/<cloud> driven from the repo root with make test/<cloud>.

Every example and smoke lane defaults to Cloudflare-parent DNS — you point it at one Cloudflare-hosted domain you own and SHC writes its records straight into that zone. This is the recommended way to try or test any cloud uniformly: one domain, one token, works everywhere. Each cloud’s native DNS (Route53, Cloud DNS, Azure DNS, …) is documented in its page and selectable with SHC_TEST_DNS_PROVIDER / var.dns_provider, but native DNS usually needs an NS-delegated subzone or a provider-hosted apex — more moving parts than a single Cloudflare zone.

CloudDNSBlock storage / LBHeadline first-live gotcha
AWSNative Route53 on the one aws connection (example/tests default to Cloudflare-parent so a tester uses their own single zone)aws-ebs-volume (awsebs) — AZ-scoped, needs availability_zone+volume_type credsRoute53 TXT needs surrounding-quote presentation form; no NLB yet (front Traefik on the bootstrap EIP, HTTP-01); EBS attach needs node-name resolve + waitAvailable
Azurenative Azure DNS, NS-delegated out of a parent (estate default) — or cloudflare-parent to point at a zone you ownazure-disk managed disks (StandardSSD/Premium_LRS) — UNVERIFIED-LIVE; LB azure-lbfresh-subscription core quota: DSv5 ships quota 0 → pivot to Standard_D2s_v4 (v4 has the default 10-core quota); admin/root are reserved admin_usernames
DigitalOceanNative DO managed DNS (delegated from a Cloudflare parent); Cloudflare-parent fallback for testersdovolume (block) + dolb (LB)cloud-init user_data must be pure ASCII — DO’s ConfigDrive double-encodes a UTF-8 em-dash and silently voids the whole document; the region-default VPC also refuses to delete
Exoscalecloudflare-parent (native Exoscale DNS is a paid account add-on)none — node-local docker volumes (no cloud block backend)plane NIC eth1 boots DOWN — the stock template configures only eth0; the module ships a 70-shc-extra-nics.network DHCP fallback
Hostingercloudflare-parent, always — Hostinger DNS is never wired (the module creates no DNS resources)none of either. No managed LB and no block volumes exist on the account; apps ride node-local docker disk and ingress is per-nodeBilling, not a bug: no hourly capacity. Every node is a subscription with a ONE-MONTH minimum that destroy cancels without a refund, so a 10-minute smoke test costs $9.99+. Also: no private network (multi-node = nebula WAN overlay only), no tags/labels (hostname is the only attribution), no cloud-init (post-install shell script), and no firewall resource in the terraform provider — nodes are unfiltered public boxes
GCP (GCE)native Cloud DNS, NS-delegated (bundled free); cloudflare-mix for a single test domaingcepd (zonal persistent disk, mirrors awsebs)GCE has no cloud-init — the startup-script must stop+mask apt-daily or the dpkg lock kills node prep (compose plugin never installs)
OpenStack (OVH US)Cloudflare-parent — records written straight into the parent zone (OVH has no Designate; no NS delegation, no openstack platform connection)none — apps use local docker disk (Cinder deferred; no SHC volume provider)Per-subnet dnsmasq REFUSES recursion when dns_nameservers is empty (resolv.conf looks healthy; every query answers REFUSED/rcode-5) → module defaults public resolvers ["1.1.1.1","8.8.8.8"]
Scalewaycloudflare-parent (native scaleway DNS unusable unless the apex is Scaleway-hosted — 403 domain not found)none (gap — edge rides node flexible IPs, no LB either)security group MUST set external_rules = true or every terraform refresh wipes inbound rules and seals SSH; also b_ssd discontinued → sbs_volume, plane NIC is ens6 not ens5
VultrCloudflare-parent (Vultr DNS refuses subdomain zones)vultr-volume (LB: vultr-lb)Stock image ships UFW ENABLED — ICMP passes so ping lies, TCP dropped → cluster joins fail X500023JNFAIL; disable UFW in prep

Each live smoke lane (make test/<cloud>) provisions a real, billable cluster, wires DNS + Let’s Encrypt, installs a public whoami app, asserts DNS resolution + a valid LE certificate, then tears the cluster down. They all obey one shared env-var contract, so the same handful of variables drives every cloud — you only add that cloud’s credentials.

Costs money. Every lane spins up real infrastructure. They auto-tear-down at the end; keep a cluster alive for debugging with the teardown toggle below, then reclaim it with make test/<cloud>/clean.

The easy path — one Cloudflare domain for every cloud

Section titled “The easy path — one Cloudflare domain for every cloud”

Own one domain on Cloudflare, mint a Zone:DNS:Edit API token scoped to it, and you can test every cloud with the same two variables:

Terminal window
export SHC_TEST_DOMAIN=example.com # a Cloudflare-hosted zone you own
export SHC_TEST_CLOUDFLARE_TOKEN=cf-xxxxxxxx # Zone:DNS:Edit on that zone

SHC writes its records (<prefix>-cloud.example.com, <prefix>-whoami.example.com) directly into that flat zone by longest-suffix match — no per-cloud subzone, no NS delegation. Then add just the target cloud’s credentials and run, e.g.:

Terminal window
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
make test/aws

Shared variables (honored by every make test/<cloud>)

Section titled “Shared variables (honored by every make test/<cloud>)”
VariableDefaultPurpose
SHC_TEST_DOMAINcryptobulls.vip (checked into tests/lib/tf-harness.sh)The base domain SHC writes hosts under. Not a secret, so it is defaulted in the harness rather than a token store — override it with your own zone. On the easy path this is a single Cloudflare-hosted zone you own. tests/hetzner is the exception: it needs a Hetzner-DNS zone and uses DNS_ZONE / SHC_TEST_HETZNER_ZONE instead.
SHC_TEST_DNS_PROVIDERcloudflareWhich SHC DNS connection to wire. cloudflare (recommended) uses one small connection pointed at your zone; a cloud-native value (aws/gcp/azure/digitalocean/scaleway) rides that cloud’s own DNS — see the per-cloud page.
SHC_TEST_CLOUDFLARE_TOKEN— (required when SHC_TEST_DNS_PROVIDER=cloudflare)Cloudflare Zone:DNS:Edit API token for SHC_TEST_DOMAIN. Env only — never a tfvars file. Alias: SHC_TEST_DNS_TOKEN (provider-neutral name; use it when the DNS provider isn’t Cloudflare). The legacy CLOUDFLARE_API_TOKEN is still honored.
SHC_TEST_PREFIXshc-<cloud>-*Naming prefix for cloud resources and the DNS host labels (cluster_name). Keep it DNS-label-safe (lowercase alnum + hyphen).
SHC_DEB_URL— (optional)Public URL of a prebuilt SHC .deb. When set, the lane installs that instead of building one from the working tree.
SHC_DEB— (optional)Path to a prebuilt .deb to install. Required when running lanes in parallel: the self-build path runs goreleaser --clean, which deletes dist/ under any sibling lane. Build once, export SHC_DEB, then fan out; an unguarded concurrent build now refuses rather than wiping its peer.

Teardown toggle: set SHC_NO_TEARDOWN=1 (aws, azure, digitalocean, exoscale, gcp) or SHC_TEST_NO_TEARDOWN=1 (hostinger, openstack, scaleway, vultr) to leave the cluster running for post-mortem inspection. Reclaim it later with make test/<cloud>/clean.

On Hostinger a kept cluster is not an idling meter — it is a live subscription that auto-renews at a full month. Leave it up only deliberately, and reclaim it the same day.

Add the target cloud’s native credentials to the shared variables above. These are read by the underlying cloud provider / SDK, never written to state:

CloudCredential env vars
AWSAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (optional AWS_DEFAULT_REGION)
AzureARM_SUBSCRIPTION_ID, ARM_TENANT_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET
DigitalOceanDIGITALOCEAN_TOKEN
ExoscaleEXOSCALE_API_KEY, EXOSCALE_API_SECRET
GCPGOOGLE_APPLICATION_CREDENTIALS (path to a service-account JSON) + a project (SHC_TEST_* / var.project)
HostingerHOSTINGER_API_TOKENplus SHC_TEST_HOSTINGER_ACCEPT_BILLING=1, which the lane requires before it will spend (see the cost note in its page)
OpenStackOS_AUTH_URL, OS_APPLICATION_CREDENTIAL_ID, OS_APPLICATION_CREDENTIAL_SECRET (or the classic OS_USERNAME/OS_PASSWORD/OS_PROJECT_*)
ScalewaySCW_ACCESS_KEY, SCW_SECRET_KEY, SCW_DEFAULT_PROJECT_ID (use a dedicated project)
VultrVULTR_API_KEY

Each lane also accepts per-cloud sizing/placement overrides (SHC_TEST_REGION, SHC_TEST_ZONE, SHC_TEST_NODE_COUNT, instance-type vars, …); see tests/clouds/<cloud>/README.md for that cloud’s full knob list and the exact assertions it runs.

Uniform across every cloud (mirrors the test/hetzner tier):

TargetEffect
make test/<cloud>Build/stage the deb, provision, verify (DNS + LE cert), auto-teardown
make test/<cloud>/cleanProduct-side unwind → tofu destroy (reclaim a kept-alive cluster)
make test/<cloud>/statustofu output of the provisioned scenario (or a hint when absent)
make test/<cloud>/<target>Forward any sub-make target (test, destroy, status, build-deb)