Fix cluster bring-up defects found converging the anvil nodes #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/foundry-k3s-cluster-4057a8"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Everything here came out of actually converging the three nodes. Static checks — yamllint,
ansible-inventory --list,--syntax-check,ansible-lint— passed on all of it.The one that mattered
Cluster traffic was about to run over Tailscale.
k3s_server_addresswas the hostnamefoundry-white, which resolves on foundry — where MagicDNS is accepted — to the node's100.xtailnet address. Every agent connection and every flannel packet would have gone through userspace WireGuard on a Raspberry Pi: the exact outcome the LAN/tailnet split was designed to prevent, and one that would have looked like nothing worse than "the cluster feels slow".Now pinned to the LAN address, with
k3s_node_ipset per node so kubelet does not choose among the Tailscale, Docker bridge and CNI addresses these hosts also carry.Environment findings
Both Pis were multi-homed — Ethernet and Wi-Fi up on the same subnet, each with a default route. That gives kubelet and flannel a second address to guess wrong about and causes ARP flux, while providing no useful failover (same subnet, same switch, same upstream). The
raspberry_pirole now blacklists the Wi-Fi driver rather than editing netplan, because thewifisstanza lives in a cloud-init-owned file that may be rewritten.Addresses now match the DHCP reservations:
foundry10.10.10.10,foundry-white10.10.10.20,foundry-black10.10.10.30.Bugs static checks could not catch
Origins-PatternusedAllowed-Originssyntaxapt-config dumpreported the config as present and correct whileunattended-upgradecrashed on startup with aValueError. The dry-run check caught it — which is exactly why that check exists.ansible_managedincopy: content:templateaction plugin injects it. Three tasks failed at runtime.systemd-timesyncdassumedchrony. The role now detects the daemon; what it gates on is the kernel's synchronised state, whichtimedatectlreports identically for either.Signed-Bydiffers from the deb822 source this role writes. Two entries for one repository with different keyrings makes apt refuse to read any source list.A defect in my own guard logic
The delegated agent-readiness check and the node-label reconciler tested whether the server was in
ansible_play_hosts_all. Butplaybooks/k3s.ymlconverges the server in an earlier play, so that was false during normal push runs too — both skipped silently on every run. Agents were never verified cluster-side and labels were never reconciled.They now test inventory group membership, which is what actually differs under
ansible-pull:inventory-pull.ymldeliberately has nok3s_servergroup at all.Traefik moves wholly to
anvilRather than
foundryowning the bundled chart whileanvilowns everything about it, K3s's Traefik stays disabled andanvilinstalls the ingress controller outright. No split ownership, noHelmChartConfigreaching across the boundary.foundrykeeps thesvccontroller.k3s.cattle.io/enablelblabel onfoundry-black. That is what confines ServiceLB and protects Caddy's ports — a property of the node, not of the workload. With Traefik disabled it matters more, since the label is now the only thing standing between ananvildeployment and foundry's :80/:443. It is already applied, so it is in place beforeanvilcan create its firstLoadBalancerService.The durable rule, now stated in the README:
foundryowns where things may run;anvilowns what runs.kubectl on all three nodes
Each node gets a managed kubeconfig and an
~/.kube/configsymlink. Two per-node differences, both deliberate:--accept-dns=falseto keep tailscaled's resolver away from CoreDNS, so MagicDNS does not resolve there. It also means kubectl on a Pi keeps working when Tailscale is down.kubectlis a symlink to the k3s binary, and that wrapper prefers/etc/rancher/k3s/k3s.yamlover~/.kube/configwhenever it exists — then hard-fails if it cannot read it. Root-only ownership made kubectl unusable on the server despite a perfectly good kubeconfig in the owner's home directory. This grants cluster-admin to an account that already has passwordless sudo on that node, so it is not an escalation.One piece of cosmetic noise is documented rather than fixed: the k3s wrapper also reads
/etc/rancher/k3s/config.yaml, which stays0600because it holds the join token, so everykubectlcall on a Pi prints a permission warning to stderr. Results and exit codes are unaffected.Verified state
All three nodes
Ready, correct internal IPs, no taints,enablelb=trueonfoundry-blackand nowhere else, zero Traefik pods.playbooks/k3s.yml,playbooks/raspberry-pi.yml,playbooks/auto-updates.ymlandplaybooks/tailscale.ymlall converge tochanged=0on a repeat run.The hourly ansible-pull converge on foundry has been failing since the cluster work landed: fatal: [foundry -> {{ groups['k3s_server'] | first }}]: FAILED! => "object of type 'dict' has no attribute 'k3s_server'" Three tasks delegate to the K3s server, and each was guarded with a `when` that checks the group is non-empty. That guard never gets a chance: Ansible templates delegate_to during task setup, before evaluating `when`, so referencing groups['k3s_server'] raises on any run where the group does not exist - regardless of how carefully the task is gated. inventory-pull.yml deliberately omits the group, which makes ansible-pull exactly that case. The target is now resolved once in tasks/main.yml, into k3s_server_available and k3s_server_host, with the host falling back to inventory_hostname when the group is missing. That value is always templatable, and every task using it stays gated on k3s_server_available so the fallback never actually runs. Two fail_msg strings referenced the group directly as well; one of them sat on an assert that is not gated, so it would have raised while reporting an unrelated failure. Worth noting what this cost while it was broken. The pull run reached ok=278 before dying on the last task, so it applied most of a converge from the mirror - including rewriting foundry's K3s config from the pre-fix code, pointing the agent at the server by hostname. On foundry that hostname resolves through MagicDNS to the tailnet address, so each hourly run quietly moved the agent onto WireGuard and restarted it, until the next push run put it back. Verified by running the real thing on foundry: ansible-pull's exact invocation against inventory-pull.yml, both playbooks/k3s.yml and the full foundry.yml, with the whole repo present. Both complete with failed=0, and the k3s play reports changed=0. The server-dependent tasks skip cleanly - 23 skipped in pull mode against 19 in push mode, which is the difference showing up where it should.