Network Setup: MetalLB + Traefik

The Problem

Kubernetes on bare metal lacks cloud LoadBalancer support. Without it:

MetalLB to the Rescue

MetalLB provides LoadBalancer implementation for bare metal.

Installation

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml

Configuration

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.200-192.168.1.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default-pool

Now services with type: LoadBalancer get real IPs from this pool.

How It Works

  1. Service created with type: LoadBalancer
  2. MetalLB assigns IP from pool
  3. L2 mode announces IP via ARP
  4. Local network routes traffic to assigned IP
  5. MetalLB forwards to service endpoints

Example Service

apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: my-app

Result:

kubectl get svc my-app
NAME     TYPE           EXTERNAL-IP      PORT(S)
my-app   LoadBalancer   192.168.1.200    80:30123/TCP

Accessible at http://192.168.1.200 from any device on the network.

Traefik Ingress Controller

LoadBalancers use one IP per service. For HTTP(S) apps, use Ingress:

Installation

apiVersion: v1
kind: Namespace
metadata:
  name: traefik
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traefik
  namespace: traefik
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traefik
  template:
    metadata:
      labels:
        app: traefik
    spec:
      serviceAccountName: traefik
      containers:
      - name: traefik
        image: traefik:v2.10
        ports:
        - name: web
          containerPort: 80
        - name: websecure
          containerPort: 443
        - name: admin
          containerPort: 8080
        args:
        - --api.insecure=true
        - --providers.kubernetesingress=true
        - --entrypoints.web.address=:80
        - --entrypoints.websecure.address=:443
---
apiVersion: v1
kind: Service
metadata:
  name: traefik
  namespace: traefik
spec:
  type: LoadBalancer
  ports:
  - name: web
    port: 80
    targetPort: 80
  - name: websecure
    port: 443
    targetPort: 443
  selector:
    app: traefik

Traefik gets 192.168.1.200 from MetalLB.

Using Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog
  namespace: default
spec:
  rules:
  - host: lab.tomarc.dev
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: blog
            port:
              number: 80
  - host: console.tomarc.dev
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: console
            port:
              number: 3000

Both services accessible via same IP, different hostnames.

DNS Configuration

Point domains to LoadBalancer IP:

lab.tomarc.dev      A    192.168.1.200
console.tomarc.dev  A    192.168.1.200
*.lab.tomarc.dev    A    192.168.1.200

Traefik routes based on Host header.

Network Architecture

Internet


Home Router (192.168.1.1)

   ├─ Mac (192.168.1.x) - Dev machine
   ├─ iDRAC (192.168.1.190) - Management
   ├─ R430 Node (192.168.1.100) - Kubernetes

   └─ MetalLB Pool (192.168.1.200-220)

      ├─ Traefik (192.168.1.200) - HTTP/HTTPS
      ├─ Console API (192.168.1.201) - Management
      └─ Future services (192.168.1.202+)

Pod Networking (Flannel)

Pods get IPs from internal network:

cluster:
  network:
    cni:
      name: flannel
    dnsDomain: cluster.local
    podSubnets:
      - 10.244.0.0/16
    serviceSubnets:
      - 10.96.0.0/12

Flannel handles pod-to-pod communication across nodes (single node for now, but ready to scale).

Service Types Comparison

ClusterIP (default)

NodePort

LoadBalancer

Ingress

Exposing to Internet

Option 1: Port Forward at Router

Router Port 80 → 192.168.1.200:80 (Traefik)
Router Port 443 → 192.168.1.200:443 (Traefik)

All HTTP traffic to your public IP goes to Traefik, which routes internally.

Option 2: Cloudflare Tunnel

Zero port forwarding:

cloudflared tunnel create homelab
cloudflared tunnel route dns homelab lab.tomarc.dev
cloudflared tunnel run homelab

Secure tunnel from Cloudflare to your Traefik. No firewall changes needed.

Option 3: Tailscale

VPN mesh network:

# On cluster
kubectl apply -f tailscale-operator.yaml

# Access from anywhere
curl http://console.tom-lab.ts.net

Secure, no public exposure.

Firewall Rules

Keep it simple:

Kubernetes NetworkPolicies for internal segmentation (future).

Performance

MetalLB L2 mode:

Traefik:

Monitoring

Check LoadBalancer assignment:

kubectl get svc --all-namespaces | grep LoadBalancer

Traefik dashboard:

kubectl port-forward -n traefik svc/traefik 8080:8080
# Visit http://localhost:8080/dashboard/

MetalLB logs:

kubectl logs -n metallb-system -l app=metallb

Troubleshooting

Service stuck in Pending

Ingress not routing

Can’t reach from external


This network setup provides a solid foundation. Simple, scalable, and follows Kubernetes best practices.

Previous: Architecture Overview