Skip to content
SHC Docs

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.

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"
}
}
}

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/exclude pair scopes the mirror to only the shc provider — without it, tofu init tries 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.)

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")
}
ArgumentTypeRequiredSensitiveEnv fallbackDescription
endpointstringnonoSHC_ENDPOINTDaemon API endpoint (https://host:8000 or unix:///run/shc/shc.sock). Optional when every resource carries its own cluster block.
tokenstringnoyesSHC_TOKENStatic bearer token for TCP endpoints (ignored for unix://). Mutually exclusive with client_id/client_secret.
client_idstringnonoSHC_CLIENT_IDOAuth2 client_credentials client id (mint with shc auth create-service-client). Self-mints + re-grants its bearer. Mutually exclusive with token.
client_secretstringnoyesSHC_CLIENT_SECRETOAuth2 client_credentials secret. Mutually exclusive with token.
token_urlstringnonoOAuth2 token endpoint; defaults to https://<endpoint-host>/auth/realms/master/protocol/openid-connect/token. Only with client_id/client_secret.
ca_pemstringnonoSHC_CA_PEMPEM bundle to trust for TLS against the cluster’s internal CA (the ca_pem output of the bootstrap node).
insecureboolnonoSkip TLS certificate verification (bootstrap/testing only; prefer ca_pem).
sshblocknoDefault SSH transport for every platform resource — see below.
registry_authblocknoPre-authenticate the node container runtime during SSH bootstrap (avoids registry rate limits).

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.

ArgumentTypeRequiredSensitiveDescription
hostslist(object)yesnoEVERY 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.
userstringnonoDefault SSH user for every entry (default root; non-root users need NOPASSWD sudo — every command runs sudo -n).
private_keystringnoyesDefault SSH private key (PEM) for every entry.
portintnonoDefault SSH port for every entry (default 22).
host_keystringnonoDefault 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.

ArgumentTypeRequiredSensitiveDescription
serverstringnonoRegistry hostname (default Docker Hub).
usernamestringnonoRegistry username.
passwordstringnoyesRegistry password or access token (crosses the wire as SSH-session stdin).

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 die 401 partway. Mint the pair with shc auth create-service-client terraform on 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, or client_id/client_secret).
  • SSH — when a cluster { ssh { ... } } block is set (or inherited from the provider’s own ssh), the resource runs the equivalent shc <verb> CLI command over SSH on whichever node in hosts answers 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:

ArgumentTypeRequiredDescription
endpointstringnoDaemon API endpoint. Required unless ssh is set.
tokenstringno (sensitive)Bearer token for TCP endpoints.
ca_pemstringnoPEM bundle to trust for TLS.
insecureboolnoSkip TLS verification (bootstrap/testing only).
sshblocknoRun 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.)


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.

ArgumentTypeRequiredSensitiveDescription
packageblockyesnoCluster-wide SHC package default (deb_url, version) — every shc_node inherits it.
nebulaboolnonoGenesis nebula overlay (shc init --nebula), applied on the bootstrap node.
fabricstringnonoGenesis 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_cidrstringnonofabric = "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_domainstringnonoThe genesis --host domain / basePath seed.
modulesmap(block)nonoStructured system-app selection + per-app overrides (see below).
vault_passwordstringnoyesVault password sealing the always-local system vault. A change is refused in place (the seal is immutable).
acmeblocknonoNamed ACME providers + the cluster default (see below).
idstringcomputednoStable 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" }
}

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.

