{"id":1702,"date":"2026-09-23T11:59:45","date_gmt":"2026-09-23T14:59:45","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1702"},"modified":"2026-09-23T11:59:45","modified_gmt":"2026-09-23T14:59:45","slug":"metallb-loadbalancer-kubernetes-bare-metal-layer2-bgp","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/09\/metallb-loadbalancer-kubernetes-bare-metal-layer2-bgp\/","title":{"rendered":"MetalLB: LoadBalancer for bare metal Kubernetes with Layer 2 and BGP"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/metallb-v1.webp\" alt=\"Mascote LinuxPro encaixando uma esfera de luz azul, o IP virtual, num rack de servidores bare metal ligado a um roteador, sob o logo do MetalLB, com o cachorro caramelo cyborg sentado ao lado\" width=\"1486\" height=\"856\" \/><\/p>\n<p>Crie um Service do tipo <code>LoadBalancer<\/code> num cluster Kubernetes na AWS ou no Google Cloud e, em segundos, ele ganha um IP externo. Fa\u00e7a o mesmo num cluster em servidores pr\u00f3prios e o <code>EXTERNAL-IP<\/code> fica em <code>&lt;pending&gt;<\/code> para sempre. O Kubernetes n\u00e3o traz um balanceador de carga de rede para bare metal: as implementa\u00e7\u00f5es que acompanham o projeto s\u00f3 conversam com as nuvens. O <strong>MetalLB<\/strong> preenche esse buraco. Ele distribui IPs de um pool que voc\u00ea define e anuncia esses IPs na rede com protocolos padr\u00e3o, ARP\/NDP ou BGP. Neste guia, voc\u00ea vai entender como ele funciona, instalar a vers\u00e3o 0.16.1, configurar os modos Layer 2 e BGP e resolver os problemas mais comuns. Todos os comandos foram testados num laborat\u00f3rio com kind e um roteador FRR.<\/p>\n<p><!-- more --><\/p>\n<h2>O problema: LoadBalancer em &lt;pending&gt;<\/h2>\n<p>Num cluster bare metal sem MetalLB, o sintoma \u00e9 este:<\/p>\n<pre><code class=\"language-bash\">kubectl create deployment nginx --image=nginx:1.29 --replicas=2\nkubectl expose deployment nginx --type=LoadBalancer --port=80\nkubectl get svc nginx<\/code><\/pre>\n<pre><code class=\"language-text\">NAME    TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE\nnginx   LoadBalancer   10.96.187.200   &lt;pending&gt;     80:32251\/TCP   5s<\/code><\/pre>\n<p>Sem um balanceador, sobram duas sa\u00eddas ruins. <code>NodePort<\/code> exp\u00f5e o servi\u00e7o numa porta alta (30000 a 32767) de todos os n\u00f3s, e o cliente precisa saber o IP de um n\u00f3 que esteja de p\u00e9. <code>externalIPs<\/code> depende de voc\u00ea rotear o IP manualmente at\u00e9 algum n\u00f3. A <a href=\"https:\/\/metallb.io\/\">documenta\u00e7\u00e3o do MetalLB<\/a> resume: essas op\u00e7\u00f5es transformam o bare metal em cidad\u00e3o de segunda classe no Kubernetes.<\/p>\n<h2>O que \u00e9 o MetalLB<\/h2>\n<p>O <a href=\"https:\/\/github.com\/metallb\/metallb\">MetalLB<\/a> \u00e9 uma implementa\u00e7\u00e3o de balanceador de carga de rede para clusters Kubernetes bare metal, escrita em Go, sob licen\u00e7a <strong>Apache 2.0<\/strong>. O reposit\u00f3rio existe desde 2017, tem cerca de 8,4 mil estrelas e o projeto \u00e9 <em>sandbox<\/em> da Cloud Native Computing Foundation. A API ainda \u00e9 marcada como beta, mas a pr\u00f3pria documenta\u00e7\u00e3o afirma que o MetalLB \u00e9 est\u00e1vel e confi\u00e1vel em produ\u00e7\u00e3o. Se voc\u00ea quer entender de onde vem o modelo de Services e controladores, veja <a href=\"\/2026\/09\/a-historia-do-kubernetes\">a hist\u00f3ria do Kubernetes<\/a>.<\/p>\n<p>Ele faz duas coisas que trabalham juntas:<\/p>\n<ul>\n<li><strong>Atribui\u00e7\u00e3o de endere\u00e7os<\/strong>: o <code>controller<\/code>, um Deployment \u00fanico no cluster, pega um IP livre de um <code>IPAddressPool<\/code> e grava no Service. Ele n\u00e3o inventa IPs: s\u00f3 entrega os que voc\u00ea colocou nos pools, sejam p\u00fablicos alugados do seu datacenter ou privados da sua LAN.<\/li>\n<li><strong>An\u00fancio externo<\/strong>: o <code>speaker<\/code>, um DaemonSet que roda em cada n\u00f3, faz a rede fora do cluster saber que aquele IP &#8220;mora&#8221; no cluster. Em modo Layer 2 ele responde ARP (IPv4) e NDP (IPv6); em modo BGP ele fecha sess\u00f5es com seus roteadores e anuncia rotas.<\/li>\n<\/ul>\n<p>Depois que o pacote chega ao n\u00f3, o trabalho do MetalLB acabou. Dali em diante quem encaminha at\u00e9 o pod \u00e9 o <code>kube-proxy<\/code> e o plugin de rede (CNI) do cluster.<\/p>\n<h3>Requisitos<\/h3>\n<ul>\n<li>Kubernetes 1.13 ou mais novo, sem outro balanceador de rede instalado.<\/li>\n<li>Um plugin de rede compat\u00edvel; a <a href=\"https:\/\/metallb.io\/installation\/network-addons\/\">p\u00e1gina de compatibilidade<\/a> marca Cilium, Flannel, Antrea e Canal como compat\u00edveis, e Calico e kube-router como compat\u00edveis com ressalvas.<\/li>\n<li>Alguns endere\u00e7os IPv4 livres para o MetalLB distribuir.<\/li>\n<li>No modo BGP, um ou mais roteadores que falem BGP.<\/li>\n<li>No modo Layer 2, a porta <strong>7946 TCP e UDP<\/strong> liberada entre os n\u00f3s, usada pelo <a href=\"https:\/\/github.com\/hashicorp\/memberlist\">memberlist<\/a> para detectar n\u00f3s fora do ar.<\/li>\n<\/ul>\n<p>Em nuvem p\u00fablica, a maioria dos provedores n\u00e3o funciona com o MetalLB, porque a rede virtual n\u00e3o aceita IPs anunciados pela VM. A <a href=\"https:\/\/metallb.io\/installation\/clouds\/\">p\u00e1gina de compatibilidade com nuvens<\/a> detalha cada caso. No OpenStack funciona, desde que voc\u00ea libere os IPs na prote\u00e7\u00e3o antispoofing da porta.<\/p>\n<h2>Modo Layer 2: simples e universal<\/h2>\n<p>No modo Layer 2, um \u00fanico n\u00f3 do cluster \u00e9 eleito &#8220;dono&#8221; de cada IP de servi\u00e7o. Quando algu\u00e9m na rede pergunta via ARP quem tem aquele IP, o <code>speaker<\/code> desse n\u00f3 responde com o MAC da pr\u00f3pria interface. Para a LAN, parece apenas que a m\u00e1quina tem v\u00e1rios IPs. Funciona em qualquer rede Ethernet, sem equipamento especial.<\/p>\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/metallb-modo-l2.webp\" alt=\"Diagrama do modo Layer 2: o cliente pergunta via ARP quem tem o IP 192.168.47.200, o speaker do n\u00f3 l\u00edder responde com seu MAC, todo o tr\u00e1fego entra pelo l\u00edder e o kube-proxy distribui entre os pods; outro n\u00f3 espera para assumir se o l\u00edder cair\" width=\"1200\" height=\"660\" loading=\"lazy\" \/><\/p>\n<p>Isso tem duas consequ\u00eancias que voc\u00ea precisa conhecer:<\/p>\n<ul>\n<li><strong>N\u00e3o \u00e9 balanceamento entre n\u00f3s.<\/strong> Todo o tr\u00e1fego de um IP entra por um n\u00f3 s\u00f3, e a banda de entrada fica limitada \u00e0 interface dele. O que o modo L2 oferece \u00e9 <em>failover<\/em>: se o l\u00edder cair, outro n\u00f3 assume o IP.<\/li>\n<li><strong>O failover depende dos clientes.<\/strong> O n\u00f3 que assume envia pacotes ARP &#8220;gratuitos&#8221; avisando o novo MAC. Sistemas modernos (Linux, Windows, macOS) atualizam o cache na hora, e a troca leva poucos segundos. Equipamentos antigos ou com implementa\u00e7\u00e3o ruim podem demorar mais.<\/li>\n<\/ul>\n<p>A elei\u00e7\u00e3o do l\u00edder n\u00e3o guarda estado: cada <code>speaker<\/code> calcula, para cada IP, uma lista ordenada pelo hash de &#8220;n\u00f3 + IP&#8221; entre os n\u00f3s eleg\u00edveis, e quem estiver em primeiro anuncia. Adicionar um n\u00f3 s\u00f3 muda o l\u00edder se o novo n\u00f3 cair no topo da lista; remover um n\u00f3 que n\u00e3o \u00e9 l\u00edder n\u00e3o muda nada.<\/p>\n<p>Quem conhece o Keepalived vai achar familiar: do ponto de vista do cliente, o IP &#8220;pula&#8221; de uma m\u00e1quina para outra. A diferen\u00e7a \u00e9 que o MetalLB n\u00e3o usa VRRP, e sim o memberlist. Por isso n\u00e3o existe o limite de 255 roteadores virtuais por rede nem IDs de roteador virtual para configurar. Em compensa\u00e7\u00e3o, ele n\u00e3o conversa com equipamentos VRRP de terceiros.<\/p>\n<h2>Modo BGP: balanceamento de verdade<\/h2>\n<p>No modo BGP, cada n\u00f3 fecha uma sess\u00e3o BGP com os roteadores da sua rede e anuncia o IP de cada servi\u00e7o como uma rota <code>\/32<\/code>. Se o roteador estiver configurado para m\u00faltiplos caminhos (ECMP), ele trata todos os n\u00f3s como pr\u00f3ximos saltos equivalentes e divide as conex\u00f5es entre eles.<\/p>\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/metallb-modo-bgp.webp\" alt=\"Diagrama do modo BGP: cada n\u00f3 do cluster fecha uma sess\u00e3o BGP com o roteador e anuncia 10.200.0.1\/32; o roteador instala a rota com dois pr\u00f3ximos saltos e divide o tr\u00e1fego dos clientes entre os n\u00f3s\" width=\"1200\" height=\"640\" loading=\"lazy\" \/><\/p>\n<p>A divis\u00e3o \u00e9 <strong>por conex\u00e3o<\/strong>, com hash de campos do pacote: todos os pacotes de uma conex\u00e3o TCP v\u00e3o para o mesmo n\u00f3. Hash de 5 campos (protocolo, IPs e portas de origem e destino) espalha melhor do que o de 3, porque separa conex\u00f5es diferentes do mesmo cliente.<\/p>\n<p>A limita\u00e7\u00e3o principal: quando o conjunto de n\u00f3s muda, por exemplo porque um n\u00f3 caiu, o roteador recalcula o hash e a maioria das conex\u00f5es ativas vai parar em outro n\u00f3, que n\u00e3o conhece aquela conex\u00e3o. O cliente recebe <em>connection reset<\/em>. \u00c9 um corte \u00fanico, n\u00e3o perda cont\u00ednua. Para suavizar, a documenta\u00e7\u00e3o sugere ECMP &#8220;resiliente&#8221; no roteador, deixar um Ingress Controller entre o BGP e os servi\u00e7os, e fazer mudan\u00e7as em hor\u00e1rio de pouco tr\u00e1fego.<\/p>\n<h3>Os tr\u00eas backends de BGP<\/h3>\n<p>O MetalLB tem tr\u00eas implementa\u00e7\u00f5es de BGP, e a <strong>v0.16.0<\/strong>, de 20 de maio de 2026, mudou qual delas \u00e9 a padr\u00e3o:<\/p>\n<ul>\n<li><strong>FRR-K8s<\/strong> (padr\u00e3o e recomendado): usa o <a href=\"https:\/\/frrouting.org\/\">FRR<\/a> por meio do <a href=\"https:\/\/github.com\/metallb\/frr-k8s\">FRR-K8s<\/a>, que roda como DaemonSet pr\u00f3prio. Traz BFD, BGP sobre IPv6 e permite somar configura\u00e7\u00e3o FRR extra \u00e0s mesmas sess\u00f5es. Novos recursos de BGP s\u00f3 entram aqui.<\/li>\n<li><strong>Nativo<\/strong>: implementa\u00e7\u00e3o pr\u00f3pria, mais leve, sem BFD e sem BGP sobre IPv6. Boa para quem s\u00f3 usa Layer 2 ou BGP simples.<\/li>\n<li><strong>FRR<\/strong> (obsoleto): configura o FRR direto, sem a camada FRR-K8s. Ser\u00e1 removido numa vers\u00e3o futura.<\/li>\n<\/ul>\n<h2>Laborat\u00f3rio: kind com tr\u00eas n\u00f3s<\/h2>\n<p>Para reproduzir tudo sem mexer em produ\u00e7\u00e3o, usamos o <a href=\"https:\/\/kind.sigs.k8s.io\/\">kind<\/a>, que sobe n\u00f3s Kubernetes como cont\u00eaineres Docker. As vers\u00f5es do teste foram kind 0.33.0, Kubernetes 1.37.0 e MetalLB 0.16.1.<\/p>\n<pre><code class=\"language-bash\">cat &gt; kind.yaml &lt;&lt;'EOF'\nkind: Cluster\napiVersion: kind.x-k8s.io\/v1alpha4\nname: metallb-lab\nnodes:\n- role: control-plane\n- role: worker\n- role: worker\nEOF\nkind create cluster --config kind.yaml\ndocker network inspect kind -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}'<\/code><\/pre>\n<p>O \u00faltimo comando mostra a sub-rede da rede <code>kind<\/code> no Docker, que faz o papel da sua LAN. No nosso caso foi <code>192.168.32.0\/20<\/code>, e reservamos o trecho <code>192.168.47.200-192.168.47.250<\/code> para o MetalLB. Na sua rede real, escolha uma faixa fora do DHCP e que nenhum equipamento use.<\/p>\n<p><strong>Armadilha do kind:<\/strong> se algum pod entrar em <code>CrashLoopBackOff<\/code> com <code>too many open files<\/code> no log, o problema \u00e9 o limite de inotify do host, n\u00e3o o MetalLB. Aumente com <code>sudo sysctl fs.inotify.max_user_instances=512<\/code>, como recomenda a documenta\u00e7\u00e3o do kind. Aconteceu no nosso teste com tr\u00eas n\u00f3s num host que j\u00e1 rodava v\u00e1rios cont\u00eaineres.<\/p>\n<h2>Instala\u00e7\u00e3o<\/h2>\n<h3>Prepara\u00e7\u00e3o: kube-proxy em modo IPVS<\/h3>\n<p>Se o seu <code>kube-proxy<\/code> roda em modo IPVS, ative o <code>strictARP<\/code>; sem ele, os n\u00f3s respondem ARP de IPs que n\u00e3o deveriam. Em modo iptables (o padr\u00e3o do kubeadm e do kind), pule este passo.<\/p>\n<pre><code class=\"language-bash\">kubectl get configmap kube-proxy -n kube-system -o yaml | \\\n  sed -e \"s\/strictARP: false\/strictARP: true\/\" | \\\n  kubectl apply -f - -n kube-system<\/code><\/pre>\n<h3>Por manifesto<\/h3>\n<p>O manifesto recomendado inclui o backend FRR-K8s:<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f https:\/\/raw.githubusercontent.com\/metallb\/metallb\/v0.16.1\/config\/manifests\/metallb-frr-k8s.yaml\nkubectl wait -n metallb-system --for=condition=ready pod --all --timeout=240s\nkubectl get pods -n metallb-system<\/code><\/pre>\n<pre><code class=\"language-text\">NAME                                     READY   STATUS    RESTARTS   AGE\ncontroller-75db68c68f-69tsz              1\/1     Running   0          2m\nfrr-k8s-daemon-fvd4w                     5\/5     Running   0          2m\nfrr-k8s-daemon-rdc2p                     5\/5     Running   0          2m\nfrr-k8s-statuscleaner-867ddf64df-h9rzt   1\/1     Running   0          2m\nspeaker-kqhsl                            1\/1     Running   0          2m\nspeaker-mk2jq                            1\/1     Running   0          2m<\/code><\/pre>\n<p>Para uma instala\u00e7\u00e3o mais enxuta, sem FRR, troque por <code>metallb-native.yaml<\/code> na mesma URL. O manifesto j\u00e1 cria o namespace <code>metallb-system<\/code> com os r\u00f3tulos <code>pod-security.kubernetes.io\/*: privileged<\/code>, necess\u00e1rios porque o <code>speaker<\/code> precisa de privil\u00e9gios de rede. Rec\u00e9m-instalado, o MetalLB fica parado: nada acontece at\u00e9 voc\u00ea criar a configura\u00e7\u00e3o.<\/p>\n<h3>Por Helm<\/h3>\n<pre><code class=\"language-bash\">kubectl create namespace metallb-system\nkubectl label namespace metallb-system \\\n  pod-security.kubernetes.io\/enforce=privileged \\\n  pod-security.kubernetes.io\/audit=privileged \\\n  pod-security.kubernetes.io\/warn=privileged\nhelm repo add metallb https:\/\/metallb.github.io\/metallb\nhelm install metallb metallb\/metallb --namespace metallb-system<\/code><\/pre>\n<p>O chart 0.16.1 tamb\u00e9m instala o FRR-K8s por padr\u00e3o. Para usar o backend nativo, passe <code>--set speaker.frr.enabled=false --set frrk8s.enabled=false<\/code>. Com Helm, os recursos de configura\u00e7\u00e3o precisam ficar no mesmo namespace em que o MetalLB foi instalado.<\/p>\n<p>Quem vem de vers\u00f5es antigas: at\u00e9 a v0.12 a configura\u00e7\u00e3o era um ConfigMap. Desde a v0.13 s\u00f3 existem os recursos customizados (CRDs) mostrados abaixo, e h\u00e1 uma <a href=\"https:\/\/metallb.io\/configuration\/migration_to_crds\/\">ferramenta de convers\u00e3o<\/a>.<\/p>\n<h2>Configurando o modo Layer 2<\/h2>\n<p>S\u00e3o dois recursos: o pool de endere\u00e7os e o an\u00fancio em Layer 2 que aponta para ele.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta1\nkind: IPAddressPool\nmetadata:\n  name: lan\n  namespace: metallb-system\nspec:\n  addresses:\n  - 192.168.47.200-192.168.47.250\n---\napiVersion: metallb.io\/v1beta1\nkind: L2Advertisement\nmetadata:\n  name: lan\n  namespace: metallb-system\nspec:\n  ipAddressPools:\n  - lan<\/code><\/pre>\n<p>Os endere\u00e7os aceitam faixas (<code>inicio-fim<\/code>), CIDR (<code>192.168.10.0\/24<\/code>) e IPv6, e voc\u00ea pode ter quantos pools quiser. Um <code>L2Advertisement<\/code> sem <code>ipAddressPools<\/code> vale para todos os pools. <strong>Sem nenhum L2Advertisement, o IP \u00e9 atribu\u00eddo mas n\u00e3o \u00e9 anunciado<\/strong>, e o servi\u00e7o n\u00e3o responde; \u00e9 o erro mais comum de quem vem das vers\u00f5es com ConfigMap.<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f l2.yaml\nkubectl get svc nginx\ncurl -s -o \/dev\/null -w '%{http_code}\\n' http:\/\/192.168.47.200\/\nkubectl describe svc nginx | sed -n '\/Events\/,$p'<\/code><\/pre>\n<pre><code class=\"language-text\">NAME    TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE\nnginx   LoadBalancer   10.96.187.200   192.168.47.200   80:32251\/TCP   4m45s\n200\nEvents:\n  Type    Reason        Age   From                Message\n  ----    ------        ----  ----                -------\n  Normal  IPAllocated   5s    metallb-controller  Assigned IP [\"192.168.47.200\"]\n  Normal  nodeAssigned  5s    metallb-speaker     announcing from node \"metallb-lab-worker2\" with protocol \"layer2\"<\/code><\/pre>\n<p>O Service que estava em <code>&lt;pending&gt;<\/code> ganhou o IP na hora. Os eventos mostram as duas etapas: o <code>controller<\/code> atribuiu o IP e o <code>speaker<\/code> do <code>worker2<\/code> passou a anunci\u00e1-lo. Para ver qual n\u00f3 anuncia cada servi\u00e7o sem ler eventos:<\/p>\n<pre><code class=\"language-bash\">kubectl get servicel2statuses -n metallb-system<\/code><\/pre>\n<pre><code class=\"language-text\">NAME       ALLOCATED NODE        SERVICE NAME   SERVICE NAMESPACE\nl2-vc27l   metallb-lab-worker2   nginx          default<\/code><\/pre>\n<h3>Testando o failover<\/h3>\n<p>No laborat\u00f3rio, derrubamos o n\u00f3 l\u00edder com <code>docker stop<\/code> enquanto um la\u00e7o de <code>curl<\/code> batia no IP a cada meio segundo. O servi\u00e7o voltou a responder em <strong>cerca de 6,3 segundos<\/strong>, e a tabela ARP do host trocou sozinha para o MAC do outro worker:<\/p>\n<pre><code class=\"language-bash\">ip neigh show 192.168.47.200<\/code><\/pre>\n<pre><code class=\"language-text\"># antes: MAC do worker2\n192.168.47.200 dev br-a312a0b87cf9 lladdr a2:7a:02:df:0c:57 REACHABLE\n# depois: MAC do worker\n192.168.47.200 dev br-a312a0b87cf9 lladdr c2:00:6c:c6:41:63 DELAY<\/code><\/pre>\n<p>Um detalhe de teste: <code>docker pause<\/code> n\u00e3o serve para simular a queda, porque o kernel do n\u00f3 pausado continua encaminhando pacotes e o servi\u00e7o nem pisca. Use <code>docker stop<\/code>, ou desligue a m\u00e1quina de verdade.<\/p>\n<h3>Limitando n\u00f3s, interfaces e servi\u00e7os<\/h3>\n<p>Por padr\u00e3o, qualquer n\u00f3 com <code>speaker<\/code> pode ser eleito, e o an\u00fancio sai por todas as interfaces. Quando s\u00f3 alguns n\u00f3s est\u00e3o ligados a uma rede, restrinja:<\/p>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta1\nkind: L2Advertisement\nmetadata:\n  name: dmz\n  namespace: metallb-system\nspec:\n  ipAddressPools:\n  - dmz\n  nodeSelectors:\n  - matchLabels:\n      rede: dmz\n  interfaces:\n  - eth1\n  serviceSelectors:\n  - matchLabels:\n      tier: frontend<\/code><\/pre>\n<p><code>serviceSelectors<\/code> chegou na v0.16.0 e limita o an\u00fancio aos Services com aqueles r\u00f3tulos. Cuidado com <code>interfaces<\/code>: ele n\u00e3o influencia a elei\u00e7\u00e3o do l\u00edder. Se o n\u00f3 eleito n\u00e3o tiver a interface, o servi\u00e7o n\u00e3o \u00e9 anunciado; combine sempre com <code>nodeSelectors<\/code>.<\/p>\n<h2>Configurando o modo BGP<\/h2>\n<p>Para o teste de BGP, recriamos o cluster com dois n\u00f3s (o control-plane, <code>192.168.32.2<\/code>, e um worker, <code>192.168.32.3<\/code>) e subimos um roteador <a href=\"https:\/\/frrouting.org\/\">FRR<\/a> 10.7.1 em cont\u00eainer, na mesma rede do kind, com o AS 64501:<\/p>\n<pre><code class=\"language-bash\">docker run -d --name metallb-lab-router --network kind --ip 192.168.40.10 \\\n  --privileged -v \"$PWD\/router:\/etc\/frr\" quay.io\/frrouting\/frr:10.7.1<\/code><\/pre>\n<p>No diret\u00f3rio <code>router\/<\/code> ficam o arquivo <code>daemons<\/code>, com <code>bgpd=yes<\/code>, e o <code>frr.conf<\/code> com a configura\u00e7\u00e3o do roteador:<\/p>\n<pre><code class=\"language-text\">router bgp 64501\n bgp router-id 192.168.40.10\n no bgp ebgp-requires-policy\n neighbor 192.168.32.2 remote-as 64500\n neighbor 192.168.32.3 remote-as 64500\n !\n address-family ipv4 unicast\n  maximum-paths 8\n exit-address-family<\/code><\/pre>\n<p><code>maximum-paths<\/code> \u00e9 o que liga o ECMP: sem ele, o roteador escolhe um \u00fanico n\u00f3. <code>no bgp ebgp-requires-policy<\/code> \u00e9 necess\u00e1rio no FRR para aceitar rotas eBGP sem pol\u00edtica expl\u00edcita; num roteador de produ\u00e7\u00e3o, prefira filtros de prefixo que aceitem s\u00f3 o seu pool.<\/p>\n<p>Do lado do MetalLB, s\u00e3o tr\u00eas recursos: o par BGP, o pool e o an\u00fancio.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta2\nkind: BGPPeer\nmetadata:\n  name: roteador\n  namespace: metallb-system\nspec:\n  myASN: 64500\n  peerASN: 64501\n  peerAddress: 192.168.40.10\n---\napiVersion: metallb.io\/v1beta1\nkind: IPAddressPool\nmetadata:\n  name: bgp\n  namespace: metallb-system\nspec:\n  addresses:\n  - 10.200.0.0\/24\n  avoidBuggyIPs: true\n---\napiVersion: metallb.io\/v1beta1\nkind: BGPAdvertisement\nmetadata:\n  name: bgp\n  namespace: metallb-system\nspec:\n  ipAddressPools:\n  - bgp<\/code><\/pre>\n<p>Use <code>metallb.io\/v1beta2<\/code> no <code>BGPPeer<\/code>; a <code>v1beta1<\/code> est\u00e1 obsoleta. O <code>avoidBuggyIPs<\/code> n\u00e3o est\u00e1 a\u00ed por acaso: sem ele, o primeiro Service do nosso teste recebeu <code>10.200.0.0<\/code>, e alguns equipamentos antigos descartam endere\u00e7os terminados em <code>.0<\/code> e <code>.255<\/code>. Com a op\u00e7\u00e3o ligada, o Service passou a receber <code>10.200.0.1<\/code>.<\/p>\n<p>No roteador, as sess\u00f5es sobem e a rota aparece com dois pr\u00f3ximos saltos:<\/p>\n<pre><code class=\"language-bash\">docker exec metallb-lab-router vtysh -c \"show bgp summary\"\ndocker exec metallb-lab-router vtysh -c \"show ip route bgp\"<\/code><\/pre>\n<pre><code class=\"language-text\">Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up\/Down State\/PfxRcd   PfxSnt\n192.168.32.2    4      64500         4         6        1    0    0 00:00:15            1        1\n192.168.32.3    4      64500         7         6        1    0    0 00:00:15            1        1\n\nB>* 10.200.0.1\/32 [20\/0] via 192.168.32.2, eth0, weight 1, 00:00:09\n  *                      via 192.168.32.3, eth0, weight 1, 00:00:09<\/code><\/pre>\n<p>Para ver o que cada n\u00f3 pretende anunciar, sem entrar no roteador:<\/p>\n<pre><code class=\"language-bash\">kubectl get servicebgpstatuses -n metallb-system<\/code><\/pre>\n<h3>BFD para detectar falhas mais r\u00e1pido<\/h3>\n<p>O BGP sozinho pode levar dezenas de segundos para perceber um vizinho morto. Nos backends FRR, d\u00e1 para acoplar uma sess\u00e3o BFD ao par:<\/p>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta1\nkind: BFDProfile\nmetadata:\n  name: rapido\n  namespace: metallb-system\nspec:\n  receiveInterval: 380\n  transmitInterval: 270\n---\napiVersion: metallb.io\/v1beta2\nkind: BGPPeer\nmetadata:\n  name: roteador\n  namespace: metallb-system\nspec:\n  myASN: 64500\n  peerASN: 64501\n  peerAddress: 192.168.40.10\n  bfdProfile: rapido<\/code><\/pre>\n<p>O roteador tamb\u00e9m precisa ter BFD ativo para esse vizinho. O <code>BGPPeer<\/code> aceita ainda senha da sess\u00e3o (<code>password<\/code> ou <code>passwordSecret<\/code>), <code>ebgpMultiHop<\/code>, VRF e <em>graceful restart<\/em>, e a <a href=\"https:\/\/metallb.io\/configuration\/_advanced_bgp_configuration\/\">configura\u00e7\u00e3o avan\u00e7ada de BGP<\/a> mostra agrega\u00e7\u00e3o de rotas, <em>local preference<\/em> e comunidades. E um mesmo pool pode ser anunciado por L2 e BGP ao mesmo tempo: basta um <code>L2Advertisement<\/code> e um <code>BGPAdvertisement<\/code> apontando para ele.<\/p>\n<h2>Usando nos Services<\/h2>\n<p>Com o MetalLB configurado, basta <code>type: LoadBalancer<\/code>. As anota\u00e7\u00f5es abaixo d\u00e3o controle fino; todas foram testadas no laborat\u00f3rio.<\/p>\n<h3>IP fixo<\/h3>\n<pre><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-fixo\n  annotations:\n    metallb.io\/loadBalancerIPs: 192.168.47.210\nspec:\n  type: LoadBalancer\n  selector:\n    app: nginx\n  ports:\n  - port: 80<\/code><\/pre>\n<p>O campo <code>spec.loadBalancerIP<\/code> tamb\u00e9m funciona, mas est\u00e1 obsoleto no Kubernetes e n\u00e3o aceita dois IPs. A anota\u00e7\u00e3o aceita uma lista separada por v\u00edrgula, necess\u00e1ria para servi\u00e7os <em>dual stack<\/em>. Se o IP n\u00e3o pertencer a nenhum pool, ou j\u00e1 estiver em uso, o Service fica em <code>&lt;pending&gt;<\/code> e o motivo aparece em <code>kubectl describe svc<\/code>.<\/p>\n<h3>Pool espec\u00edfico e IPs &#8220;caros&#8221;<\/h3>\n<p>Um cen\u00e1rio comum: um pool grande de IPs privados e poucos IPs p\u00fablicos alugados. Marque o pool caro com <code>autoAssign: false<\/code>, e ele s\u00f3 ser\u00e1 usado por quem pedir explicitamente:<\/p>\n<pre><code class=\"language-yaml\">apiVersion: metallb.io\/v1beta1\nkind: IPAddressPool\nmetadata:\n  name: publico\n  namespace: metallb-system\nspec:\n  addresses:\n  - 203.0.113.10\/32\n  autoAssign: false\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: site\n  annotations:\n    metallb.io\/address-pool: publico\nspec:\n  type: LoadBalancer\n  selector:\n    app: site\n  ports:\n  - port: 443<\/code><\/pre>\n<p>Lembre de incluir o pool num <code>L2Advertisement<\/code> ou <code>BGPAdvertisement<\/code>. No nosso teste, um pool esquecido fora do an\u00fancio entregou o IP ao Service, mas o <code>curl<\/code> falhou e o ARP ficou <code>INCOMPLETE<\/code> no host. Para ambientes com v\u00e1rios times, o pool tamb\u00e9m aceita <code>serviceAllocation<\/code>, que o restringe a namespaces ou Services por r\u00f3tulo e define prioridade entre pools.<\/p>\n<h3>Compartilhando um IP entre Services<\/h3>\n<p>Por padr\u00e3o, cada Service ganha seu IP. Para colocar dois Services no mesmo endere\u00e7o, por exemplo DNS em TCP e UDP ou aplica\u00e7\u00f5es em portas diferentes, use a mesma chave de compartilhamento:<\/p>\n<pre><code class=\"language-yaml\">apiVersion: v1\nkind: Service\nmetadata:\n  name: web-http\n  annotations:\n    metallb.io\/allow-shared-ip: \"web\"\n    metallb.io\/loadBalancerIPs: 192.168.47.220\nspec:\n  type: LoadBalancer\n  selector:\n    app: nginx\n  ports:\n  - port: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: web-alt\n  annotations:\n    metallb.io\/allow-shared-ip: \"web\"\n    metallb.io\/loadBalancerIPs: 192.168.47.220\nspec:\n  type: LoadBalancer\n  selector:\n    app: nginx\n  ports:\n  - port: 8080\n    targetPort: 80<\/code><\/pre>\n<p>As condi\u00e7\u00f5es: mesma chave, portas diferentes, e os dois com <code>externalTrafficPolicy: Cluster<\/code> (ou exatamente o mesmo seletor de pods). No laborat\u00f3rio, <code>192.168.47.220:80<\/code> e <code>192.168.47.220:8080<\/code> responderam os dois.<\/p>\n<h3>externalTrafficPolicy: Cluster ou Local<\/h3>\n<p>Essa op\u00e7\u00e3o do Service muda o comportamento do MetalLB:<\/p>\n<ul>\n<li><strong>Cluster<\/strong> (padr\u00e3o): o n\u00f3 que recebe o tr\u00e1fego repassa para qualquer pod do servi\u00e7o, inclusive em outro n\u00f3. A distribui\u00e7\u00e3o entre pods \u00e9 uniforme, mas o pod v\u00ea como origem o IP do n\u00f3, n\u00e3o o do cliente.<\/li>\n<li><strong>Local<\/strong>: o tr\u00e1fego s\u00f3 vai para pods do pr\u00f3prio n\u00f3, e o pod v\u00ea o <strong>IP real do cliente<\/strong>. Em BGP, s\u00f3 os n\u00f3s que t\u00eam pods anunciam a rota. Em L2, s\u00f3 n\u00f3s com pods podem ser eleitos.<\/li>\n<\/ul>\n<p>No teste com BGP, com os dois pods no mesmo worker, bastou mudar para <code>Local<\/code> e a rota encolheu para um pr\u00f3ximo salto:<\/p>\n<pre><code class=\"language-bash\">kubectl patch svc web -p '{\"spec\":{\"externalTrafficPolicy\":\"Local\"}}'\ndocker exec metallb-lab-router vtysh -c \"show ip route bgp\"<\/code><\/pre>\n<pre><code class=\"language-text\">B>* 10.200.0.1\/32 [20\/0] via 192.168.32.3, eth0, weight 1, 00:00:04<\/code><\/pre>\n<p>O custo do <code>Local<\/code> em BGP \u00e9 o desequil\u00edbrio: o roteador divide por n\u00f3, n\u00e3o por pod. Com dois pods no n\u00f3 A e um no n\u00f3 B, cada pod do n\u00f3 A recebe 25% do tr\u00e1fego e o do n\u00f3 B recebe 50%. Use <em>anti-affinity<\/em> para espalhar os pods um por n\u00f3.<\/p>\n<h2>Troubleshooting<\/h2>\n<p>A <a href=\"https:\/\/metallb.io\/troubleshooting\/\">p\u00e1gina de troubleshooting<\/a> parte de uma divis\u00e3o simples: <strong>se o Service n\u00e3o ganha IP, o problema est\u00e1 no <code>controller<\/code>; se ganha IP mas n\u00e3o responde, est\u00e1 nos <code>speaker<\/code>s<\/strong> (ou na rede). Os comandos que mais usamos:<\/p>\n<pre><code class=\"language-bash\">kubectl describe svc &lt;servico&gt;                           # eventos IPAllocated e nodeAssigned\nkubectl logs -n metallb-system deploy\/controller           # atribui\u00e7\u00e3o de IPs\nkubectl logs -n metallb-system -l component=speaker        # an\u00fancios\nkubectl get servicel2statuses,servicebgpstatuses -n metallb-system\nkubectl get configurationstates -n metallb-system          # configura\u00e7\u00e3o v\u00e1lida por componente<\/code><\/pre>\n<h3>Configura\u00e7\u00e3o inv\u00e1lida<\/h3>\n<p>Os webhooks recusam boa parte dos erros na hora. No laborat\u00f3rio, um segundo pool com a mesma faixa foi barrado:<\/p>\n<pre><code class=\"language-text\">admission webhook \"ipaddresspoolvalidationwebhook.metallb.io\" denied the request:\nCIDR \"10.200.0.0\/24\" in pool \"errado\" overlaps with already defined CIDR \"10.200.0.0\/24\"<\/code><\/pre>\n<p>Nem tudo \u00e9 pego pelo webhook, porque a configura\u00e7\u00e3o \u00e9 a soma de v\u00e1rios recursos. Quando a soma \u00e9 inv\u00e1lida, o MetalLB <strong>ignora a mudan\u00e7a e continua com a \u00faltima configura\u00e7\u00e3o v\u00e1lida<\/strong>. O recurso <code>ConfigurationState<\/code>, desde a v0.15.3, mostra o resultado por componente, e os logs trazem <code>failed to parse the configuration<\/code>.<\/p>\n<h3>O control-plane n\u00e3o anuncia<\/h3>\n<p>N\u00f3s com o r\u00f3tulo <code>node.kubernetes.io\/exclude-from-external-load-balancers<\/code> s\u00e3o ignorados, e o kubeadm e o kind colocam esse r\u00f3tulo no control-plane. No nosso teste de BGP, o control-plane fechou a sess\u00e3o mas enviou zero prefixos, e o log do speaker dizia exatamente o motivo:<\/p>\n<pre><code class=\"language-text\">\"event\":\"skipping should announce bgp\",\"ips\":[\"10.200.0.0\"],\"protocol\":\"bgp\",\n\"reason\":\"speaker's node has labeled 'node.kubernetes.io\/exclude-from-external-load-balancers'\"<\/code><\/pre>\n<p>Em cluster de n\u00f3 \u00fanico ou quando voc\u00ea quer mesmo anunciar pelo control-plane, remova o r\u00f3tulo (<code>kubectl label node &lt;no&gt; node.kubernetes.io\/exclude-from-external-load-balancers-<\/code>) ou passe <code>--ignore-exclude-lb<\/code> aos speakers.<\/p>\n<h3>Anuncia, mas n\u00e3o responde<\/h3>\n<ul>\n<li><strong>N\u00e3o teste com ping.<\/strong> O IP do servi\u00e7o n\u00e3o responde ICMP; teste a porta da aplica\u00e7\u00e3o.<\/li>\n<li><strong>Testar de dentro de um n\u00f3 n\u00e3o prova nada.<\/strong> Um n\u00f3 alcan\u00e7a o IP pelo CNI mesmo que o an\u00fancio esteja quebrado. Teste de uma m\u00e1quina fora do cluster.<\/li>\n<li><strong>Em L2, use <code>arping<\/code><\/strong> de um host na mesma sub-rede: <code>arping -I eth0 192.168.47.200<\/code> deve mostrar um \u00fanico MAC, o do n\u00f3 l\u00edder. Dois MACs indicam dois speakers brigando, um IP duplicado na rede ou o CNI respondendo ARP. Nenhuma resposta costuma ser prote\u00e7\u00e3o antispoofing de MAC no switch ou no hypervisor; confirme com <code>tcpdump -n -i eth0 arp<\/code> no n\u00f3 eleito.<\/li>\n<li><strong>Wi-Fi:<\/strong> alguns dispositivos, como o Raspberry Pi, param de responder ARP pela interface sem fio. O contorno citado na doc \u00e9 o modo prom\u00edscuo (<code>ip link set wlan0 promisc on<\/code>).<\/li>\n<li><strong>Em BGP, confira a sess\u00e3o e a rota:<\/strong> <code>vtysh -c \"show bgp neighbor\"<\/code> no roteador ou no cont\u00eainer FRR do n\u00f3, e a m\u00e9trica <code>frrk8s_bgp_session_up<\/code>.<\/li>\n<li><strong>Retorno assim\u00e9trico:<\/strong> se o cliente est\u00e1 em outra sub-rede e o pacote entra por uma interface que n\u00e3o \u00e9 a do gateway padr\u00e3o, o <code>rp_filter<\/code> do Linux descarta a resposta. Resolva com rotas est\u00e1ticas nos n\u00f3s ou roteamento por origem.<\/li>\n<\/ul>\n<p>Para abrir um bug, a doc pede os logs em n\u00edvel <code>debug<\/code> e o resultado do script <a href=\"https:\/\/raw.githubusercontent.com\/metallb\/metallb\/v0.16.1\/troubleshooting\/collect.sh\">collect.sh<\/a>, que junta logs e recursos. As imagens do MetalLB n\u00e3o t\u00eam shell; para depurar dentro do pod, use <code>kubectl debug<\/code> com um cont\u00eainer ef\u00eamero.<\/p>\n<h2>Monitoramento e atualiza\u00e7\u00e3o<\/h2>\n<p>MetalLB e FRR-K8s exp\u00f5em m\u00e9tricas Prometheus. Desde a v0.16.0, o endpoint \u00e9 s\u00f3 HTTPS, com certificado autoassinado ou fornecido por voc\u00ea, e o chart 0.16.1 corrigiu as anota\u00e7\u00f5es de coleta para esse esquema. As mais \u00fateis s\u00e3o <code>frrk8s_bgp_session_up<\/code> para as sess\u00f5es BGP e <code>metallb_k8s_client_config_stale_bool<\/code> para configura\u00e7\u00e3o parada. O guia de <a href=\"\/2026\/09\/monitorando-servidores-linux-com-prometheus\">monitoramento com Prometheus<\/a> mostra como coletar e alertar; e o <a href=\"\/2026\/09\/go-uptime-monitoramento-self-hosted-derivado-do-gatus\">Go Uptime<\/a> pode vigiar o IP do servi\u00e7o de fora do cluster, que \u00e9 o teste que realmente importa.<\/p>\n<p>Para atualizar, leia as <a href=\"https:\/\/metallb.io\/release-notes\/\">notas de vers\u00e3o<\/a> e reaplique o manifesto da nova vers\u00e3o ou rode <code>helm upgrade<\/code>; o chart atualiza os CRDs sozinho. Aten\u00e7\u00e3o a quem instalou por Helm com os valores padr\u00e3o antes da 0.16: o backend mudou de FRR para FRR-K8s, o que troca a topologia dos pods e o prefixo das m\u00e9tricas de <code>metallb_<\/code> para <code>frrk8s_<\/code>. Para manter o FRR antigo durante a transi\u00e7\u00e3o, fixe <code>speaker.frr.enabled=true<\/code> e <code>frrk8s.enabled=false<\/code>. E lembre das limita\u00e7\u00f5es de failover: em L2 o IP muda de n\u00f3, em BGP as conex\u00f5es ativas s\u00e3o resetadas; fa\u00e7a a atualiza\u00e7\u00e3o em hor\u00e1rio de pouco tr\u00e1fego.<\/p>\n<p>Para desmontar o laborat\u00f3rio: <code>kind delete cluster --name metallb-lab<\/code> e <code>docker rm -f metallb-lab-router<\/code>.<\/p>\n<h2>L2 ou BGP?<\/h2>\n<ul>\n<li><strong>Layer 2<\/strong> se voc\u00ea tem uma LAN simples, n\u00e3o controla o roteador ou quer resolver em cinco minutos. Aceite que um n\u00f3 recebe todo o tr\u00e1fego de cada IP e que o failover leva alguns segundos.<\/li>\n<li><strong>BGP<\/strong> se voc\u00ea tem roteadores com BGP e precisa de balanceamento real entre n\u00f3s, mais banda do que uma interface aguenta, ou j\u00e1 roda uma rede roteada no datacenter. Planeje as mudan\u00e7as de n\u00f3 por causa dos resets.<\/li>\n<\/ul>\n<p>O MetalLB faz um trabalho pequeno e bem definido: dar um IP ao Service e levar o tr\u00e1fego at\u00e9 algum n\u00f3. Isso basta para tirar o bare metal da condi\u00e7\u00e3o de segunda classe, e \u00e9 a base sobre a qual voc\u00ea coloca um Ingress Controller, um Gateway API ou os seus pr\u00f3prios servi\u00e7os TCP e UDP. C\u00f3digo e documenta\u00e7\u00e3o completa est\u00e3o em <a href=\"https:\/\/metallb.io\/\">metallb.io<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Complete guide to MetalLB 0.16: why the LoadBalancer stays in pending on bare metal, how Layer 2 and BGP modes work, installation, IPAddressPool, annotations, traffic policy and troubleshooting, all tested in a lab with kind and FRR.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[46,21,2,111,120],"tags":[488,486,490,226,489,225,487,485],"class_list":["post-1702","post","type-post","status-publish","format-standard","hentry","category-devops","category-infra","category-linux","category-opensource","category-servidores","tag-bare-metal","tag-bgp","tag-frr","tag-k8s","tag-kind","tag-kubernetes","tag-load-balancer","tag-metallb"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1702","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1702"}],"version-history":[{"count":1,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1702\/revisions"}],"predecessor-version":[{"id":1704,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1702\/revisions\/1704"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1702"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1702"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1702"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}