Total code of homelab infrastructure
  • HCL 94.2%
  • Makefile 5.8%
Find a file
alfred 4718f7d70e bump worker VM memory 12Gi -> 16Gi
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>
2026-08-29 14:41:31 +02:00
docs update PVC-dataset mapping after migrating all 10 volumes to nameTemplate naming 2026-08-11 22:28:22 +02:00
.envrc network interface change to physical 2026-07-22 13:59:00 +02:00
.gitignore stop tracking .env, keep it local 2026-07-31 20:38:09 +02:00
cert-manager.tf commit previously untracked/uncommitted infra: gateway.tf, external-dns.tf, and pending edits 2026-08-11 16:11:00 +02:00
cilium.tf secrets addition 2026-08-04 13:28:01 +02:00
descheduler.tf add descheduler to auto-rebalance pods across worker nodes 2026-08-28 12:17:22 +02:00
external-dns.tf bump worker VM memory to 12Gi; commit min-ttl fix that was only live 2026-08-28 15:07:06 +02:00
gateway-api.tf secrets addition 2026-08-04 13:28:01 +02:00
gateway.tf commit previously untracked/uncommitted infra: gateway.tf, external-dns.tf, and pending edits 2026-08-11 16:11:00 +02:00
main.tf add remote worker node and Cilium CNI 2026-07-31 21:48:45 +02:00
Makefile renameing 2026-07-21 16:56:04 +02:00
outputs.tf Upload files to "/" 2026-07-21 12:43:58 +00:00
README.md add planned-improvements section noting Cilium BGP peering follow-up 2026-08-17 21:56:50 +02:00
sops.tf secrets addition 2026-08-04 13:28:01 +02:00
storage.tf add nameTemplate to make tns-csi volume adoption actually automatic 2026-08-11 16:09:15 +02:00
talos.tf cilium implementation 2026-08-02 13:39:20 +02:00
terraform.tfvarstemplate commit previously untracked/uncommitted infra: gateway.tf, external-dns.tf, and pending edits 2026-08-11 16:11:00 +02:00
variables.tf bump worker VM memory 12Gi -> 16Gi 2026-08-29 14:41:31 +02:00
versions.tf adding changes for race condition 2026-08-03 11:38:20 +02:00

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.yamlnot committed

Prerequisites

  • OpenTofu >= 1.6
  • talosctl and kubectl on 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/CiliumBGPAdvertisement resources, 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.*