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

206 lines
7.1 KiB
Terraform

variable "hypervisors" {
description = "Per-hypervisor configuration. Keys must match provider aliases in providers.tf (e.g. hv2, hv3)."
type = map(object({
api_url = string # e.g. https://192.168.1.10:8006/
node_name = string
storage = string
}))
}
variable "ipam_endpoint" {
description = "Base URL of the PHPIPAM API, e.g. https://ipam.example.com/api"
type = string
default = ""
}
variable "network_subnets" {
description = "Named map of CIDR ranges managed by PHPIPAM. Only used when enable_ipam = true."
type = map(string)
default = {}
}
variable "ansible_user" {
description = "User created on each managed host for Ansible access."
type = string
}
variable "search_domain" {
description = "DNS search domain set on all managed hosts."
type = string
}
variable "dns_nameservers" {
description = "DNS nameserver(s) set on all managed hosts."
type = string
}
variable "technitium_host" {
description = "URL of the Technitium DNS Server API (e.g. http://192.168.0.3:5380). Required once Technitium is running."
type = string
default = ""
}
variable "technitium_enabled" {
description = "Set to false to stop Terraform from managing Technitium DNS zones/records, without changing anything else. Still requires technitium_api_token in Infisical to actually turn on."
type = bool
default = true
}
variable "dns_zones" {
description = "Additional Technitium DNS zones to create, beyond the auto-managed search_domain zone. Keyed by zone name."
type = map(object({
type = optional(string, "Primary")
forwarder = optional(string)
}))
default = {}
}
variable "dns_records" {
description = "Manually managed DNS records. Each targets a zone by name (the search_domain zone or one from dns_zones). A record in the search_domain zone whose name matches an auto-added host replaces that host record instead of conflicting with it."
type = list(object({
zone = string
name = string # subdomain label, or "@" for the zone apex
type = string # A, AAAA, CNAME, NS, TXT, MX
value = string # IP / target / text / mail exchanger
ttl = optional(number)
priority = optional(number) # required for MX records
}))
default = []
}
variable "caddy_host" {
description = "Caddy admin API endpoint. When using the SSH tunnel this is the endpoint as seen from the caddy host (e.g. http://localhost:2019). Leave empty to disable Caddy management."
type = string
default = ""
}
variable "caddy_ssh_host" {
description = "SSH target (user@host:port) used to tunnel to the Caddy admin API. Empty to reach caddy_host directly."
type = string
default = ""
}
variable "caddy_ssh_host_key" {
description = "SSH host key of the caddy host, known_hosts format (get it with: ssh-keyscan <ip>). Required when caddy_ssh_host is set."
type = string
default = ""
}
variable "caddy_proxies" {
description = "Reverse-proxy sites keyed by name. Each maps a hostname to one or more upstream backends (Caddy dial targets, e.g. \"192.168.0.40:32400\"). protocol picks plain HTTP on :80 (default) or HTTPS on :443 with automatic certificates. Set path to restrict the route to specific request paths."
type = map(object({
host = string
upstreams = list(string)
protocol = optional(string, "http")
path = optional(list(string))
}))
default = {}
}
variable "ansible_inventory_path" {
description = "Path where the generated Ansible inventory file is written. Defaults to ../ansible/inventories/generated.yml relative to this module, which points to the parent repo when used as a git submodule."
type = string
default = "./ansible/inventories/generated.yml"
}
# LXC, VM and Flatcar VM definitions per hypervisor, keyed by the same names
# used in hypervisors and providers.tf. All three maps default to {} so a node
# can be declared with any subset of workload types.
variable "nodes" {
type = map(object({
lxc = optional(map(object({
hostname = string
vmid = number
template = string
os_type = optional(string, "debian")
unprivileged = bool
onboot = bool
tags = string
cores = optional(number, 1)
memory = number
swap = number
disk_size = string
nic_name = string
bridge = string
vlan = optional(number)
ip = string
nameserver = optional(string)
gw = string
nic2 = optional(object({
nic_name = optional(string, "eth1")
bridge = optional(string, "vmbr0")
vlan = optional(number)
ip = string
gw = optional(string)
}))
})), {})
machines = optional(map(object({
hostname = string
vmid = number
ip = string
vlan = optional(number, 0)
template = string
full_clone = bool
onboot = bool
ciupgrade = optional(bool, false)
tags = string
agent = number
memory = number
disk_size = optional(string, "32")
balloon = number
cpu_type = string
cores = number
sockets = number
vcpus = number
bios = string
machine = string
bridge = string
nic2 = optional(object({
bridge = optional(string, "vmbr0")
vlan = optional(number)
ip = string
gw = optional(string)
}))
})), {})
flatcar = optional(map(object({
hostname = string
vmid = number
ip = string
gw = optional(string)
vlan = optional(number, 0)
bridge = optional(string, "vmbr0")
onboot = optional(bool, true)
tags = optional(string, "")
cores = optional(number, 2)
cpu_type = optional(string, "x86-64-v2-AES")
memory = optional(number, 4096)
disk_size = optional(number, 32)
qemu_guest_agent = optional(bool, false) # needs a Butane unit running qemu-ga in a container
butane = optional(string) # inline Butane YAML
butane_file = optional(string) # path relative to this directory
})), {})
}))
default = {}
}
# Flatcar release imported onto every node that runs Flatcar VMs. Pin a version
# and copy its sha512 from the image's .DIGESTS file; see opentofu/README.md.
variable "flatcar_image" {
type = object({
channel = optional(string, "stable")
version = string
sha512 = string
datastore = optional(string, "local")
})
default = {
version = "4593.2.5"
sha512 = "21fdba07ffc73aac80aa40f1f99e0460e2d53b1c2815a774d5e2940d2b5cfa78fb2c86391fbf3bb76e28e8ece1ca41333f30ce18d1dd3f0c016e95bed120aba8"
}
}
variable "snippets_datastore" {
description = "Datastore holding Ignition snippets for Flatcar VMs. Needs the snippets content type enabled on every node."
type = string
default = "local"
}