From 6fe33cbb964dbefd0c428043dfeec7ea4ec9e2f5 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Tue, 19 May 2026 22:05:11 +0200 Subject: [PATCH] traefik: migrate to bgp-pool (10.2.99.86 / fd99::86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traefik's LoadBalancer moves from the default-pool (10.2.40.86 / fd40::86, L2-announced) to the BGP-routed off-link pool. Every hostname behind traefik — every *.home.renner.uno, vscode.home.renner.uno, the cluster status page, paperless, immich, etc. — now reaches the ingress via the UDM's BGP-learned routes instead of L2 ARP on the mgmt LAN. This is the lever that takes vscode "fully working via the routed network" — the TLS + DNS access path goes through the same BGP route as the direct bgp-pool LB IPs. Same migration covers every other traefik-fronted service in one step. Companion updates: explicit `external-dns.alpha.kubernetes.io/target` annotations on the pve and test-browser IngressRoutes (Traefik doesn't populate IngressRoute.status.loadBalancer, so the target is hardcoded and has to track the LB IP); the bootstrap.sh / e2e-test.sh smoke-test IPs; a stale code comment in cilium-l2.yaml. The label/annotation pattern matches the ha-vscode migration: chart's `service.labels` sets `lb-pool: bgp` (single Service handles both TCP and UDP for HTTP/3, so one label is enough), and the `io.cilium/lb-ipam-ips` annotation pins the IPs deterministically. external-dns publishes the new IPs to Cloudflare + UniFi DNS providers on its next 60s reconcile cycle after the Service IP flips. Co-Authored-By: Claude Opus 4.7 (1M context) --- kubernetes/apps/pve/ingressroute.yaml | 2 +- kubernetes/apps/test-browser/ingressroute.yaml | 2 +- kubernetes/bootstrap.sh | 2 +- kubernetes/e2e-test.sh | 4 ++-- kubernetes/infrastructure/post-crds/cilium-l2.yaml | 2 +- kubernetes/infrastructure/traefik/helmrelease.yaml | 11 +++++++++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/kubernetes/apps/pve/ingressroute.yaml b/kubernetes/apps/pve/ingressroute.yaml index aef43c4e..bfaf836f 100644 --- a/kubernetes/apps/pve/ingressroute.yaml +++ b/kubernetes/apps/pve/ingressroute.yaml @@ -32,7 +32,7 @@ metadata: # external-dns-unifi (traefik-proxy source) has no implicit target. # Pin to the traefik Service VIP (kubernetes/infrastructure/traefik # service.annotations io.cilium/lb-ipam-ips). - external-dns.alpha.kubernetes.io/target: "10.2.40.86,fd40::86" + external-dns.alpha.kubernetes.io/target: "10.2.99.86,fd99::86" spec: entryPoints: - websecure diff --git a/kubernetes/apps/test-browser/ingressroute.yaml b/kubernetes/apps/test-browser/ingressroute.yaml index 674515bd..9e8580dd 100644 --- a/kubernetes/apps/test-browser/ingressroute.yaml +++ b/kubernetes/apps/test-browser/ingressroute.yaml @@ -30,7 +30,7 @@ metadata: annotations: # See pve/ingressroute.yaml — IngressRoute status isn't populated # by Traefik, so target the traefik Service VIP explicitly. - external-dns.alpha.kubernetes.io/target: "10.2.40.86,fd40::86" + external-dns.alpha.kubernetes.io/target: "10.2.99.86,fd99::86" spec: entryPoints: - websecure diff --git a/kubernetes/bootstrap.sh b/kubernetes/bootstrap.sh index 6a02d365..79d0cd54 100755 --- a/kubernetes/bootstrap.sh +++ b/kubernetes/bootstrap.sh @@ -352,6 +352,6 @@ if phase_runs wait-apps; then echo "" echo -n "Ingress (local): " - curl -sk --connect-timeout 5 --resolve status.renner.uno:443:10.2.40.86 https://status.renner.uno -o /dev/null -w "%{http_code}" 2>/dev/null || echo "N/A" + curl -sk --connect-timeout 5 --resolve status.renner.uno:443:10.2.99.86 https://status.renner.uno -o /dev/null -w "%{http_code}" 2>/dev/null || echo "N/A" echo "" fi diff --git a/kubernetes/e2e-test.sh b/kubernetes/e2e-test.sh index f4d601e8..ab906805 100755 --- a/kubernetes/e2e-test.sh +++ b/kubernetes/e2e-test.sh @@ -3,10 +3,10 @@ # Exits non-zero if any check fails. Prints a summary at the end. # # Assumes kubeconfig already loaded at ~/.kube/config and the runner can -# reach the ingress LB IP (10.2.40.86) and the cluster API endpoint. +# reach the ingress LB IP (10.2.99.86) and the cluster API endpoint. set -uo pipefail -LB_IP="10.2.40.86" +LB_IP="10.2.99.86" PASS=0 FAIL=0 diff --git a/kubernetes/infrastructure/post-crds/cilium-l2.yaml b/kubernetes/infrastructure/post-crds/cilium-l2.yaml index ab125887..5c7ed4f6 100644 --- a/kubernetes/infrastructure/post-crds/cilium-l2.yaml +++ b/kubernetes/infrastructure/post-crds/cilium-l2.yaml @@ -94,7 +94,7 @@ spec: # # IPv4 + IPv6 ranges in this pool keep matching last bytes (10.2.99.x ↔ # fd99::x) so an LB has the same "tail" in both families, matching the -# pattern the existing default-pool already follows (10.2.40.86 / fd40::86). +# pattern the existing default-pool already follows. apiVersion: cilium.io/v2alpha1 kind: CiliumLoadBalancerIPPool metadata: diff --git a/kubernetes/infrastructure/traefik/helmrelease.yaml b/kubernetes/infrastructure/traefik/helmrelease.yaml index 7bacd201..d37ee9bc 100644 --- a/kubernetes/infrastructure/traefik/helmrelease.yaml +++ b/kubernetes/infrastructure/traefik/helmrelease.yaml @@ -92,8 +92,15 @@ spec: ipFamilies: - IPv4 - IPv6 + # `lb-pool: bgp` selects the off-link BGP-routed pool (10.2.99.0/24 + # + fd99::/64). Cilium advertises this LB to the UDM via BGP and + # clients reach it via the UDM's BGP-learned routes — no L2 announce, + # no on-link ARP, no GARP race. Single Service handles both TCP + # (HTTP/HTTPS) and UDP (HTTP/3) so one label covers everything. + labels: + lb-pool: bgp annotations: - io.cilium/lb-ipam-ips: "10.2.40.86,fd40::86" + io.cilium/lb-ipam-ips: "10.2.99.86,fd99::86" # HTTP/3 adds a UDP port on the entrypoint; the chart emits a # separate UDP Service when any UDP port is exposed. Sharing-key # tells Cilium LB-IPAM to put the TCP and UDP Services on the @@ -101,7 +108,7 @@ spec: # UDP Service joins the same allocation deterministically. io.cilium/lb-ipam-sharing-key: "traefik-vip" annotationsUDP: - io.cilium/lb-ipam-ips: "10.2.40.86,fd40::86" + io.cilium/lb-ipam-ips: "10.2.99.86,fd99::86" io.cilium/lb-ipam-sharing-key: "traefik-vip" metrics: prometheus: