The VIP and HAProxy frontend/backend aren't exposed by the pfsense provider, so they must be created by hand before this stack is deployed.
4.6 KiB
k8s-bootstrap
This file provides guidance to Claude Code when working in 02-k8s-bootstrap. See the repo-root CLAUDE.md for shared conventions, deployment order, and CI/CD.
Bootstraps a k3s cluster on the Proxmox VMs created by proxmox-infra. Starts VMs via the Proxmox REST API, then provisions k3s over SSH using @pulumi/command remote.Command.
How it works
- Starts all VMs via
POST /api2/json/nodes/{node}/qemu/{vmid}/status/start - Waits for port 22 to open on each VM (bash
/dev/tcp), installs k3s onk3s-master-1with--cluster-init --tls-san <master1Ip> --tls-san <controlPlaneVipAddress> - Waits for
https://<controlPlaneVipAddress>:6443/healthzto respond before anyone else joins, since every subsequent join uses the VIP, notmaster1Ip. The VIP itself is a pfSense IP Alias Virtual IP with HAProxy TCP-load-balancing 6443 across all three masters — set up manually, not managed by any Pulumi stack (see "pfSense one-time setup" below). This is deliberately not a second kube-vip/ARP-based VIP:03-k8s-infra's existing kube-vip DaemonSet already handles Service/LoadBalancer VIPs (e.g. Traefik) separately, entirely inside the cluster, and API access is too critical to depend on the same ARP-failover mechanism - Joins
k3s-master-2andk3s-master-3as embedded etcd nodes via--server https://<controlPlaneVipAddress>:6443 - Joins
k3s-worker-1,k3s-worker-2, andk3s-worker-3as agent nodes viaK3S_URL=https://<controlPlaneVipAddress>:6443 - Formats the workers' dedicated
scsi1disk (ext4, if not already formatted) and mounts it at/mnt/longhorn-extra(via/etc/fstab) — this is the disk03-k8s-infralater registers as an extra Longhorn disk, to keep Longhorn off the root filesystem and avoidDiskPressure - Reads
/etc/rancher/k3s/k3s.yamlfrom master-1 via SSH, patches the server URL tocontrolPlaneVipAddress, and exports it as the secret stack outputkubeconfig
VM IDs and the CI runner SSH private key are read automatically from the proxmox-infra stack output via StackReference — no manual setup needed for those.
Required Pulumi config
# Pre-shared k3s token — any strong random string
pulumi config set --secret k3sToken "$(openssl rand -hex 32)"
# Node IPs — static DHCP leases from the router (not secrets)
pulumi config set master1Ip "192.168.1.x"
pulumi config set master2Ip "192.168.1.x"
pulumi config set master3Ip "192.168.1.x"
pulumi config set worker1Ip "192.168.1.x"
pulumi config set worker2Ip "192.168.1.x"
pulumi config set worker3Ip "192.168.1.x"
# Control-plane VIP — floating IP for API server access, fronted by pfSense
# + HAProxy (see "pfSense one-time setup" below). Must be a free address
# outside the router's DHCP pool.
pulumi config set --secret controlPlaneVipAddress "192.168.1.147"
Proxmox credentials (pve1Endpoint, pve1ApiToken, pve2Endpoint, pve2ApiToken, pve3Endpoint, pve3ApiToken) are read automatically from proxmox-infra via StackReference — do not set them here.
Prerequisites
proxmox-infra must be deployed first. On its first pulumi up it generates an ed25519 SSH keypair via @pulumi/tls, writes the public key into every VM's cloud-init, and exports the private key as a secret stack output. VMs must be re-provisioned (or have the public key manually added to ~/.ssh/authorized_keys) before this stack can SSH in.
pfSense one-time setup (before deploying)
The @pulumi/pfsense provider used by 01-proxmox-infra doesn't expose Virtual IP or HAProxy resources (only DHCP static mappings, DNS overrides, and firewall aliases), so this is manual, not Pulumi-managed:
- Confirm
controlPlaneVipAddressfalls outside the DHCP pool (Services → DHCP Server → Range). - Firewall → Virtual IPs → Add: Type =
IP Alias, Interface =LAN, Address =<controlPlaneVipAddress>/32. - System → Package Manager → install
haproxyif not already installed. - Services → HAProxy → Backend: add a pool (e.g.
k3s-apiservers), modeTCP, health checkTCP check(orssl-hello-chk), servers =<master1Ip>:6443,<master2Ip>:6443,<master3Ip>:6443. - Services → HAProxy → Frontend: add a frontend listening on
<controlPlaneVipAddress>:6443, typeTCP(not HTTP — this must stay an opaque passthrough so clients validate k3s's own TLS cert, not one terminated at pfSense), default backend = the pool from step 4. - Services → HAProxy → Settings: enable HAProxy, apply.
CI/CD
Workflow: ../.gitea/workflows/02-deploy-k8s-bootstrap.yaml. Secret: K8S_BOOTSTRAP_PULUMI_DEV_YAML. Deploy is manual (workflow_dispatch) — re-running this stack restarts the bootstrap sequence against live VMs.