Skip to content
SHC Docs

Getting started with Terraform / OpenTofu

A task-oriented quickstart: install the shc provider from the network mirror, pick an example, set credentials, and tofu init / plan / apply a cluster. Commands use tofu (OpenTofu); terraform works identically.

See also The shc provider and The module library for the full reference.

  • OpenTofu ≥ 1.6 (or Terraform ≥ 1.6). The provider and modules declare required_version = ">= 1.6.0".
  • A cloud account for your chosen substrate (Hetzner, AWS, etc.), with a credential the cloud’s own Terraform provider reads (usually an environment variable such as HCLOUD_TOKEN, AWS_PROFILE, GOOGLE_CREDENTIALS).
  • A URL for the SHC .deb to install on each node (bootstrap.package.deb_url).
  • An SSH key the provider uses to install and bootstrap each node (or let the module mint one with create_ssh_key = true).

The shc provider is not on the public registry — install it from the GitLab Pages network mirror. Write a CLI config file (many examples ship one as registry.tfrc):

registry.tfrc
provider_installation {
network_mirror {
url = "https://<PAGES-URL>/"
include = ["registry.terraform.io/selfhosted-cloud/shc",
"registry.opentofu.org/selfhosted-cloud/shc"]
}
direct {
exclude = ["registry.terraform.io/selfhosted-cloud/shc",
"registry.opentofu.org/selfhosted-cloud/shc"]
}
}

Then point the CLI at it for the session:

Terminal window
export TF_CLI_CONFIG_FILE="$PWD/registry.tfrc"

Notes:

  • <PAGES-URL> is the project’s GitLab Pages root (e.g. https://<your-group>-<project>.gitlab.io/). Confirm it under Deploy → Pages.
  • The include/exclude pair scopes the mirror to only the shc provider so your cloud providers (hcloud, aws, google, …) still install direct from the public registry.
  • The mirror serves the provider under BOTH registry.terraform.io and registry.opentofu.org, so the short source selfhosted-cloud/shc resolves for either tool.
  • Prefer a persistent config? Put the same provider_installation block in ~/.terraformrc (Terraform) or ~/.tofurc (OpenTofu) instead of the TF_CLI_CONFIG_FILE export.

Your root’s required_providers names the provider with its explicit source host:

terraform {
required_providers {
shc = {
source = "selfhosted-cloud/shc"
version = "~> 0.3"
}
}
}

See The provider → Installing from the network mirror for the dev-only per-commit channel and the mirror-integrity tradeoff.

Copy the example that matches your substrate from terraform/examples/ (see the Examples table for the full list). For a first cluster, a <cloud>-shc root (a single module "shc" call) is the simplest:

Terminal window
cp -r terraform/examples/hetzner-shc my-cluster
cd my-cluster

Most examples ship a terraform.tfvars.example. Copy it and fill it in:

Terminal window
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # cluster_name, deb_url, vault_pw, shc_host, dns_zone, ...

Two credential sets are involved:

  • The cloud provider — export the environment variable the cloud’s provider reads, e.g. export HCLOUD_TOKEN=... (Hetzner), export AWS_PROFILE=... (AWS). Keep it out of .tfvars and state.
  • SHC bootstrapbootstrap.vault_password seals the cluster’s system vault (required); the ssh block (key material or a private_key_path) is how the provider reaches each node. Some examples mint the SSH key and a random vault password in-config (tls_private_key + random_password).

Sensitive material lands in state. The vault password, SSH private key, and any connection credentials are written to Terraform state (the provider marks them sensitive, but they are still there). Use an encrypted state backend.

Terminal window
tofu init # installs the shc provider from the mirror + the cloud provider
tofu plan # review: infra + bootstrap (shc_cluster/shc_node) + topology
tofu apply

One apply provisions all three layers with no out-of-band steps:

  1. Infra — instances, network, firewalls (plain cloud resources).
  2. Bootstrap — the provider installs the .deb and runs shc init on node 1 and shc node join on the rest, over its SSH transport (no bearer token needed — see below).
  3. Topology / platform — DNS, ACME, LB attach, mesh, and any app deployments the root declares.

When it finishes, read the outputs:

Terminal window
tofu output # cluster_endpoint, node names/IPs, etc.
tofu output -raw ssh_private_key # if the module minted the key

6. How the provisioned nodes relate to shc init / shc node join

Section titled “6. How the provisioned nodes relate to shc init / shc node join”

The module maps directly onto the CLI bootstrap verbs — every node is an shc_node, and exactly one has bootstrap = true:

TerraformCLI equivalentOn which node
shc_node with bootstrap = trueshc initnodes[0] (the first node of the alphabetically-first node_groups key)
every other shc_nodeshc node joineach remaining node
shc_meshshc cluster mesh enablecluster-wide (WAN topology)

The provider runs those verbs over an SSH transport: it installs the .deb and runs shc ... ON the bootstrap node, which authenticates for free against its own local admin socket (peer-credential auth). No bearer token, no admin_token, no hand-run driver.sh. (An alternative adopt path lets a node self-init from cloud-init user_data rendered by data.shc_cloudinit, with Terraform polling it healthy over HTTP — see The provider.)

After apply, drive the live cluster with the CLI as usual — SSH to the bootstrap node and run shc status, shc install <app>, shc node ls, etc. — or manage more resources declaratively by adding shc_deployment, shc_connection, and shc_config blocks to the same root.

  • Upgrade the cluster by bumping bootstrap.package.version (or the deb_url): the provider reinstalls the .deb and re-checks health one node at a time (raft quorum stays intact).
  • Add apps with shc_deployment (see The provider → shc_deployment).
  • Destroy with tofu destroy. If the substrate module was applied with protected = true, flip it to false and apply once first — protected resources refuse destruction.
  • tofu init can’t find the provider — the mirror isn’t reachable or <PAGES-URL> is wrong. curl https://<PAGES-URL>/registry.terraform.io/selfhosted-cloud/shc/index.json should return JSON. Confirm the URL under Deploy → Pages and that Pages is publicly readable.
  • Missing vault_passwordbootstrap.vault_password is required; the SSH bootstrap engine hard-errors when it is empty.
  • X400998SSHSUD — the SSH user lacks passwordless sudo. The modules’ default admin user has NOPASSWD sudo; a custom non-root user needs the same, or use user = "root".
  • Diagnostics with an X… code — that’s a daemon error code; the provider surfaces it verbatim so you can grep it.