Files
k8s-starter-pack/docs/05-metallb.md
T

142 lines
5.1 KiB
Markdown
Raw Normal View History

2026-09-03 02:19:46 +01:00
# 05 — MetalLB: routing real service addresses with BGP
The app-of-apps from [`docs/04-app-of-apps.md`](04-app-of-apps.md) installs
MetalLB alongside the demo. This cluster uses a dedicated service network,
`10.10.60.0/24`, instead of borrowing addresses from the nodes'
`10.10.10.0/24` LAN.
## Why this is needed
A Kubernetes Service of `type: LoadBalancer` asks for an address reachable
from outside the cluster. A cloud platform would allocate and route one, but
bare-metal Kubernetes has no external load-balancer service. Without one, the
Service remains at `<pending>`.
**MetalLB** fills that gap. It allocates an address from a controlled pool and
advertises a route for it with **BGP (Border Gateway Protocol)**. BGP is how
the Kubernetes workers tell the MikroTik router, "this service address is
reachable through me."
## This cluster's routing design
```text
MikroTik RB5009
address: 10.10.10.1
ASN: 64512
│
│ eBGP, two equal-cost sessions
├──────────────────────┐
▼ ▼
worker01 / Gimli worker02 / Titus
10.10.10.203 10.10.10.204
ASN 64520 ASN 64520
└──────────┬───────────┘
│ advertise allocated /32 routes
▼
MetalLB pool: 10.10.60.10-10.10.60.250
```
An **ASN (Autonomous System Number)** identifies one side of a BGP
relationship. These are private ASNs used only inside this network.
The router has `multipath=2`, enabling **ECMP (Equal-Cost Multi-Path)**: when
both workers advertise the same service route with equal attributes, the
router can use both paths. If either physical Proxmox host fails, its route is
withdrawn and the surviving worker remains.
## What the Git configuration contains
[`apps/metallb/`](../apps/metallb/) is an umbrella Helm chart. It installs the
official MetalLB chart and three cluster-specific resources:
- [`ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) permits
allocation from `10.10.60.10-10.10.60.250`.
- [`bgppeer.yaml`](../apps/metallb/templates/bgppeer.yaml) creates sessions
from only the two labelled worker nodes to `10.10.10.1`.
- [`bgpadvertisement.yaml`](../apps/metallb/templates/bgpadvertisement.yaml)
advertises each allocated service address as an individual `/32` route.
Advertising `/32`s matters: the router learns only addresses currently owned
by Services, rather than sending the entire `/24` toward the cluster.
The resources use Argo CD sync wave `1`; MetalLB's chart and CRDs apply in the
default wave first. A **CRD (Custom Resource Definition)** teaches Kubernetes
resource types such as `BGPPeer` before those objects are created.
## MikroTik configuration
The RB5009 already has two passive RouterOS 7.23.3 connections: `.203` and
`.204` initiate sessions from ASN `64520` to router ASN `64512`. Its input
filter accepts only `/32` routes inside the dedicated service prefix:
```routeros
/routing/filter/rule
add chain=cilium-bgp-in rule="if (dst in 10.10.60.0/24 && dst-len==32) { accept }"
add chain=cilium-bgp-out rule="if (dst-len>=0) { reject }"
```
The existing names retain `cilium-` from the previous cluster, but names are
only labels; they do not couple the peers to a CNI implementation. No BGP
password is used. Never add one to these manifests because this repository is
public.
## Verify after the app-of-apps syncs
Check MetalLB's resources:
2026-08-22 17:30:45 +01:00
```sh
2026-09-03 02:19:46 +01:00
kubectl -n metallb-system get ipaddresspools,bgppeers,bgpadvertisements
2026-08-22 17:30:45 +01:00
```
2026-09-03 02:19:46 +01:00
On the MikroTik, both sessions should be established:
```routeros
/routing/bgp/session/print detail without-paging
```
The demo requests `10.10.60.11` in
[`apps/demo/service.yaml`](../apps/demo/service.yaml):
2026-08-22 17:30:45 +01:00
```sh
2026-09-03 02:19:46 +01:00
kubectl -n demo get svc demo
2026-08-22 17:30:45 +01:00
```
2026-09-03 02:19:46 +01:00
Its `EXTERNAL-IP` should become `10.10.60.11`. The router should learn a host
route for it, and `http://10.10.60.11` should show the demo page.
2026-08-22 17:30:45 +01:00
2026-09-03 02:19:46 +01:00
## Give Argo CD a routed address
Argo CD itself was installed imperatively rather than through this repository,
so its Service is patched directly. The address is already reserved in
[`docs/network.md`](network.md):
2026-08-22 17:30:45 +01:00
```sh
kubectl -n argocd patch svc argocd-server --type merge -p \
2026-09-03 02:19:46 +01:00
'{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.io/loadBalancerIPs":"10.10.60.10"}}}'
2026-08-22 17:30:45 +01:00
```
2026-09-03 02:19:46 +01:00
Verify:
2026-08-22 17:30:45 +01:00
```sh
kubectl -n argocd get svc argocd-server
```
2026-09-03 02:19:46 +01:00
Once `EXTERNAL-IP` is `10.10.60.10`, visit `https://10.10.60.10`. Argo CD's
default certificate is self-signed, so a browser warning is expected.
2026-08-22 17:30:45 +01:00
2026-09-03 02:19:46 +01:00
> **Ask Claude:** why does the demo's address live in Git while Argo CD's is
> applied by hand? The answer is the bootstrap boundary: Argo CD manages the
> demo, but its own initial installation remains outside its GitOps loop.
2026-08-22 17:30:45 +01:00
## Checklist
2026-09-03 02:19:46 +01:00
- [ ] MetalLB Application is `Synced` and `Healthy`
- [ ] Both MikroTik BGP sessions are established
- [ ] Router accepts only `10.10.60.0/24` `/32` routes from the workers
- [ ] Demo Service has `EXTERNAL-IP` `10.10.60.11`
- [ ] `http://10.10.60.11` loads
- [ ] Argo CD Service has `EXTERNAL-IP` `10.10.60.10`
- [ ] `https://10.10.60.10` reaches Argo CD
2026-08-22 17:30:45 +01:00
Continue to [`docs/06-longhorn.md`](06-longhorn.md).