# 05 — MetalLB: getting real IP addresses on bare metal The app-of-apps you just applied ([`docs/04-app-of-apps.md`](04-app-of-apps.md)) already brought up `metallb` alongside `demo`. This doc explains what it does, walks you through the one piece only you can decide (which IP addresses to actually use), and finishes by giving Argo CD's own UI a real address too. ## Why this is needed at all On a cloud provider, a Kubernetes Service of `type: LoadBalancer` triggers the cloud to provision an actual load balancer and hand your Service a real, routable IP automatically. Bare metal — your Talos node, sitting on your own network — has no such thing behind it. Without help, a `LoadBalancer` Service on bare metal just sits with its address stuck at `` forever. **MetalLB** is that help: software that runs inside your cluster, watches for `LoadBalancer` Services, and hands them a real address from a range *you* configure — then makes that address actually reachable on your network. It's the standard, widely-used answer to this exact gap, which is why it's installed as a pre-installed app in this repo rather than left as a "next step." `apps/demo/service.yaml` is already `type: LoadBalancer` — that's what you saw sitting at `` if you looked closely in the last doc. This doc is what turns that into a real, reachable address. ## What's already running **`apps/metallb.yaml`** → one Application, pointing at `apps/metallb`, a Helm chart that does two things at once (see [`apps/metallb/Chart.yaml`](../apps/metallb/Chart.yaml)): - Pulls in the official upstream MetalLB chart as a dependency — this is also the real, working example of a Helm-based app mentioned in [`docs/08-adding-new-apps.md`](08-adding-new-apps.md). - Carries two templates of its own ([`apps/metallb/templates/`](../apps/metallb/templates)): an `IPAddressPool` and an `L2Advertisement` — MetalLB's own custom resources, which tell it which addresses it's allowed to use and how to advertise them. Right now these still contain **placeholder** IP addresses that won't work on your network — that's what you're about to fix. Both the upstream chart and our two extra resources sync together as one Application. Look at the `sync-wave` annotation on [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) — it's what keeps this safe despite being one sync: MetalLB's own resources (including its CRDs) apply first, and Argo CD waits for them to be healthy before applying our two resources, which depend on those CRDs existing. ## Answer three questions before editing anything > **Ask Claude:** literally ask — "help me pick MetalLB's IP range and the > addresses for Argo CD and the demo app." This is the one part of this > whole repo that depends entirely on *your* network, which nothing in > this repo can know in advance. Have your router's admin page or DHCP > settings handy; Claude will ask what your LAN's subnet is and what range > your DHCP server already hands out, so the pool it suggests doesn't > collide with anything. You need three decisions: 1. **The pool range** — a handful of addresses (5–10 is plenty for this repo) on your LAN's subnet that your DHCP server will never hand to anything else. Commonly a slice just above or below your DHCP range, e.g. if your router hands out `.100`–`.199`, something like `.240`–`.250` is a safe, unused slice. 2. **Argo CD's UI address** — one address from that pool. 3. **The demo app's address** — a different address from that pool. ## Edit the placeholders 1. [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) — replace the `addresses` range with your chosen pool. 2. [`apps/demo/service.yaml`](../apps/demo/service.yaml) — replace the `metallb.universe.tf/loadBalancerIPs` annotation with the demo app's chosen address (must be inside the pool from step 1). 3. [`docs/network.md`](network.md) — record the pool range and the demo app's address in the tables there (Argo CD's address goes in once you've picked it below). 4. Commit and push all three: ```sh git add apps/metallb/templates/ipaddresspool.yaml apps/demo/service.yaml docs/network.md git commit -m "Configure MetalLB's address pool for my network" git push ``` 5. Force a sync rather than waiting for the next poll: ```sh argocd app sync metallb demo ``` ## Verify ```sh kubectl -n demo get svc demo ``` `EXTERNAL-IP` should now show the address you chose instead of ``. Visit `http://` in a browser — same "It works!" page as before, now on its own real IP instead of a NodePort. ```sh kubectl -n metallb-system get ipaddresspools,l2advertisements ``` Confirms your pool and advertisement were applied as expected. ## Give Argo CD's UI a real address too So far you've reached the Argo CD UI through `kubectl port-forward` (see [`docs/03-installing-argocd.md`](03-installing-argocd.md)). Now that MetalLB is running, you can give it a permanent address instead. Argo CD's own install isn't managed through this repo's GitOps loop (it's the imperative bootstrap step — see `docs/03`), so this one change is applied directly rather than through Git: ```sh kubectl -n argocd patch svc argocd-server --type merge -p \ '{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.universe.tf/loadBalancerIPs":""}}}' ``` Replace `` with the address you chose in step 2 above. Then: ```sh kubectl -n argocd get svc argocd-server ``` Once `EXTERNAL-IP` shows your chosen address, visit `https://` directly — no port-forward needed from now on. (You'll still hit the same self-signed certificate warning as before; see [`docs/03-installing-argocd.md`](03-installing-argocd.md) for why that's expected here.) Update [`docs/network.md`](network.md) one more time with this address — commit and push it. This one's a genuine exception to "everything's driven by Git": the patch command above isn't a file in this repo, so this file is the only place this address gets recorded at all. > **Ask Claude:** "why did the demo app get its LoadBalancer IP through > Git, but Argo CD's didn't?" — a good check that the boundary between > "managed by this repo" and "the imperative bootstrap that starts it" > has actually landed. ## Checklist - [ ] Chosen a pool range, an Argo CD UI address, and a demo app address, with Claude's help, that fit your actual network - [ ] `apps/metallb/templates/ipaddresspool.yaml` edited to your real range - [ ] `apps/demo/service.yaml` edited to your real demo address - [ ] `docs/network.md` updated with the pool range and both addresses - [ ] Committed, pushed, synced - [ ] `kubectl -n demo get svc demo` shows a real `EXTERNAL-IP`, page loads in a browser - [ ] `argocd-server` patched to `LoadBalancer`, reachable at its own address without port-forwarding Continue to [`docs/06-longhorn.md`](06-longhorn.md).