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.
1. Prerequisites
Section titled “1. Prerequisites”- 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
.debto 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).
2. Point the CLI at the provider mirror
Section titled “2. Point the CLI at the provider mirror”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):
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:
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/excludepair 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.ioandregistry.opentofu.org, so the short sourceselfhosted-cloud/shcresolves for either tool. - Prefer a persistent config? Put the same
provider_installationblock in~/.terraformrc(Terraform) or~/.tofurc(OpenTofu) instead of theTF_CLI_CONFIG_FILEexport.
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.
3. Pick an example
Section titled “3. Pick an example”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:
cp -r terraform/examples/hetzner-shc my-clustercd my-clusterMost examples ship a terraform.tfvars.example. Copy it and fill it in:
cp terraform.tfvars.example terraform.tfvars$EDITOR terraform.tfvars # cluster_name, deb_url, vault_pw, shc_host, dns_zone, ...4. Set credentials
Section titled “4. Set credentials”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.tfvarsand state. - SHC bootstrap —
bootstrap.vault_passwordseals the cluster’s system vault (required); thesshblock (key material or aprivate_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.
5. Init, plan, apply
Section titled “5. Init, plan, apply”tofu init # installs the shc provider from the mirror + the cloud providertofu plan # review: infra + bootstrap (shc_cluster/shc_node) + topologytofu applyOne apply provisions all three layers with no out-of-band steps:
- Infra — instances, network, firewalls (plain cloud resources).
- Bootstrap — the provider installs the
.deband runsshc initon node 1 andshc node joinon the rest, over its SSH transport (no bearer token needed — see below). - Topology / platform — DNS, ACME, LB attach, mesh, and any app deployments the root declares.
When it finishes, read the outputs:
tofu output # cluster_endpoint, node names/IPs, etc.tofu output -raw ssh_private_key # if the module minted the key6. 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:
| Terraform | CLI equivalent | On which node |
|---|---|---|
shc_node with bootstrap = true | shc init | nodes[0] (the first node of the alphabetically-first node_groups key) |
every other shc_node | shc node join | each remaining node |
shc_mesh | shc cluster mesh enable | cluster-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.
7. Day-2 and teardown
Section titled “7. Day-2 and teardown”- Upgrade the cluster by bumping
bootstrap.package.version(or thedeb_url): the provider reinstalls the.deband 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 withprotected = true, flip it tofalseand apply once first — protected resources refuse destruction.
Troubleshooting
Section titled “Troubleshooting”tofu initcan’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.jsonshould return JSON. Confirm the URL under Deploy → Pages and that Pages is publicly readable.Missing vault_password—bootstrap.vault_passwordis required; the SSH bootstrap engine hard-errors when it is empty.X400998SSHSUD— the SSH user lacks passwordless sudo. The modules’ defaultadminuser has NOPASSWD sudo; a custom non-root user needs the same, or useuser = "root".- Diagnostics with an
X…code — that’s a daemon error code; the provider surfaces it verbatim so you can grep it.