- HCL 94.2%
- Makefile 5.8%
Both worker VMs were getting memory-constrained with Immich (postgres, server, ML) added alongside everything else already running on them. Applied live and both VMs rebooted to pick it up; this just brings the repo in sync with what's actually running. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| docs | ||
| .envrc | ||
| .gitignore | ||
| cert-manager.tf | ||
| cilium.tf | ||
| descheduler.tf | ||
| external-dns.tf | ||
| gateway-api.tf | ||
| gateway.tf | ||
| main.tf | ||
| Makefile | ||
| outputs.tf | ||
| README.md | ||
| sops.tf | ||
| storage.tf | ||
| talos.tf | ||
| terraform.tfvarstemplate | ||
| variables.tf | ||
| versions.tf | ||
Talos Kubernetes cluster on Proxmox
OpenTofu configuration that provisions Talos Linux VMs on Proxmox and bootstraps
a Kubernetes cluster on them — VMs, Talos machine configs, etcd bootstrap, and
client credentials, all from a single tofu apply.
Layout
| File | Purpose |
|---|---|
main.tf |
Proxmox provider, Talos image schematic, ISO download, control-plane and worker VMs |
talos.tf |
Cluster secrets, machine configs, config apply, etcd bootstrap, kubeconfig/talosconfig files |
variables.tf |
All input variables |
versions.tf |
Provider requirements |
outputs.tf |
kubeconfig and talosconfig outputs (both sensitive) |
terraform.tfvars |
Environment-specific values — not committed |
generated/ |
Written by apply: talosconfig.yaml, kubeconfig.yaml — not committed |
Prerequisites
- OpenTofu >= 1.6
talosctlandkubectlon your PATH- direnv (optional but recommended)
- A Proxmox API token with permission to create VMs and download ISOs
- A reserved IP for the control-plane VIP, on the same L2 segment as the nodes and outside your DHCP pool
First-time setup
Create terraform.tfvars with the values that have no defaults:
cluster_name = "homelab"
controlplane_vip = "192.168.2.50"
proxmox_endpoint = "https://pve02.lan:8006/"
Keep the Proxmox API token out of any file — pass it as an environment variable:
Restoring on a new machine: sops_age_private_key has no other source of truth than the Vaultwarden secure note it was backed up to at generation time — restore the full age.agekey file content from there, not age-keygen (a freshly generated key can't decrypt anything already committed with the old one).
Then:
tofu init
tofu apply
After tofu apply
1. Wait for the cluster to actually be ready
tofu apply finishing means every API call was accepted — not that the
cluster is up. Talos still has to install to disk, reboot, start etcd, deploy
the CNI, and register nodes. This takes several minutes.
2. Point your tools at the generated configs
Apply writes credentials to generated/. Create .envrc in the repo root:
export TALOSCONFIG=$PWD/generated/talosconfig.yaml
export KUBECONFIG=$PWD/generated/kubeconfig.yaml
Then approve it once:
direnv allow
From now on, cd into this directory and both tools are configured
automatically; leave the directory and the variables are unset again.
Without direnv, source the variables manually per shell session:
export TALOSCONFIG=$PWD/generated/talosconfig.yaml
export KUBECONFIG=$PWD/generated/kubeconfig.yaml
3. Verify
talosctl health # blocks until etcd, API server, and nodes are healthy
kubectl get nodes # all nodes should reach Ready
talosctl get extensions # confirm qemu-guest-agent and iscsi-tools are installed
talosctl get addresses # confirm the VIP is attached to the control-plane node
Once the VIP is up, the cluster API is reachable at
https://<controlplane_vip>:6443 — this is the address baked into the
kubeconfig, so it stays valid across control-plane maintenance.
Notes and gotchas
State contains cluster secrets. talos_machine_secrets generates the
cluster's root CAs and stores them in state. Never commit unencrypted state.
Destroying and recreating that resource generates entirely new CAs and
invalidates the existing cluster.
Scaling the control plane. etcd needs an odd number of members — go from 1 to 3, never to 2. Two members is strictly worse than one for availability.
cluster_endpoint is permanent. It is baked into the cluster's TLS
certificates at creation time. Changing it later requires regenerating certs
across every node, so it points at the VIP from day one even with a single
control-plane node.
Install image must match the ISO schematic. The Talos image factory
schematic ID is used both for the ISO and for machine.install.image. If these
diverge, the VMs boot with the extensions but install without them.
Node IPs come from the guest agent. ipv4_addresses is a list of lists in
a non-guaranteed order, so locals in talos.tf flattens it and drops
loopback rather than indexing a fixed position. If an apply fails reading a node
IP, the VM is usually just still booting — re-run tofu apply.
Planned improvements
- Cilium BGP peering with the UniFi gateway (UCG-Fiber). Investigated as a
cleaner alternative to the current "allow both Talos node IPs" firewall rule
for reaching Home Assistant/Zigbee2MQTT across VLANs. Cilium can advertise
the Pod CIDR directly over BGP - real routable pod IPs, no per-node IP
tracking, survives node failover without a new firewall rule. Confirmed the
UCG-Fiber supports BGP. Real setup cost: new
CiliumBGPClusterConfig/CiliumBGPPeerConfig/CiliumBGPAdvertisementresources, plus a hand-written FRR config uploaded to the UniFi gateway (Settings → Policy Table → Policy Engine → Dynamic Routing). Not started - the firewall-rule approach already works and this would be a bigger follow-up project.
Not committed
terraform.tfvars
generated/
*.tfstate
*.tfstate.*