Files
home-network-k3s-impl/infra/tofu
jakman85andClaude Opus 4.8 95e3eafac6 iac: harden node lifecycle — tofu for_each + self-contained ansible rejoin
Implements T-1.5 findings F1/F2 (author-first, no cluster change).

F1 (tofu): refactor libvirt node resources count -> for_each keyed by
node name so single-node -replace/destroy cannot cascade to siblings.
Add ordered-list/primary helper locals; update outputs.tf off the map;
add moved.tf (12 moved blocks). tofu plan proves a pure state-address
migration: 0 to add, 0 to change, 0 to destroy (all 12 'has moved to').
State-persist via a 0-change apply is operator-permission-gated.

F2 (ansible): k3s_server secondary fetches the join token via
delegate_to the primary (published fact as fast path), so a single-node
rejoin with --limit k3s-dev-N is self-contained and no longer depends on
a fact an earlier play had to set. Syntax-check + CI gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-16 19:42:12 -05:00
..

infra/tofu — dev/test cluster nodes (OpenTofu + libvirt)

Phase 1 T-1.1. Provisions the "Create" lifecycle stage (docs/02): a dedicated NAT network and 3 cloud-init'd KVM VMs, each with a thin OS disk and a blank thin Longhorn data disk. Authoring only — nothing here has been applied.

Serves R-ENV-01/02, R-PLAT-01, ADR-0001 (k3s substrate), and the network half of the LOCKED PHASE1-SCOPE decisions.

What it builds (defaults = LOCKED decisions)

Resource Value
NAT network k3s-dev, 10.73.0.0/24, gateway 10.73.0.1, DHCP off (static IPs)
Nodes k3s-dev-1/2/3 at 10.73.0.11/.12/.13
Per node 4 vCPU, 6 GiB RAM, host-passthrough CPU (nested virt for KubeVirt)
OS disk 20 GiB thin qcow2, backed by the base cloud image
Data disk 40 GiB blank thin qcow2 → /dev/vdb, mounted at /var/lib/longhorn by cloud-init
Seed libvirt_cloudinit_disk wired to ../cloud-init/ templates
Reserved 10.73.0.240–.250 kept out of any DHCP range for Cilium L2 ingress VIPs

Base OS image is a variable (image_source), defaulting to Debian 12 genericcloud amd64. All sizing/IPs/counts are variables — see variables.tf.

Files

  • versions.tf — OpenTofu 1.9.0 pin + dmacvicar/libvirt ~> 0.8.1 provider pin.
  • variables.tf — every knob (counts, sizing, IPs, image, SSH key).
  • main.tf — network, base/OS/data volumes, cloud-init seed, domains.
  • outputs.tf — node IPs/names, primary_node_ip, k3s_api_endpoint, and a rendered ansible_inventory for infra/ansible/.
  • terraform.tfvars.example — copy to terraform.tfvars; only ssh_authorized_key is required.

Usage (operator, in the dev container — NOT run here)

cd infra/tofu
cp terraform.tfvars.example terraform.tfvars   # set ssh_authorized_key
tofu init
tofu plan
tofu apply
# hand the node IPs to Ansible:
tofu output -raw ansible_inventory > ../ansible/inventory/hosts.ini

Hand-off contract

  • primary_node_ip / k3s_api_endpoint (10.73.0.11:6443) is the value the Ansible Cilium bootstrap and the Flux platform/cilium HelmRelease must BOTH use for k8sServiceHost / k8sServicePort (docs/02 parity contract).
  • Destroy/rebuild proof (T-1.5): tofu destroy -target=libvirt_domain.node[1] then tofu apply re-creates that node; Ansible re-joins it; Longhorn replicas reconverge (nodes are cattle — docs/02 statelessness).

Static validation (PM, in dev container)

tofu fmt -check -recursive
tofu init -backend=false
tofu validate