Terraform¶
Provision HomeCloud account resources from Terraform or OpenTofu: queues, buckets, secrets, IAM, databases, Redis, functions, image repositories, domains, compute, SSH keys, and draft applications. The provider calls console.{apex}/api/v1 with a SigV1 Access Key (or a short-lived key from GitHub OIDC). It does not manage the homelab (K3s, Helm, Compose).
Architecture: ADR-049.
Provider repo: terraform-provider-homecloud (keep the terraform-provider-* name — HashiCorp Registry requires that prefix).
Listed on the Terraform Registry as homecloudlab/homecloud (v0.1.1). Run terraform init — no local build and no dev.tfrc for normal use. Workspaces already locked to v0.1.0: terraform init -upgrade (needed for homecloud configure / ~/.homecloud/credentials). Community providers show as self-signed; the key ID 4B8BCFED1A615BA9 is our GPG key. That is expected.
Create a key¶
Use a dedicated IAM user with console role developer or admin, then an Access Key bound to that user. See Access Keys. Never put the secret in .tf files.
Local laptop: homecloud configure once, then terraform apply. The provider reads the same %USERPROFILE%\.homecloud\credentials file as the CLI (profile / HC_PROFILE for other accounts).
CI: set HC_ACCESS_KEY_ID + HC_SECRET_ACCESS_KEY, or HC_ROLE_ARN (GitHub OIDC). If HC_ROLE_ARN is set, a leftover credentials file on the runner is ignored.
| Variable | Meaning |
|---|---|
HC_ACCESS_KEY_ID |
Access Key ID (overrides the credentials file) |
HC_SECRET_ACCESS_KEY |
Secret |
HC_PROFILE |
Named profile in the credentials file |
HC_APEX |
Platform apex (default holab.abrdns.com) |
HC_ACCOUNT_ID |
Optional account UUID (default: whoami) |
HC_ENDPOINT |
Optional console URL override (tests) |
IAM create/update/delete needs a console role of owner or admin. Function delete also needs owner/admin. Queue/bucket/secret Create/Delete/Get can use a Service Account key with the matching IAM actions (mq:CreateQueue, so:CreateBucket, secrets:CreateSecret, …). Unmapped SA console routes return 403 iam.management_sa_not_enabled.
GitHub OIDC (no long-lived key)¶
CI can mint a temporary Access Key from a GitHub Actions OIDC JWT. No HC_SECRET_ACCESS_KEY in repo secrets.
- Create an IAM role whose trust allows GitHub Actions (
Principal.Federated+Conditiononsubandaud). Attach managed policies such asMQAdmin/SOBucketAdmin/SecretsAdmin. - In GitHub Actions,
permissions: id-token: write. - Set
HC_ROLE_ARN(and optionallyHC_OIDC_AUDIENCE, defaulthttps://console.{apex}). The provider exchanges the JWT atPOST /api/v1/sts/assume-role-with-web-identityand uses the short-lived SigV1 credentials (includingX-Homecloud-Session-Token).
Assumed-role sessions use IAM authorize() on the same mapped routes as Service Account keys (queue / bucket / secret Create/Delete/Get). Unmapped console routes return 403 iam.management_role_not_enabled. IAM / MDB / functions / compute still need a User-bound key.
Trust example (replace account number, repo, and audience):
{
"Version": "2026-07-24",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:homecloud:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "https://console.holab.abrdns.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:HomeCloudLab/my-infra:*"
}
}
}]
}
Copy a workflow skeleton from examples/github-oidc. In Actions the provider reads ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN when HC_WEB_IDENTITY_TOKEN is unset.
| Variable | Meaning |
|---|---|
HC_ROLE_ARN |
IAM role ARN to assume |
HC_WEB_IDENTITY_TOKEN |
OIDC JWT (optional in GitHub Actions; the provider can fetch it) |
HC_OIDC_AUDIENCE |
JWT audience (default https://console.{apex}) |
HC_SESSION_TOKEN |
Already-exchanged STS session token (optional) |
Configure¶
terraform {
required_providers {
homecloud = {
source = "homecloudlab/homecloud"
}
}
}
provider "homecloud" {
apex = "holab.abrdns.com"
}
OpenTofu works the same (tofu apply).
Install¶
terraform {
required_providers {
homecloud = {
source = "homecloudlab/homecloud"
version = "~> 0.1"
}
}
}
Hack on the provider itself: copy dev.tfrc.example to dev.tfrc, set TF_CLI_CONFIG_FILE, and skip terraform init (overrides ignore the Registry). terraform apply then warns that development overrides are in effect.
P1 / P1b resources¶
data "homecloud_account" "this" {}
resource "homecloud_mq_queue" "jobs" {
name = "jobs"
}
resource "homecloud_so_bucket" "assets" {
name = "assets"
}
resource "homecloud_secret" "db" {
name = "database-url"
description = "Primary DB"
values = {
DATABASE_URL = var.database_url
}
}
Bucket schema is name only. Versioning / lifecycle / website stay in the console. Secret values are write-only (Terraform 1.11+) and never stored in state.
P2 IAM¶
IAM create/update/delete needs a User-bound Access Key with console role owner or admin (iam.manage). Developer keys get 403. Service Account keys and assumed-role OIDC sessions are not enabled on IAM routes.
data "homecloud_iam_service_account" "functions" {
name = "functions"
}
resource "homecloud_iam_policy" "mq" {
name = "ci-mq"
document = jsonencode({
Version = "2026-07-24"
Statement = [{
Effect = "Allow"
Action = ["mq:*"]
Resource = "arn:homecloud:mq::${data.homecloud_account.this.account_number}:queue/*"
}]
})
}
resource "homecloud_iam_role" "ci" {
name = "ci"
}
resource "homecloud_iam_policy_attachment" "functions_mq" {
policy_arn = homecloud_iam_policy.mq.arn
principal_type = "service_account"
principal_id = data.homecloud_iam_service_account.functions.id
}
Use the policy's arn (already IAM-canonical). Attachments take a principal UUID, not a name. Version in the document is 2026-07-24, not AWS 2012-10-17. Put GitHub OIDC trust on homecloud_iam_role (see above) when CI assumes the role.
P3 MDB / Redis¶
Create waits until status=active (errors if failed). GET is by name or UUID. User password is write-only and never returned. Redis passwords stay in credentials_secret.
resource "homecloud_mdb_instance" "app" {
name = "app-db"
engine = "postgresql"
instance_class = "micro"
}
resource "homecloud_mdb_user" "ci" {
instance_name = homecloud_mdb_instance.app.name
username = "ci"
password = var.db_password
}
resource "homecloud_redis_instance" "cache" {
name = "app-cache"
instance_class = "micro"
}
P4 Functions / IR / Domains¶
Spec-only function (no IDE files). Function URL is a sibling. Set wait_for_verified on homecloud_domain to poll TXT or nameserver verification. Image tags stay out of Terraform.
resource "homecloud_function" "hello" {
name = "hello"
handler = "main.handler"
}
resource "homecloud_function_url" "hello" {
function_name = homecloud_function.hello.name
}
resource "homecloud_ir_repository" "app" {
name = "frontend"
}
resource "homecloud_domain" "site" {
hostname = "example.com"
dns_mode = "homecloud"
wait_for_verified = true
}
resource "homecloud_dns_record" "www" {
domain_id = homecloud_domain.site.id
type = "A"
host = "www"
record = "1.2.3.4"
}
resource "homecloud_domain_attachment" "fn" {
domain_id = homecloud_domain.site.id
target_id = homecloud_function.hello.id
target_type = "function"
host = "www"
}
resource "homecloud_domain_attachment" "vm" {
domain_id = homecloud_domain.site.id
target_id = homecloud_machine.web.id
target_type = "machine"
host = "api"
}
Function delete needs owner/admin. Create/update works with a developer key.
P5 Compute / SSH / Applications¶
Machine create waits on the Operations API. Application create stays draft (no provision/deploy). SSH private_key is sensitive and only returned on create.
resource "homecloud_ssh_key" "ci" {
name = "ci"
}
resource "homecloud_application" "api" {
name = "api"
slug = "api"
template = "api-only"
}
resource "homecloud_compute_machine" "web" {
name = "web-1"
machine_class = "basic"
image_id = "ubuntu-24.04"
ssh_key_ids = [homecloud_ssh_key.ci.id]
}
| Resource | API |
|---|---|
homecloud_mq_queue |
/api/v1/accounts/{id}/queues |
homecloud_so_bucket |
/api/v1/accounts/{id}/storage/buckets |
homecloud_secret |
/api/v1/accounts/{id}/secrets |
data.homecloud_account |
whoami + GET /accounts/{id} |
homecloud_iam_policy |
/api/v1/accounts/{id}/iam/policies |
homecloud_iam_role |
/api/v1/accounts/{id}/iam/roles |
homecloud_iam_policy_attachment |
/iam/principals/attachments |
data.homecloud_iam_service_account |
GET /iam/service-accounts/{name} |
homecloud_mdb_instance |
/api/v1/accounts/{id}/databases |
homecloud_mdb_user |
/databases/{instance}/users |
homecloud_redis_instance |
/api/v1/accounts/{id}/caches |
homecloud_function |
/api/v1/accounts/{id}/functions |
homecloud_function_url |
.../functions/{name}/url |
homecloud_ir_repository |
/api/v1/accounts/{id}/registry/repositories |
homecloud_domain |
/api/v1/accounts/{id}/domains |
homecloud_dns_record |
/domains/{id}/dns-records |
homecloud_domain_attachment |
/api/v1/accounts/{id}/domain-attachments |
homecloud_compute_machine |
/api/v1/accounts/{id}/compute/machines |
homecloud_ssh_key |
/api/v1/accounts/{id}/compute/ssh-keys |
homecloud_application |
/api/v1/accounts/{id}/applications |
State id is the control-plane resources.id UUID. iam_arn is the IAM-canonical ARN (arn:homecloud:mq::…:queue/name). Import is almost always by name (slug for applications). After import, terraform plan should be empty if HCL matches the live object. The console stays fully writable — there is no managed_by=terraform lock.
Worked examples¶
| Directory | What it creates |
|---|---|
examples/mvp |
Queue + bucket + account data |
examples/secret |
Secret with write-only values |
examples/iam |
Policy + role + SA attachment (owner/admin) |
examples/mdb |
PostgreSQL + user + Redis |
examples/p4 |
Function + URL + IR repo + domain |
examples/p5 |
SSH key + draft application (machine commented) |
examples/github-oidc |
GitHub Actions OIDC trust + workflow skeleton |
Registry listing¶
Live: registry.terraform.io/providers/homecloudlab/homecloud. New v* tags are signed with the GPG key in docs/signing-key.asc. Publisher notes: PUBLISHING.md.
The console does not offer copy-HCL or Queue API curl tabs. Terraform lives here and in the provider repo.
Not in Terraform¶
Object bodies, MQ messages, function IDE files, mail inbox, IR tags, domain DNS verify, machine firewall/disks, application provision/deploy, and platform bootstrap stay on the SDK/CLI, the console, or GitHub Actions.