A teaching-first, single-cluster GitOps setup: Talos Linux + Argo CD with an app-of-apps layout, MetalLB pre-installed for real LoadBalancer IPs on bare metal, a demo app, and a progressive docs/ walkthrough with a CLAUDE.md tutor persona and guided bootstrap checklist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.9 KiB
05 — MetalLB: getting real IP addresses on bare metal
The app-of-apps you just applied (docs/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
<pending> 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 <pending> 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):
- 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/07-adding-new-apps.md. - Carries two templates of its own
(
apps/metallb/templates/): anIPAddressPooland anL2Advertisement— 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 —
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:
- 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–.250is a safe, unused slice. - Argo CD's UI address — one address from that pool.
- The demo app's address — a different address from that pool.
Edit the placeholders
apps/metallb/templates/ipaddresspool.yaml— replace theaddressesrange with your chosen pool.apps/demo/service.yaml— replace themetallb.universe.tf/loadBalancerIPsannotation with the demo app's chosen address (must be inside the pool from step 1).docs/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).- Commit and push all three:
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 - Force a sync rather than waiting for the next poll:
argocd app sync metallb demo
Verify
kubectl -n demo get svc demo
EXTERNAL-IP should now show the address you chose instead of <pending>.
Visit http://<that-address> in a browser — same "It works!" page as
before, now on its own real IP instead of a NodePort.
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). 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:
kubectl -n argocd patch svc argocd-server --type merge -p \
'{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.universe.tf/loadBalancerIPs":"<ARGOCD_IP>"}}}'
Replace <ARGOCD_IP> with the address you chose in step 2 above. Then:
kubectl -n argocd get svc argocd-server
Once EXTERNAL-IP shows your chosen address, visit
https://<that-address> 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 for why that's
expected here.)
Update docs/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.yamledited to your real rangeapps/demo/service.yamledited to your real demo addressdocs/network.mdupdated with the pool range and both addresses- Committed, pushed, synced
kubectl -n demo get svc demoshows a realEXTERNAL-IP, page loads in a browserargocd-serverpatched toLoadBalancer, reachable at its own address without port-forwarding
Continue to docs/06-your-first-change.md.