Files
nickandClaude Opus 5 90a2753a2a Run oswald-runner, ai and oswl-connector as Flatcar VMs
Replaces the Debian VM that hosted the Gitea Actions runner and adds two
new hosts on goose, all provisioned by Ignition instead of cloud-init:

- oswald-runner: Gitea Actions runner, keeping its registration in
  /var/lib/oswald_runner on disk rather than running ephemeral, which was
  leaving stale offline runners behind whenever a job was interrupted.
- ai: LiteLLM proxy and the Gitea MCP server.
- oswl-connector: tailscaled against headscale, advertising 192.168.7.0/24
  and itself as an exit node.

Their registration tokens and keys come from Infisical through the new
butane_vars plumbing, so nothing secret is committed -- but each one does
land in plaintext in the rendered Ignition snippet on the node, which is
the tradeoff for having no cloud-init here.

The provider gains an ssh block on both nodes because Proxmox has no API
for uploading snippets. That, the snippets content type, and the headscale
preauth key all have to be set up by hand first; flatcar-todo.md has the
steps and the reasoning behind the awkward parts.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 10:02:23 +00:00

113 lines
3.9 KiB
Terraform

terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "0.112.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.2"
}
# Butane -> Ignition transpiler for Flatcar VMs (no provider config needed).
ct = {
source = "poseidon/ct"
version = "~> 0.14"
}
technitium = {
source = "kenske/technitium"
version = "~> 0.2"
}
caddy = {
source = "conradludgate/caddy"
version = "~> 0.2"
}
# phpipam = {
# source = "lord-kyron/phpipam"
# version = "1.6.2"
# }
}
}
provider "technitium" {
host = var.technitium_host
# Fall back to a placeholder so the provider passes its credential check on
# the first run (before the token exists in Infisical). No technitium
# resources are created while the token is absent, so the dummy is never used.
token = coalesce(try(data.external.infisical.result["technitium_api_token"], ""), "token-not-yet-provisioned")
}
provider "caddy" {
# Endpoint as seen from wherever the request lands: with the SSH tunnel below
# this is the caddy host's own loopback admin API; without it, a directly
# reachable admin endpoint. Falls back to the Caddy default so the provider
# configures cleanly before caddy_host is set (no resources use it until then).
host = coalesce(var.caddy_host, "http://localhost:2019")
# Tunnel to the admin API over SSH when it only listens on loopback (the
# default, recommended setup). Reuses the ansible key generated above. Leave
# caddy_ssh_host empty to talk to caddy_host directly.
dynamic "ssh" {
for_each = var.caddy_ssh_host != "" ? [1] : []
content {
host = var.caddy_ssh_host
key_file = local_sensitive_file.ssh_private_key.filename
host_key = var.caddy_ssh_host_key
}
}
}
data "external" "infisical" {
program = ["bash", "-c", "infisical export --template=./secrets.tmpl"]
}
# Uncomment when phpIPAM is running and credentials are in Infisical.
# provider "phpipam" {
# app_id = "tofu"
# endpoint = var.ipam_endpoint
# username = coalesce(try(data.external.infisical.result["ipam_username"], ""), "user-not-yet-provisioned")
# password = coalesce(try(data.external.infisical.result["ipam_password"], ""), "pass-not-yet-provisioned")
# insecure = false
# nest_custom_fields = false
# }
# One provider alias per hypervisor. The API token is "user@realm!tokenid=secret".
provider "proxmox" {
alias = "proxmox"
endpoint = trimsuffix(var.hypervisors["proxmox"].api_url, "/api2/json")
api_token = "${data.external.infisical.result["token_id"]}=${data.external.infisical.result["hv2_token_secret"]}"
insecure = true
# SSH is only used to upload Ignition snippets for Flatcar VMs (Proxmox has
# no API for snippets). Requires the ansible public key in root's
# authorized_keys on the node; see flatcar-todo.md.
ssh {
username = "root"
private_key = base64decode(data.external.infisical.result["ssh_private_key_b64"])
node {
name = var.hypervisors["proxmox"].node_name
address = regex("^https?://([^:/]+)", var.hypervisors["proxmox"].api_url)[0]
}
}
}
provider "proxmox" {
alias = "goose"
endpoint = trimsuffix(var.hypervisors["goose"].api_url, "/api2/json")
api_token = "${data.external.infisical.result["token_id"]}=${data.external.infisical.result["goose_token_secret"]}"
insecure = true
# SSH is only used to upload Ignition snippets for Flatcar VMs (Proxmox has
# no API for snippets). Requires the ansible public key in root's
# authorized_keys on the node; see flatcar-todo.md.
ssh {
username = "root"
private_key = base64decode(data.external.infisical.result["ssh_private_key_b64"])
node {
name = var.hypervisors["goose"].node_name
address = regex("^https?://([^:/]+)", var.hypervisors["goose"].api_url)[0]
}
}
}