ArgumentTypeRequiredSensitiveDescription
clusterblocknoPer-resource cluster/transport override (join reference).
bootstrapboolnonotrue ⇒ run shc init; false (default) ⇒ shc node join. Forces replacement.
hoststringyesnoNode address (host/IP, or full https://…/unix:// URL). SSH target and bootstrap endpoint base.
packageblocknonoPackage to install before SSH init/join (deb_url, version); ssh-engine only. A change does an in-place rolling upgrade.
sshblocknoSelects the SSH engine (install + init/join over SSH). Fields: user, private_key(sensitive), port, host_key.
adoptboolnonoAdopt a self-initialized/self-joined node: poll it healthy over HTTP.
namestringyesnoNode name (--name) — the membership identity polled for. Forces replacement.
nicstringnonoThe one internal-traffic interface (--nic). Forces replacement.
runtimestringnonoContainer runtime (--runtime). Forces replacement.
public_addressstringnonoPublic ingress address (--public-address); managed DNS reads it. Repaired in place.
private_ipstringnonoExpected private/plane IP; the SSH node-prepared gate waits for an interface to hold it.
nebulaboolnonoBootstrap-only. shc init --nebula.
fabricstringnonoBootstrap-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_cidrstringnonoBootstrap-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_domainstringnonoBootstrap-only. The --host seed.
modulesmap(block)nonoBootstrap-only. System-app selection + overrides (same shape as shc_cluster.modules).
vault_passwordstringnoyesBootstrap-only. Seals the system vault; in-place change refused.
acmeblocknonoBootstrap-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_token cannot be freshly minted by either mechanism — shc init produces 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
}

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.

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
enabledboolyesWhether 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]
}

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).

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
namestringyesTenant name. Forces replacement.
vault_connectionstringnoName 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"
}

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).

ArgumentTypeRequiredSensitiveDescription
clusterblocknoPer-resource cluster/transport override.
usernamestringyesnoUsername in the tenant realm (Keycloak lowercases it). Forces replacement.
tenantstringno (default default)noTenant whose realm owns the user. Forces replacement.
emailstringnonoUser email (no update verb — a change forces replacement).
passwordstringyesyesThe user’s password; write-only. A change runs shc user reset-password in place.
grantsset(string)nonoRoles 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"]
}

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.

ArgumentTypeRequiredSensitiveDescription
clusterblocknoPer-resource cluster/transport override.
namestringyesnoConnection name. Forces replacement.
typestringyesnoConnection type (e.g. route53, hetzner, cloudflare, pfsense, s3, aws, gcp).
tenantstringno (default master)noOwning tenant. Forces replacement.
credentialsmap(string)noyesCredential fields per type (write-only). An endpoint key is passed as the backend endpoint rather than a secret.
zoneslist(string)nonoFor 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"]
}

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).

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
connection_namestringyesThe connection being shared (bare connection is a reserved root name). Forces replacement.
capabilitystringyesCapability the grantee may mint (e.g. aws-iam, s3-bucket). Forces replacement.
granteestringyesTenant authorised to mint. Forces replacement.
tenantstringno (default master)The connection’s owning tenant. Forces replacement.
boundary_policystringnoOptional 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")
}

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.

