The docs had drifted badly behind ~85 commits of delivery. The root README still
said "Phase 0 — Foundations. No cluster exists yet"; the infra docs still
described a libvirt/KVM cluster that has been retired for two days; and
DECISIONS-LOG.md — the PM's authoritative record — stopped at 2026-08-25 AM.
Anyone following these docs would have been working from a false map.
Verified against the live cluster (3/3 nodes, 7/7 Flux Kustomizations Ready,
10/10 HelmReleases Ready) rather than inferred from commit messages.
README.md
- Phase 0 -> a per-phase status table: 0-6 delivered and deployed, 7 not open.
- Layout now shows the wave split, platform-config, apps/vms, infra, bootstrap.
- Real conformance number (36/51, 70.6%) and how to regenerate it.
- Devcontainer workflow; note that the gate was red and why.
infra/PHASE1-STATUS.md — rewritten. Was a `virsh`/libvirt resume guide for a
cluster that no longer exists. Now: the Proxmox substrate, the pfSense NAT
paths, both kubectl access paths (Keycloak OIDC normal / cert break-glass),
the toolchain, off-repo state that must not be lost, and the fact that Flux's
git source is an app inside the cluster (`git push origin main` IS the deploy).
infra/PHASE1-SCOPE.md — historical banner. The exit criteria and sizing
rationale are still worth keeping; the libvirt execution steps are not.
infra/README.md — Proxmox, plus a map of the subtree and where the cluster
actually lives (it is not on this workstation).
infra/devcontainer/README.md — the "run tofu and ansible on the HOST" limitation
is OBSOLETE: it was a dmacvicar/libvirt polkit/PID problem, and bpg/proxmox
talks to an HTTP API. Everything runs in the container now. Documents the full
CI-gate toolchain and what each bind mount is load-bearing for (the age keyring
and the read-only spec repo especially — without the latter, spec-immutability
silently skips instead of verifying).
infra/tofu/README.md — was "Authoring only — nothing here has been applied". It
is applied and live. Disk sizes corrected 40/100 -> 64/128 GiB (confirmed
against the running nodes), pfSense NAT marked done, and a warning that
terraform.tfstate is absent on a fresh workstation — an apply with empty state
would try to recreate the three live nodes.
infra/tofu/variables.tf — section comment disagreed with its own defaults.
infra/cluster.sh — retired-with-instructions header. It silently drives libvirt
domains that do not exist; behaviour unchanged, but it no longer lies.
apps/vms/unifi/README.md — was "NOT WIRED IN", 60Gi, and listed a file that has
since been dropped. Now: live status, 40Gi, resolved open questions marked
resolved, and the two constraints the next person will otherwise rediscover —
KubeVirt's 2048B inline userData cap, and that apps-vms must never carry
postBuild.substituteFrom (envsubst corrupts the cloud-init).
DECISIONS-LOG.md — backfilled the gap as one structured entry: the Proxmox
migration, NAT + kube-vip, the twice-proven oauth2-proxy cold-start deadlock,
the Flux source cutover, CI going live in-cluster, ten app onboardings and the
renderer bugs they exposed, the wave split (verified: DBs adopted, not
recreated — original ages intact, prune restored), KubeVirt storage, the UniFi
VM, and kubectl SSO. Plus verified live state and the carried-forward open list.
Two corrections to the record while verifying:
- kubectl OIDC is LIVE (`kubectl auth whoami` -> oidc:<user>,
[oidc:platform-admins]), so the rolling restart 3472f1d deferred has
happened.
- UniFi OS Server is NOT installed in the guest. The VM is healthy (Ubuntu
24.04.4, guest agent connected, live-migratable) but :11443 answers nothing,
and the CNP explicitly permits cluster -> :11443, so it is genuinely not
listening rather than a policy artifact.
Docs and comments only; no manifest semantics change.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
200 lines
6.5 KiB
Terraform
200 lines
6.5 KiB
Terraform
# variables.tf — every sizing / IP / image knob is a variable (Proxmox migration).
|
|
# Defaults encode the migration target: 3 k3s nodes on the Proxmox VE box
|
|
# (node "proxmox", 192.168.1.0/24 LAN). Override via terraform.tfvars (see
|
|
# terraform.tfvars.example). Traces R-ENV-01 (reproducible node provisioning),
|
|
# R-PLAT-01.
|
|
|
|
# --- Proxmox connection / placement ----------------------------------------
|
|
|
|
variable "proxmox_endpoint" {
|
|
description = <<-EOT
|
|
Proxmox VE API endpoint, e.g. "https://192.168.2.102:8006/". Optional: if unset
|
|
the provider reads PROXMOX_VE_ENDPOINT from the environment directly. Set this
|
|
(e.g. via `export TF_VAR_proxmox_endpoint="$PROXMOX_VE_ENDPOINT"`) to let tofu
|
|
NORMALIZE a schemeless endpoint (prepend https://) — see versions.tf. The API
|
|
TOKEN is NOT a variable; it is read from PROXMOX_VE_API_TOKEN by the provider so
|
|
it never enters tofu state/vars.
|
|
EOT
|
|
type = string
|
|
default = null
|
|
}
|
|
|
|
variable "proxmox_insecure" {
|
|
description = "Skip TLS verification for the Proxmox self-signed cert (also read from PROXMOX_VE_INSECURE)."
|
|
type = bool
|
|
default = true
|
|
}
|
|
|
|
variable "proxmox_node" {
|
|
description = "Proxmox VE node name that hosts the VMs (VERIFIED: proxmox)."
|
|
type = string
|
|
default = "proxmox"
|
|
}
|
|
|
|
variable "datastore" {
|
|
description = "Datastore for VM OS + data disks (VERIFIED: Storage, ZFS 4.3TB, rootdir+images)."
|
|
type = string
|
|
default = "Storage"
|
|
}
|
|
|
|
variable "image_datastore" {
|
|
description = "Datastore that holds the downloaded cloud image / cloud-init snippets (VERIFIED: local, dir storage)."
|
|
type = string
|
|
default = "local"
|
|
}
|
|
|
|
variable "bridge" {
|
|
description = "Proxmox network bridge for the node NIC (VERIFIED: vmbr0, no VLAN tag)."
|
|
type = string
|
|
default = "vmbr0"
|
|
}
|
|
|
|
# --- cluster identity / naming ---------------------------------------------
|
|
|
|
variable "cluster_name" {
|
|
description = "Logical cluster name; used as a resource-name prefix."
|
|
type = string
|
|
default = "k3s-dev"
|
|
}
|
|
|
|
variable "node_name_prefix" {
|
|
description = "VM hostname prefix. Nodes are <prefix>1, <prefix>2, ... (LOCKED: k3s-dev-1/2/3)."
|
|
type = string
|
|
default = "k3s-dev-"
|
|
}
|
|
|
|
variable "node_count" {
|
|
description = "Number of k3s nodes (LOCKED: 3-server HA embedded etcd)."
|
|
type = number
|
|
default = 3
|
|
|
|
validation {
|
|
condition = var.node_count == length(var.node_ips)
|
|
error_message = "node_count must equal the number of entries in node_ips."
|
|
}
|
|
}
|
|
|
|
# --- per-node sizing (Proxmox target: 6 vCPU / 16 GiB / 64 GiB OS + 128 GiB data) --
|
|
|
|
variable "vcpu" {
|
|
description = "vCPUs (cores) per node. Generous — the box has plenty."
|
|
type = number
|
|
default = 6
|
|
}
|
|
|
|
variable "memory_mib" {
|
|
description = "RAM per node in MiB (16 GiB)."
|
|
type = number
|
|
default = 16384
|
|
}
|
|
|
|
variable "os_disk_gib" {
|
|
description = "OS (root) disk size per node, GiB. Imported from the cloud image, on the Storage datastore."
|
|
type = number
|
|
default = 64
|
|
}
|
|
|
|
variable "data_disk_gib" {
|
|
description = "Dedicated Longhorn data disk per node, GiB. Blank, on the Storage datastore."
|
|
type = number
|
|
default = 128
|
|
}
|
|
|
|
variable "cpu_type" {
|
|
description = "Proxmox vCPU type. `host` passes through the physical CPU (nested-virt for KubeVirt, Phase 5)."
|
|
type = string
|
|
default = "host"
|
|
}
|
|
|
|
# --- network (Proxmox target: main LAN 192.168.1.0/24, gw .1, DNS PiHole .18) ---
|
|
|
|
variable "network_cidr" {
|
|
description = "Cluster subnet in CIDR (informational; nodes get static IPs from node_ips)."
|
|
type = string
|
|
default = "192.168.1.0/24"
|
|
}
|
|
|
|
variable "network_prefix" {
|
|
description = "Netmask prefix length for node static addressing."
|
|
type = number
|
|
default = 24
|
|
}
|
|
|
|
variable "network_gateway" {
|
|
description = "Default gateway for nodes (VERIFIED: 192.168.1.1, pfSense)."
|
|
type = string
|
|
default = "192.168.1.1"
|
|
}
|
|
|
|
variable "dns_server" {
|
|
description = "DNS resolver for nodes (VERIFIED: 192.168.1.18, PiHole)."
|
|
type = string
|
|
default = "192.168.1.18"
|
|
}
|
|
|
|
variable "node_ips" {
|
|
description = "Static IPs per node, in order (VERIFIED: .50/.51/.52 on 192.168.1.0/24)."
|
|
type = list(string)
|
|
default = ["192.168.1.50", "192.168.1.51", "192.168.1.52"]
|
|
}
|
|
|
|
# Reserved for Cilium L2-announcement ingress VIPs (VERIFIED: .53-.60).
|
|
# Not materialised as Proxmox objects; surfaced in outputs. Cilium's IP pool
|
|
# (platform-config/cilium/l2-ip-pool.yaml) consumes this range.
|
|
variable "cilium_l2_vip_range_start" {
|
|
description = "First reserved Cilium L2 VIP."
|
|
type = string
|
|
default = "192.168.1.53"
|
|
}
|
|
|
|
variable "cilium_l2_vip_range_end" {
|
|
description = "Last reserved Cilium L2 VIP."
|
|
type = string
|
|
default = "192.168.1.60"
|
|
}
|
|
|
|
# --- base OS image ----------------------------------------------------------
|
|
|
|
variable "image_source" {
|
|
description = <<-EOT
|
|
Source URL for the base OS cloud image (qcow2), downloaded to image_datastore
|
|
by proxmox_virtual_environment_download_file. Default is Debian 12 (bookworm)
|
|
generic amd64 (full firmware/driver, hybrid BIOS/UEFI). Use `generic`, NOT
|
|
`genericcloud`: the genericcloud image panics with "Attempted to kill init" on
|
|
import + disk-resize under bpg (bpg issue #1639).
|
|
EOT
|
|
type = string
|
|
default = "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2"
|
|
}
|
|
|
|
variable "image_file_name" {
|
|
description = <<-EOT
|
|
File name the cloud image is stored as on image_datastore. With `import`-content
|
|
storage (PVE 8.2+) the native .qcow2 name is used directly — the old .img rename
|
|
was an `iso`-content workaround and is no longer needed. Uses the `generic`
|
|
(full firmware/driver, hybrid BIOS/UEFI) image, NOT `genericcloud`: genericcloud
|
|
panics with "Attempted to kill init" on import + disk-resize (bpg issue #1639).
|
|
EOT
|
|
type = string
|
|
default = "debian-12-generic-amd64.qcow2"
|
|
}
|
|
|
|
# --- cloud-init identity ----------------------------------------------------
|
|
|
|
variable "ssh_authorized_key" {
|
|
description = "SSH PUBLIC key injected into the login user (public only — never a private key)."
|
|
type = string
|
|
}
|
|
|
|
variable "login_user" {
|
|
description = "Non-root login user created by cloud-init; also the Ansible ansible_user."
|
|
type = string
|
|
default = "k3s"
|
|
}
|
|
|
|
variable "node_domain" {
|
|
description = "DNS domain suffix for node FQDNs (informational; dev uses split-horizon / hosts)."
|
|
type = string
|
|
default = "k3s.dev.local"
|
|
}
|