Files
oswald-infra/main.tf
T
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

157 lines
5.3 KiB
Terraform

resource "local_sensitive_file" "ssh_private_key" {
filename = pathexpand("~/.ssh/infra_ansible_key")
content_base64 = data.external.infisical.result["ssh_private_key_b64"]
file_permission = "0600"
}
locals {
# Flatten all declared hosts from tfvars. Derived from var.nodes (not module
# outputs) so the values are known at plan time and for_each can use them
# before any VMs are created.
all_declared_hosts = flatten([
for hv_name, hv in var.nodes : concat(
[for name, lxc in try(hv.lxc, {}) : { name = name, ip = split("/", lxc.ip)[0] }],
[for name, vm in try(hv.machines, {}) : { name = name, ip = split("/", vm.ip)[0] }],
[for name, fc in try(hv.flatcar, {}) : { name = name, ip = fc.ip }]
)
])
technitium_enabled = var.technitium_enabled && try(data.external.infisical.result["technitium_api_token"], "") != ""
ipam_enabled = try(data.external.infisical.result["ipam_username"], "") != ""
caddy_enabled = var.caddy_host != ""
# Shared inputs passed to every proxmox_node module call.
node_common = {
search_domain = var.search_domain
dns_nameservers = var.dns_nameservers
ssh_key = data.external.infisical.result["ssh_public_key"]
ansible_user = var.ansible_user
lxc_password = data.external.infisical.result["lxc_password"]
ssh_private_key_path = local_sensitive_file.ssh_private_key.filename
}
}
# Provider aliases must be statically declared, so each hypervisor needs its
# own module block. To add or remove a hypervisor, add/remove the corresponding
# provider alias in providers.tf and a module block here. Everything else
# (node contents, common config) lives in the parent repo's terraform.tfvars.
module "proxmox" {
source = "./opentofu/modules/proxmox_node"
node_name = var.hypervisors["proxmox"].node_name
storage = var.hypervisors["proxmox"].storage
lxc = try(var.nodes["proxmox"].lxc, {})
machines = try(var.nodes["proxmox"].machines, {})
flatcar = try(var.nodes["proxmox"].flatcar, {})
search_domain = local.node_common.search_domain
dns_nameservers = local.node_common.dns_nameservers
ssh_key = local.node_common.ssh_key
ansible_user = local.node_common.ansible_user
lxc_password = local.node_common.lxc_password
ssh_private_key_path = local.node_common.ssh_private_key_path
flatcar_image = var.flatcar_image
snippets_datastore = var.snippets_datastore
butane_vars = data.external.infisical.result
providers = {
proxmox = proxmox.proxmox
}
}
module "goose" {
source = "./opentofu/modules/proxmox_node"
node_name = var.hypervisors["goose"].node_name
storage = var.hypervisors["goose"].storage
lxc = try(var.nodes["goose"].lxc, {})
machines = try(var.nodes["goose"].machines, {})
flatcar = try(var.nodes["goose"].flatcar, {})
search_domain = local.node_common.search_domain
dns_nameservers = local.node_common.dns_nameservers
ssh_key = local.node_common.ssh_key
ansible_user = local.node_common.ansible_user
lxc_password = local.node_common.lxc_password
ssh_private_key_path = local.node_common.ssh_private_key_path
flatcar_image = var.flatcar_image
snippets_datastore = var.snippets_datastore
butane_vars = data.external.infisical.result
providers = {
proxmox = proxmox.goose
}
}
locals {
hv_hosts = {
proxmox = { lxc = module.proxmox.lxc_hosts, vm = module.proxmox.vm_hosts, flatcar = module.proxmox.flatcar_hosts }
goose = { lxc = module.goose.lxc_hosts, vm = module.goose.vm_hosts, flatcar = module.goose.flatcar_hosts }
}
all_hosts = flatten([
for hv_name, hv in local.hv_hosts : [
for h in concat(hv.lxc, hv.vm, hv.flatcar) : [
for tag in h.filtered_tags : {
tag = tag
name = h.name
ip = h.ip
vmid = h.vmid
}
]
]
])
tag_groups = {
for tag in distinct([for h in local.all_hosts : h.tag]) :
tag => [
for h in local.all_hosts : {
name = h.name
ip = h.ip
vmid = h.vmid
} if h.tag == tag
]
}
}
# Uncomment when phpIPAM is running and credentials are in Infisical.
# module "phpipam" {
# source = "./opentofu/modules/phpipam"
#
# enabled = local.ipam_enabled
# hosts = local.all_declared_hosts
# network_subnets = var.network_subnets
# }
module "technitium" {
source = "./opentofu/modules/technitium"
enabled = local.technitium_enabled
zone = var.search_domain
hosts = local.all_declared_hosts
zones = var.dns_zones
records = var.dns_records
}
module "caddy" {
source = "./opentofu/modules/caddy"
enabled = local.caddy_enabled
proxies = var.caddy_proxies
providers = {
caddy = caddy
}
}
resource "local_file" "ansible_inventory" {
filename = "${path.module}/${var.ansible_inventory_path}"
content = templatefile("${path.module}/opentofu/inventory.tpl", {
hypervisors = local.hv_hosts
tag_groups = local.tag_groups
ansible_user = var.ansible_user
ansible_ssh_private_key_file = local_sensitive_file.ssh_private_key.filename
})
}