ArgumentTypeRequiredSensitiveDescription
clusterblocknoPer-resource cluster/transport override.
namestringyesnoRepository name. Forces replacement.
urlstringyesnoRemote repo URL (https://… or git+…). Forces replacement.
tokenstringnoyesAccess token for a private repo (write-only). Forces replacement.
tenantstringnonoOwning tenant. Forces replacement.
environmentstringnonoOwning environment. Forces replacement.
priorityintno (computed)noResolution priority (higher wins).
sync_intervalstringnonoAuto-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"
}

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.

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
tenantstringno (default default)Owning tenant. Forces replacement.
repostringnoCatalog repo name to resolve app from; absent = default resolution. Forces replacement.
appstringyesBare catalog app name (never slash-qualified). Forces replacement.
stackstringno (computed)Stack name; defaults to the app base name. Forces replacement.
environmentstringno (default default)Environment. Forces replacement.
varsmap(string)noApp variables; sent in full each apply.
presetstringnoResource preset (e.g. small).
diskstringnoVolume/disk sizing.
storagestringnoStorage provider for volumes; install-time only. Forces replacement.
acme_providerstringnoNamed ACME provider issuing public certs; blank = cluster default. Changeable in place — the daemon re-issues under the new CA without a recreate.
runtimestringnoTarget runtime: compose, swarm, or podman; install-time only. Forces replacement.
scalemap(int)noPer-service swarm replica counts; only effective under runtime = "swarm".
publicboolno (default false)--public: expose to the internet.
gatedboolno (default false)--gated: put Keycloak SSO in front.
integratemap(block)noProvider bindings keyed by plug (see below).
connectionsmap(block)noConnection bindings keyed by the app’s declared binding name (see below).
portsmap(block)noL4 port mappings keyed by "external[/udp]" (see below).
hostsmap(block)noL7 HTTP routes keyed by "hostname[/path]" (see below).
nodestringno (computed)Install-time placement pin; not changeable in place.
ttlstringnoLease duration (Go duration, e.g. 72h); re-applying the same value renews. Refused on protected environments.
wait_healthyboolno (default true)Block Create/Update on deployment health.
protectedboolno (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 no target — the daemon infers the DNS endpoint.
  • integrate (key = plug name): stack XOR connection, plus optional environment (only with stack).
  • connections (key = the app’s declared binding name): connection (string, required) — needs a matching shc_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 ttl set never converges to an empty plan — every apply re-stamps the lease (like GitLab’s auto_stop_in). That is by design. vars/ports/hosts/integrate are authoritative-from-config: the provider sends the full desired sets plus explicit removals each apply, but removing a connections key is a documented no-op until connection-detach lands.

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.

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
tenantstringno (default default)Owning tenant. Forces replacement.
environmentstringno (default default)Environment. Forces replacement.
stackstringyesThe existing stack to attach mappings to. Forces replacement.
portsmap(block)noPort 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 }
}
}

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.

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
keystringnoConfig key path (e.g. dns.provider); single-key form, set with value. Forces replacement. Mutually exclusive with yaml.
valuestringnoValue for the single-key form (stored verbatim as string). Required alongside key.
yamlstringnoNested YAML document flattened to dotted paths; every leaf must be scalar. Mutually exclusive with key/value.
tenantstringnoTenant 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
}

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.

ArgumentTypeRequiredDescription
clusterblocknoPer-resource cluster/transport override.
hostnamestringyesThe record’s hostname (identity key). Forces replacement.
targetstringyesRecord target: IPv4 (A), IPv6 (AAAA), or hostname (CNAME). Upserts in place.
ttlintno (computed)Record TTL in seconds; omit for the provider default.
proxiedboolno (default false)Serve through the provider proxy (e.g. Cloudflare orange-cloud).
dns_providerstringnoDNS 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
}

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 }

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).

ArgumentTypeRequiredDescription
clusterblocknoCluster override.
namestringyesThe bare catalog app name.
repostringnoRepo 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.

Mints a short-lived LAN enrollment token — the credential shc node join consumes. Minted fresh on every read, sensitive, consume within the same apply.

ArgumentTypeRequiredDescription
clusterblocknoCluster override.
ttlstringnoRequested lifetime (Go duration, e.g. 15m); omit for the daemon default.

Exports: token (sensitive), expires_at, id.

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).

ArgumentTypeRequiredSensitiveDescription
clusterblocknoCluster override.
rolestringyesnocluster (bootstrap via shc init) or node (join via shc node join).
packageblocknonoPackage to install (deb_url required, version optional).
nicstringnonoInternal-traffic interface (--nic).
runtimestringnonoContainer runtime (--runtime).
namestringnonoNode name (--name).
nebulaboolnonorole cluster only: bring up the overlay at init.
fabricstringnonorole cluster only: shc init --fabric (swarm|shc|none). Setting it with role node is refusedshc node join has no --fabric, so it could only be dropped silently. Validated identically to shc_node.fabric, at plan time.
fabric_cidrstringnonorole cluster only, fabric = "shc" only: --fabric-cidr (daemon default 10.84.0.0/16).
host_domainstringnonorole cluster only: the --host seed.
public_addressstringnonoNode’s public ingress address (--public-address).
modulesmap(block)nonorole cluster only: system-app selection + overrides.
vault_passwordstringnoyesrole cluster only: embedded verbatim in the output — use an encrypted state backend.
tokenstringnoyesrole node only: enrollment token to join with. Omitted = mint fresh on every read.
ttlstringnonorole 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_data

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.