惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
小众软件
小众软件
I
InfoQ
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Martin Fowler
Martin Fowler
月光博客
月光博客
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
V
Visual Studio Blog
博客园 - 叶小钗
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
GbyAI
GbyAI
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Bare-Metal LoadBalancer on K3s: MetalLB + Traefik with Ar...
david · 2026-06-14 · via DEV Community

david

Originally published at woitzik.dev

Cloud Kubernetes clusters get LoadBalancers for free. You create a Service of type LoadBalancer, and within seconds your cloud provider hands you a public IP. On a bare-metal K3s cluster running on Proxmox VMs, that request hangs in <pending> forever.

This is the first thing that breaks every homelab Kubernetes setup. MetalLB fixes it — but wiring it up correctly with Traefik and ArgoCD has a few non-obvious steps.

View the complete homelab infrastructure source on GitHub 🐙

Why <pending> Happens

Kubernetes delegates LoadBalancer service provisioning to the underlying cloud provider. On bare metal, there is no cloud provider — so the controller just waits forever for an external IP that never comes.

MetalLB fills this gap by implementing a software load balancer that integrates directly with the Kubernetes API. In L2 mode, it responds to ARP requests for the assigned IP on your local network, making the service reachable like any other device on the subnet.

The Architecture

External Request (10.0.20.200)
        │
        ▼
MetalLB (L2 ARP — announces IP on VLAN 20)
        │
        ▼
Traefik Service (LoadBalancer type)
        │
        ├── Ingress: auth.yourdomain.com → Authelia
        ├── Ingress: vault.yourdomain.com → Vaultwarden
        └── Ingress: *.yourdomain.com → Wildcard TLS

MetalLB owns the IP. Traefik owns the routing. ArgoCD owns both.

Step 1: Deploy MetalLB via ArgoCD

MetalLB ships as a Helm chart. The ArgoCD Application is minimal:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: metallb
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://metallb.github.io/metallb
    targetRevision: 0.14.3
    chart: metallb
  destination:
    server: https://kubernetes.default.svc
    namespace: metallb-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

This deploys the MetalLB controller and speaker pods but does not configure any IP pools yet. The configuration lives in a separate ArgoCD Application pointing at your Git repository — this is the App-of-Apps pattern in action.

Step 2: Configure the IP Pool

MetalLB configuration is done via Kubernetes CRDs after v0.13. Two resources are required: an IPAddressPool that defines the range, and an L2Advertisement that tells MetalLB to announce those IPs via ARP on the local network.

# kubernetes/system/metallb-config/pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.0.20.200-10.0.20.240
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool

The range 10.0.20.200-10.0.20.240 sits inside VLAN 20 (the server VLAN) but outside the DHCP range — so no address conflicts with dynamically assigned devices.

The separate ArgoCD Application that applies this config:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: metallb-config
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/dwoitzik/homelab-infrastructure.git
    targetRevision: HEAD
    path: kubernetes/system/metallb-config
  destination:
    server: https://kubernetes.default.svc
    namespace: metallb-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Splitting the Helm chart deployment from the CRD configuration into two separate Applications is intentional. MetalLB CRDs must exist before the configuration resources can be applied — ArgoCD's sync waves handle this ordering, but keeping them separate makes the dependency explicit and avoids race conditions on fresh cluster installs.

Step 3: Traefik as the Ingress Controller

K3s ships with Traefik by default, but managing it via ArgoCD gives you version control and declarative configuration. We disable the built-in K3s Traefik and deploy our own:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: traefik
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://helm.traefik.io/traefik
    targetRevision: 27.0.2
    chart: traefik
    helm:
      values: |
        ports:
          web:
            redirectTo:
              port: websecure
          websecure:
            tls:
              enabled: true
        ingressRoute:
          dashboard:
            enabled: false
        tlsStore:
          default:
            defaultCertificate:
              secretName: wildcard-yourdomain-tls
  destination:
    server: https://kubernetes.default.svc
    namespace: kube-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Three deliberate decisions here:

web.redirectTo: websecure — all HTTP traffic is immediately redirected to HTTPS at the ingress level. No application needs to handle this itself.

tlsStore.default.defaultCertificate — sets the wildcard certificate as the default TLS certificate for all Ingress resources that don't specify their own. Every service behind Traefik gets HTTPS automatically.

dashboard.enabled: false — the Traefik dashboard exposes routing configuration and middleware details. Disabled in production-adjacent setups; access it via kubectl port-forward when needed.

Step 4: Verify MetalLB Assignment

After deploying both Applications and syncing, check that Traefik received an external IP:

kubectl get svc -n kube-system traefik

Expected output:

NAME      TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)
traefik   LoadBalancer   10.96.x.x      10.0.20.200    80:xxx/TCP,443:xxx/TCP

If EXTERNAL-IP shows <pending>, MetalLB is not yet running or the IP pool is not configured. Check the MetalLB speaker logs:

kubectl logs -n metallb-system -l component=speaker

The Result

Once MetalLB and Traefik are running:

  • Any Service of type LoadBalancer in the cluster gets a real IP from the 10.0.20.200-10.0.20.240 pool
  • Any Ingress resource gets automatic HTTPS via the wildcard certificate
  • HTTP is redirected to HTTPS at the edge — no application configuration needed
  • Everything is GitOps-managed — a git push is the only deployment mechanism

Point your DNS wildcard (*.yourdomain.com) at 10.0.20.200 and every Ingress you create is immediately reachable with valid TLS.


The same principles — centralized ingress, TLS termination at the edge, declarative configuration — apply directly to enterprise Azure environments. In Azure, the equivalent is an Application Gateway or Azure Firewall in front of a private AKS cluster. If you are building that for a regulated environment, the Enterprise Terraform Blueprints cover the network isolation layer.