SHC Terraform provider
selfhosted-cloud/shc is a custom Terraform / OpenTofu provider that
configures a Self-Hosted Cloud cluster through its daemon — bootstrapping
nodes, installing apps, wiring DNS, and managing tenants, users,
connections, and scoped config. It has ~12 resources and 4 data sources.
This page covers declaring the provider, pointing the CLI at the network
mirror, the provider configuration block, the two transports, and a
reference section for every resource and data source. Commands use tofu;
terraform works identically.
- Declaring the provider
- Installing from the network mirror
- Provider configuration
- Transports and the per-resource
clusterblock - Resources —
shc_cluster·shc_node·shc_mesh·shc_tenant·shc_user·shc_connection·shc_connection_grant·shc_repo·shc_deployment·shc_stack_ports·shc_config·shc_dns_record - Data sources —
shc_cluster_status·shc_catalog_app·shc_enrollment_token·shc_cloudinit
Declaring the provider
Section titled “Declaring the provider”Name the provider with its explicit source host (it matches the
provider’s Address, and OpenTofu users need it spelled out since their
default registry host differs):
terraform { required_providers { shc = { source = "selfhosted-cloud/shc" version = "~> 0.3" } }}Installing from the network mirror
Section titled “Installing from the network mirror”The provider is not on the public Terraform / OpenTofu registry. It is
published to a GitLab Pages provider network mirror (static JSON + zips
over HTTPS). Point the CLI at it with a provider_installation block in
~/.terraformrc (Terraform), ~/.tofurc (OpenTofu), or a repo-local file
selected with export TF_CLI_CONFIG_FILE="$PWD/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"] }}<PAGES-URL>is the project’s GitLab Pages root (e.g.https://<your-group>-<project>.gitlab.io/). Confirm it under Deploy → Pages; the mirror must be publicly readable.- The
include/excludepair scopes the mirror to only the shc provider — without it,tofu inittries the mirror for hcloud/aws/google too and fails. The mirror serves the provider under both registry hostnames, so the short source resolves for either tool.
Dev-only per-commit channel. For iterating against an in-flight main
commit (not a release), the mirror also serves a per-commit-sha provider
type shc-<8charsha>, whose single version is always 0.0.0:
shc = { source = "selfhosted-cloud/shc-a1b2c3d4", version = "0.0.0" }These pins are ephemeral — a sha ages out of the snapshot retention
window and its mirror directory disappears, bricking tofu init. Use a real
release pin for anything durable. This channel needs a broader include
(registry.terraform.io/selfhosted-cloud/*).
Integrity. The mirror omits the protocol-optional hashes field, so
tofu/terraform compute the h1: hash on first install and record it in
your .terraform.lock.hcl — trust-on-first-use, with tamper detection from
then on via the lock file. (Network mirrors have no signature channel at
all; only the public registries serve signatures.)
Provider configuration
Section titled “Provider configuration”The provider block is optional — it pins a day-2 target. When you
bootstrap a cluster in the same plan, you can omit it and set a per-resource
cluster { ... } block instead, so a cluster created in the same graph is
addressable.
provider "shc" { endpoint = "https://node-1:8000" # or "unix:///run/shc/shc.sock"
# Preferred for real runs: OAuth2 client_credentials (self-minting). client_id = "terraform" client_secret = var.shc_client_secret
ca_pem = file("cluster-ca.pem")}| Argument | Type | Required | Sensitive | Env fallback | Description |
|---|---|---|---|---|---|
endpoint | string | no | no | SHC_ENDPOINT | Daemon API endpoint (https://host:8000 or unix:///run/shc/shc.sock). Optional when every resource carries its own cluster block. |
token | string | no | yes | SHC_TOKEN | Static bearer token for TCP endpoints (ignored for unix://). Mutually exclusive with client_id/client_secret. |
client_id | string | no | no | SHC_CLIENT_ID | OAuth2 client_credentials client id (mint with shc auth create-service-client). Self-mints + re-grants its bearer. Mutually exclusive with token. |
client_secret | string | no | yes | SHC_CLIENT_SECRET | OAuth2 client_credentials secret. Mutually exclusive with token. |
token_url | string | no | no | — | OAuth2 token endpoint; defaults to https://<endpoint-host>/auth/realms/master/protocol/openid-connect/token. Only with client_id/client_secret. |
ca_pem | string | no | no | SHC_CA_PEM | PEM bundle to trust for TLS against the cluster’s internal CA (the ca_pem output of the bootstrap node). |
insecure | bool | no | no | — | Skip TLS certificate verification (bootstrap/testing only; prefer ca_pem). |
ssh | block | no | — | — | Default SSH transport for every platform resource — see below. |
registry_auth | block | no | — | — | Pre-authenticate the node container runtime during SSH bootstrap (avoids registry rate limits). |
ssh block (provider default transport)
Section titled “ssh block (provider default transport)”Selects the SSH transport as the default for every platform resource that
does not carry its own cluster.ssh override: resources run shc ... CLI
verbs over SSH on a cluster node — no bearer token needed.
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
hosts | list(object) | yes | no | EVERY node the provider may run shc ... on, most-likely-leader first. Tried in order until one connects and is willing to serve the operation. Feed it module.<cluster>.cluster_ssh_hosts. |
user | string | no | no | Default SSH user for every entry (default root; non-root users need NOPASSWD sudo — every command runs sudo -n). |
private_key | string | no | yes | Default SSH private key (PEM) for every entry. |
port | int | no | no | Default SSH port for every entry (default 22). |
host_key | string | no | no | Default known_hosts-format host-key pin; omitted, the engine trusts-on-first-use. |
Each hosts entry is { host, user, private_key, port, host_key } — only
host is required; the rest fall back to the block-level defaults above, so
the common case is a bare list of addresses. Per-entry overrides exist for
estates whose nodes genuinely differ (mixed images mid-migration, a
heterogeneous multi-cloud cluster, one node still keying root).
There is no scalar ssh.host. It existed briefly as single-node sugar
for a one-entry list and was removed: one pinned address cannot fail over
when a rebuild reassigns public IPs or when the node it names is no longer
the raft leader, and both of those cost a live estate an outage on
2026-07-27. A genuinely single-node cluster writes:
ssh = { hosts = [{ host = "198.51.100.10" }] user = "admin" private_key = var.ssh_key}When no entry can serve an operation, the error names every host with its
own reason (auth vs timeout vs refused vs not-leader) — never wrap the list
in try(..., "127.0.0.1"), which dials the machine running terraform.
registry_auth block
Section titled “registry_auth block”| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
server | string | no | no | Registry hostname (default Docker Hub). |
username | string | no | no | Registry username. |
password | string | no | yes | Registry password or access token (crosses the wire as SSH-session stdin). |
Authentication modes
Section titled “Authentication modes”Exactly one of:
client_id+client_secret(preferred) — the provider self-mints its bearer and transparently re-grants it (proactively at ~80% of the token lifespan, and once on a 401), so applies longer than one token lifetime never die401partway. Mint the pair withshc auth create-service-client terraformon the cluster.token(static) — cannot re-grant mid-run; a long apply outlives its lifespan and fails.ssh— no bearer at all; runs the CLI over SSH on the bootstrap node.- A
unix://endpoint needs none (peer-credential auth).
token and the client-credentials pair conflict loudly at configure time.
Both have env fallbacks, so a stray environment variable can be the surprise
second half of that conflict.
Transports and the per-resource cluster block
Section titled “Transports and the per-resource cluster block”The platform resources (shc_node’s token mint, shc_connection,
shc_config, shc_mesh, shc_dns_record, shc_stack_ports, shc_user,
shc_tenant, shc_deployment, …) each support two transports:
- HTTP API — a bearer token authenticates calls to the daemon (needs
endpoint/token, orclient_id/client_secret). - SSH — when a
cluster { ssh { ... } }block is set (or inherited from the provider’s ownssh), the resource runs the equivalentshc <verb>CLI command over SSH on whichever node inhostsanswers and can serve the operation, authenticated by its local unix-socket peer credential — no bearer token.
Every resource and data source accepts an optional per-resource cluster
override block so a cluster created in the same plan is addressable:
resource "shc_config" "platform" { cluster = { ssh = { hosts = module.shc.cluster_ssh_hosts, user = "admin", private_key = var.ssh_key } } # ...}Resource cluster block:
| Argument | Type | Required | Description |
|---|---|---|---|
endpoint | string | no | Daemon API endpoint. Required unless ssh is set. |
token | string | no (sensitive) | Bearer token for TCP endpoints. |
ca_pem | string | no | PEM bundle to trust for TLS. |
insecure | bool | no | Skip TLS verification (bootstrap/testing only). |
ssh | block | no | Run this resource’s ops over SSH — same fields as the provider ssh block. When both HTTP and ssh are set, ssh wins. |
(The cluster block carries a static token only — the
client-credentials mode is a provider-block feature. On a data source
cluster block, endpoint is Required.)
Resources
Section titled “Resources”Cluster and node lifecycle
Section titled “Cluster and node lifecycle”shc_cluster
Section titled “shc_cluster”The cluster’s genesis config — a pure, inputs-only declarative record. It
owns no node and does no SSH: every node is an shc_node, and the one with
bootstrap = true runs shc init and emits the cluster creds. This resource
is the single home for cluster-wide genesis parameters a module wires onto
that bootstrap node. It emits nothing load-bearing (only a stable id),
which keeps the dependency graph acyclic.
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
package | block | yes | no | Cluster-wide SHC package default (deb_url, version) — every shc_node inherits it. |
nebula | bool | no | no | Genesis nebula overlay (shc init --nebula), applied on the bootstrap node. |
fabric | string | no | no | Genesis cross-node data plane (shc init --fabric): swarm (docker overlay), shc (routed WireGuard — excludes the swarm runtime), or none. Unset omits the flag entirely and preserves today’s behaviour. Cluster-scoped and consumed once at genesis. |
fabric_cidr | string | no | no | fabric = "shc" only: the supernet the fabric carves per-node /24s from (--fabric-cidr; daemon default 10.84.0.0/16). Setting it with any other fabric is refused at plan time. |
host_domain | string | no | no | The genesis --host domain / basePath seed. |
modules | map(block) | no | no | Structured system-app selection + per-app overrides (see below). |
vault_password | string | no | yes | Vault password sealing the always-local system vault. A change is refused in place (the seal is immutable). |
acme | block | no | no | Named ACME providers + the cluster default (see below). |
id | string | computed | no | Stable id — host_domain when set, else "shc-cluster". Never an endpoint. |
package block: deb_url (string, required) — URL of the .deb to
install; version (string, optional) — a change (propagated to nodes)
triggers their in-place rolling upgrade.
modules block (map keyed by system-app name; the key SET is the install
selection — unset installs the daemon’s default set, {} installs none, a
populated map installs exactly those apps): host (string), base_path
(string), vars (map(string)) — seeded under system.apps.<name>.*.
Init-time only.
acme block: providers (map keyed by operator label, each with
directory, email, eab_kid, eab_hmac(sensitive)), default (string),
email (string). A blank directory is Let’s Encrypt production.
resource "shc_cluster" "main" { package = { deb_url = var.deb_url } host_domain = "cloud.example.com" vault_password = var.vault_pw acme = { providers = { prod = { email = "ops@example.com" } }, default = "prod" }}shc_node
Section titled “shc_node”A cluster node. The model is symmetric — every node is an shc_node.
Exactly one sets bootstrap = true and runs shc init (minting the CA +
genesis and emitting the creds); every other runs shc node join. Exactly
one bootstrap mechanism applies per node: adopt = true (poll a
self-init/self-joined node over HTTP) or an ssh block (install the .deb
and run init/join over SSH); adopt wins when both are set.
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
cluster | block | no | — | Per-resource cluster/transport override (join reference). |
bootstrap | bool | no | no | true ⇒ run shc init; false (default) ⇒ shc node join. Forces replacement. |
host | string | yes | no | Node address (host/IP, or full https://…/unix:// URL). SSH target and bootstrap endpoint base. |
package | block | no | no | Package to install before SSH init/join (deb_url, version); ssh-engine only. A change does an in-place rolling upgrade. |
ssh | block | no | — | Selects the SSH engine (install + init/join over SSH). Fields: user, private_key(sensitive), port, host_key. |
adopt | bool | no | no | Adopt a self-initialized/self-joined node: poll it healthy over HTTP. |
name | string | yes | no | Node name (--name) — the membership identity polled for. Forces replacement. |
nic | string | no | no | The one internal-traffic interface (--nic). Forces replacement. |
runtime | string | no | no | Container runtime (--runtime). Forces replacement. |
public_address | string | no | no | Public ingress address (--public-address); managed DNS reads it. Repaired in place. |
private_ip | string | no | no | Expected private/plane IP; the SSH node-prepared gate waits for an interface to hold it. |
nebula | bool | no | no | Bootstrap-only. shc init --nebula. |
fabric | string | no | no | Bootstrap-only. shc init --fabric (swarm|shc|none). CLUSTER-scoped: recorded once at genesis and read back by every joiner — shc node join has no --fabric, so setting it on a join node is a plan-time error. fabric = "shc" excludes runtime = "swarm". |
fabric_cidr | string | no | no | Bootstrap-only, fabric = "shc" only. shc init --fabric-cidr (daemon default 10.84.0.0/16). Must be IPv4 with a prefix shorter than /24. |
host_domain | string | no | no | Bootstrap-only. The --host seed. |
modules | map(block) | no | no | Bootstrap-only. System-app selection + overrides (same shape as shc_cluster.modules). |
vault_password | string | no | yes | Bootstrap-only. Seals the system vault; in-place change refused. |
acme | block | no | no | Bootstrap-only. Named ACME providers + default (same shape as shc_cluster.acme). |
Exports (computed): node_id, healthy, id (= node name). Bootstrap
node only (null on a join node): endpoint, ca_pem, admin_token
(sensitive), join_token (sensitive), ready.
admin_tokencannot be freshly minted by either mechanism —shc initproduces no HTTP-usable credential. It echoes the provider block’s token (null if none). This is exactly the gap the SSH transport works around: the platform resources run without a token by talking to the bootstrap node’s local admin socket over SSH.
resource "shc_node" "bootstrap" { bootstrap = true name = "node-1" host = "198.51.100.10" runtime = "compose" package = { deb_url = var.deb_url } ssh = { user = "admin", private_key = var.ssh_key } host_domain = "cloud.example.com" vault_password = var.vault_pw}shc_mesh
Section titled “shc_mesh”The cluster-wide Nebula overlay toggle (shc cluster mesh enable/disable).
A singleton per cluster (id = "mesh") — one resource covers the whole
cluster’s membership, re-synced on every apply (enable is additive +
idempotent over current membership, so it is safe to depends_on after every
shc_node join). Removing the resource disables the mesh.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
enabled | bool | yes | Whether the overlay is enabled cluster-wide (Create/Update call ClusterMeshEnable/Disable). |
Exports: nodes (computed list of {node_id, node_name, mesh_ip, installed, running, error}), id (constant mesh).
resource "shc_mesh" "main" { enabled = true depends_on = [shc_node.workers]}Tenancy and access
Section titled “Tenancy and access”shc_tenant
Section titled “shc_tenant”An SHC tenant and its realm. An optional vault_connection routes that
tenant’s app secrets to an external vault-capable connection; omit it for the
local vault (system-tier secrets always stay local).
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
name | string | yes | Tenant name. Forces replacement. |
vault_connection | string | no | Name of a vault-capable shc_connection backing this tenant’s secrets; omit for local vault. |
Exports: active (bool), provisioning_warning (string), id (= name).
resource "shc_tenant" "acme" { name = "acme"}shc_user
Section titled “shc_user”An SSO user in a tenant’s Keycloak realm plus a managed (non-authoritative)
set of role grants. Requires the system Keycloak (user_management
capability).
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
cluster | block | no | — | Per-resource cluster/transport override. |
username | string | yes | no | Username in the tenant realm (Keycloak lowercases it). Forces replacement. |
tenant | string | no (default default) | no | Tenant whose realm owns the user. Forces replacement. |
email | string | no | no | User email (no update verb — a change forces replacement). |
password | string | yes | yes | The user’s password; write-only. A change runs shc user reset-password in place. |
grants | set(string) | no | no | Roles to grant (e.g. tenant, tenant:manage, stack:<name>, stack:<name>:operate); only listed roles are managed. |
Exports: id (= <username>/<tenant>).
resource "shc_user" "alice" { username = "alice" tenant = shc_tenant.acme.name password = var.alice_pw grants = ["tenant", "stack:gitea:operate"]}Connections and grants
Section titled “Connections and grants”shc_connection
Section titled “shc_connection”A typed connection to an external backend (route53, hetzner, cloudflare,
pfsense, s3, aws, gcp, …). Its type declares which capability providers it
lights up; creating it escrows the credentials to the vault and probes each
capability, exposed as computed booleans.
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
cluster | block | no | — | Per-resource cluster/transport override. |
name | string | yes | no | Connection name. Forces replacement. |
type | string | yes | no | Connection type (e.g. route53, hetzner, cloudflare, pfsense, s3, aws, gcp). |
tenant | string | no (default master) | no | Owning tenant. Forces replacement. |
credentials | map(string) | no | yes | Credential fields per type (write-only). An endpoint key is passed as the backend endpoint rather than a secret. |
zones | list(string) | no | no | For DNS connections: the zones this connection manages. |
Exports (capability booleans + probed zones): dns, local_dns,
proxy, ca_signer, restic, volume, dns_zones (list), id
(= tenant/name).
resource "shc_connection" "dns" { name = "cloudflare" type = "cloudflare" credentials = { token = var.cf_token } zones = ["example.com"]}shc_connection_grant
Section titled “shc_connection_grant”Authorises a connection to mint a capability’s scoped sub-credential for a
grantee tenant (shc connection grant). A deployment binding is refused
without a matching grant (X403910CONGRT).
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
connection_name | string | yes | The connection being shared (bare connection is a reserved root name). Forces replacement. |
capability | string | yes | Capability the grantee may mint (e.g. aws-iam, s3-bucket). Forces replacement. |
grantee | string | yes | Tenant authorised to mint. Forces replacement. |
tenant | string | no (default master) | The connection’s owning tenant. Forces replacement. |
boundary_policy | string | no | Optional permissions boundary as inline JSON; the one in-place mutable attribute (re-apply re-scopes). |
Exports: id (= tenant/connection/capability/grantee).
resource "shc_connection_grant" "aws_iam" { connection_name = shc_connection.aws.name capability = "aws-iam" grantee = shc_tenant.acme.name boundary_policy = file("./boundary.json")}Apps and repositories
Section titled “Apps and repositories”shc_repo
Section titled “shc_repo”A remote SHC app repository (git or https). Repos are remote-only so any node
can materialize apps on demand; a file:// URL is rejected at plan time.
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
cluster | block | no | — | Per-resource cluster/transport override. |
name | string | yes | no | Repository name. Forces replacement. |
url | string | yes | no | Remote repo URL (https://… or git+…). Forces replacement. |
token | string | no | yes | Access token for a private repo (write-only). Forces replacement. |
tenant | string | no | no | Owning tenant. Forces replacement. |
environment | string | no | no | Owning environment. Forces replacement. |
priority | int | no (computed) | no | Resolution priority (higher wins). |
sync_interval | string | no | no | Auto-sync interval (Go duration, e.g. 1h); omit to disable. |
Exports: app_count (int), synced_at, created_at, updated_at
(RFC3339 strings), id (= name).
resource "shc_repo" "shc" { name = "shc" url = "https://<PAGES-URL>/apps/index.yaml"}shc_deployment
Section titled “shc_deployment”One installed app instance (install / update) — the canonical way to deploy
an app declaratively. App source is structured: repo (a repo name;
absent = default catalog) plus the bare app name (no positional
repo/app). Schema version 1.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
tenant | string | no (default default) | Owning tenant. Forces replacement. |
repo | string | no | Catalog repo name to resolve app from; absent = default resolution. Forces replacement. |
app | string | yes | Bare catalog app name (never slash-qualified). Forces replacement. |
stack | string | no (computed) | Stack name; defaults to the app base name. Forces replacement. |
environment | string | no (default default) | Environment. Forces replacement. |
vars | map(string) | no | App variables; sent in full each apply. |
preset | string | no | Resource preset (e.g. small). |
disk | string | no | Volume/disk sizing. |
storage | string | no | Storage provider for volumes; install-time only. Forces replacement. |
acme_provider | string | no | Named ACME provider issuing public certs; blank = cluster default. Changeable in place — the daemon re-issues under the new CA without a recreate. |
runtime | string | no | Target runtime: compose, swarm, or podman; install-time only. Forces replacement. |
scale | map(int) | no | Per-service swarm replica counts; only effective under runtime = "swarm". |
public | bool | no (default false) | --public: expose to the internet. |
gated | bool | no (default false) | --gated: put Keycloak SSO in front. |
integrate | map(block) | no | Provider bindings keyed by plug (see below). |
connections | map(block) | no | Connection bindings keyed by the app’s declared binding name (see below). |
ports | map(block) | no | L4 port mappings keyed by "external[/udp]" (see below). |
hosts | map(block) | no | L7 HTTP routes keyed by "hostname[/path]" (see below). |
node | string | no (computed) | Install-time placement pin; not changeable in place. |
ttl | string | no | Lease duration (Go duration, e.g. 72h); re-applying the same value renews. Refused on protected environments. |
wait_healthy | bool | no (default true) | Block Create/Update on deployment health. |
protected | bool | no (default false) | Destroy guard (X409001DEPROT). |
Nested value shapes:
ports(key"external[/udp]"):service(string),container(int),connection(string — proxy-capable connection for LB exposure; absent = node publish).hosts(key"hostname[/path]"):service(string),port(int). There is notarget— the daemon infers the DNS endpoint.integrate(key = plug name):stackXORconnection, plus optionalenvironment(only withstack).connections(key = the app’s declared binding name):connection(string, required) — needs a matchingshc_connection_grant.
Exports: status, healthy (bool), node, lease_expires_at (RFC3339;
empty when ttl unset), id (= tenant/environment/stack).
resource "shc_deployment" "gitea" { repo = shc_repo.shc.name app = "gitea" public = true hosts = { "git.example.com" = { service = "web", port = 3000 } } ports = { "22" = { service = "web", container = 22 } }}A deployment with
ttlset never converges to an empty plan — every apply re-stamps the lease (like GitLab’sauto_stop_in). That is by design.vars/ports/hosts/integrateare authoritative-from-config: the provider sends the full desired sets plus explicit removals each apply, but removing aconnectionskey is a documented no-op until connection-detach lands.
shc_stack_ports
Section titled “shc_stack_ports”Manages only the L4 port mappings on an already-existing stack (never
installs or uninstalls). The named stack must already exist (e.g. the
system Traefik stack every cluster ships at init). This is how the core
module’s “lb” topology attaches a connection-backed LB exposure to the system
Traefik declaratively.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
tenant | string | no (default default) | Owning tenant. Forces replacement. |
environment | string | no (default default) | Environment. Forces replacement. |
stack | string | yes | The existing stack to attach mappings to. Forces replacement. |
ports | map(block) | no | Port mappings keyed by "external[/udp]" (same shape as shc_deployment.ports); dropping a key issues --port '<spec>!' — a trailing ! marks a removal. |
Exports: id (= tenant/environment/stack).
resource "shc_stack_ports" "traefik" { stack = "traefik" ports = { "80" = { connection = shc_connection.edge.name } "443" = { connection = shc_connection.edge.name } }}Config and DNS
Section titled “Config and DNS”shc_config
Section titled “shc_config”Scoped SHC configuration keys — cluster-wide (no tenant) or at a tenant’s
scope. Home of role defaults like dns.provider / backup.provider and
dns.public_address. Accepts exactly one of two mutually exclusive forms:
single-key (key + value) or a nested yaml document flattened to dotted
paths. Schema version 1.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
key | string | no | Config key path (e.g. dns.provider); single-key form, set with value. Forces replacement. Mutually exclusive with yaml. |
value | string | no | Value for the single-key form (stored verbatim as string). Required alongside key. |
yaml | string | no | Nested YAML document flattened to dotted paths; every leaf must be scalar. Mutually exclusive with key/value. |
tenant | string | no | Tenant to scope key(s) to; omit for cluster-wide (global) scope. Forces replacement. |
Exports: id (= scope/tenant/key, or scope/tenant/* for the yaml
form).
resource "shc_config" "platform" { yaml = <<-EOF dns: provider: cloudflare public_address: ${local.bootstrap_ip} acme: email: ops@example.com EOF}shc_dns_record
Section titled “shc_dns_record”A managed DNS record for a host (/api/dns/record). The daemon
auto-classifies target (IPv4 → A, IPv6 → AAAA, else CNAME). No tenant
attribute — this surface is platform-level.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Per-resource cluster/transport override. |
hostname | string | yes | The record’s hostname (identity key). Forces replacement. |
target | string | yes | Record target: IPv4 (A), IPv6 (AAAA), or hostname (CNAME). Upserts in place. |
ttl | int | no (computed) | Record TTL in seconds; omit for the provider default. |
proxied | bool | no (default false) | Serve through the provider proxy (e.g. Cloudflare orange-cloud). |
dns_provider | string | no | DNS provider (shc_connection name) override — named dns_provider, not provider (reserved). Forces replacement. |
Exports: type (A/AAAA/CNAME), zone_id, record_id, id
(= hostname).
resource "shc_dns_record" "status" { hostname = "status.example.com" target = module.shc.bootstrap_node.public_ipv4 dns_provider = shc_connection.dns.name}Data sources
Section titled “Data sources”shc_cluster_status
Section titled “shc_cluster_status”Reports the cluster’s membership and quorum status (shc cluster status).
Input: the optional cluster override block only.
Exports (computed): cluster_name, total_nodes, healthy_nodes,
degraded_nodes, unreachable_nodes, voters, required_voters,
leader_id, leader_name, quorum_healthy, background_work_node,
expected_voters, raft_voters, voter_deficit, id.
data "shc_cluster_status" "this" {}output "quorum" { value = data.shc_cluster_status.this.quorum_healthy }shc_catalog_app
Section titled “shc_catalog_app”Looks up one app in the cluster’s resolved catalog. name is the bare app
name; the optional repo names the repo it must resolve from (slash-qualified
names are rejected).
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Cluster override. |
name | string | yes | The bare catalog app name. |
repo | string | no | Repo name the app must resolve from; absent = default resolution. |
Exports: version, category, description, homepage, icon,
source, tags (list), parent_app, vapp (bool), repo_id, repo_name,
id.
shc_enrollment_token
Section titled “shc_enrollment_token”Mints a short-lived LAN enrollment token — the credential shc node join
consumes. Minted fresh on every read, sensitive, consume within the same
apply.
| Argument | Type | Required | Description |
|---|---|---|---|
cluster | block | no | Cluster override. |
ttl | string | no | Requested lifetime (Go duration, e.g. 15m); omit for the daemon default. |
Exports: token (sensitive), expires_at, id.
shc_cloudinit
Section titled “shc_cloudinit”Renders cloud-init user_data that self-bootstraps an SHC node — installs the
.deb, then runs shc init (role cluster) or shc node join <token> (role
node). The render is client-side (the daemon has no render route); feed the
output to a compute resource’s user_data (the adopt bootstrap path).
| Argument | Type | Required | Sensitive | Description |
|---|---|---|---|---|
cluster | block | no | — | Cluster override. |
role | string | yes | no | cluster (bootstrap via shc init) or node (join via shc node join). |
package | block | no | no | Package to install (deb_url required, version optional). |
nic | string | no | no | Internal-traffic interface (--nic). |
runtime | string | no | no | Container runtime (--runtime). |
name | string | no | no | Node name (--name). |
nebula | bool | no | no | role cluster only: bring up the overlay at init. |
fabric | string | no | no | role cluster only: shc init --fabric (swarm|shc|none). Setting it with role node is refused — shc node join has no --fabric, so it could only be dropped silently. Validated identically to shc_node.fabric, at plan time. |
fabric_cidr | string | no | no | role cluster only, fabric = "shc" only: --fabric-cidr (daemon default 10.84.0.0/16). |
host_domain | string | no | no | role cluster only: the --host seed. |
public_address | string | no | no | Node’s public ingress address (--public-address). |
modules | map(block) | no | no | role cluster only: system-app selection + overrides. |
vault_password | string | no | yes | role cluster only: embedded verbatim in the output — use an encrypted state backend. |
token | string | no | yes | role node only: enrollment token to join with. Omitted = mint fresh on every read. |
ttl | string | no | no | role node only: TTL when minting a token. |
Exports: rendered (sensitive — the cloud-init user_data),
token_expires_at, id (= <role>-cloudinit).
data "shc_cloudinit" "bootstrap" { role = "cluster" package = { deb_url = var.deb_url } host_domain = "cloud.example.com" vault_password = var.vault_pw}# feed data.shc_cloudinit.bootstrap.rendered to the instance's user_dataCoded errors
Section titled “Coded errors”Daemon errors carry an X<HTTP><SEQ><MNEMONIC> code (see the
error code reference). The provider surfaces the code
verbatim and leading in diagnostic detail, so operators can grep by code.