62.4 LoadBalancer Services: MetalLB on kind and the AWS Load Balancer Controller
Why: the problem
NodePort made the storefront reachable, but testers had to know node IPs and a strange port. Real users of an app like Swiggy want one stable address on port 80 or 443. A LoadBalancer Service gives exactly that: Kubernetes asks someone outside the cluster to create a load balancer and puts its address in the Service's EXTERNAL-IP column. Who is that someone? On a cloud, it is a controller. On EKS today the recommended one is the AWS Load Balancer Controller, which creates a Network Load Balancer (NLB) for a Service of type LoadBalancer, and an Application Load Balancer (ALB) for an Ingress. On bare metal or kind nobody answers, so EXTERNAL-IP stays
What: every term in plain English
- type: LoadBalancer — Asks an external controller for a load balancer.
- EXTERNAL-IP — The address the controller assigned;
until then. - MetalLB — A load balancer implementation for bare metal and kind.
- IPAddressPool — MetalLB resource listing IPs it may hand out.
- L2Advertisement — Tells MetalLB to answer ARP for those IPs on the local network.
- AWS Load Balancer Controller — Creates NLBs for Services and ALBs for Ingresses on EKS.
- NLB — AWS Network Load Balancer, layer 4 (TCP/UDP).
- Target type ip vs instance — ip sends straight to pod IPs; instance goes through node ports.
- loadBalancerClass — Chooses which controller handles the Service.
- loadBalancerSourceRanges — Limits which client CIDRs may connect.
How: step by step
- Change store to type LoadBalancer. See
. - Install MetalLB. Native manifests.
- Give MetalLB an IP pool from the kind network. IPAddressPool and L2Advertisement.
- Watch EXTERNAL-IP appear and curl it. From a container on the kind network.
- Read the EKS NLB manifest. Annotations for the AWS controller.
- Limit sources with loadBalancerSourceRanges. Basic protection.
- Clean up. Keep MetalLB for later lessons.
Example 1: See why EXTERNAL-IP stays on kind
Goal. See why EXTERNAL-IP stays
store-lb.yaml
apiVersion: v1
kind: Service
metadata:
name: store-lb
namespace: net
spec:
type: LoadBalancer
selector:
app: store
ports:
- name: http
port: 80
targetPort: http
What each line does
type: LoadBalancer— Requests an external load balancer.No nodePort set— Kubernetes still allocates one automatically.<pending>— No controller in kind fulfils the request.Still usable— ClusterIP and NodePort work meanwhile.Events— Nothing is logged, because nobody is watching.
Green output
$ kubectl apply -f store-lb.yaml
$ kubectl get svc -n net store-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
store-lb LoadBalancer 10.96.183.77 <pending> 80:31402/TCP
$ sleep 60; kubectl get svc -n net store-lb -o jsonpath='{.status.loadBalancer}{"\n"}'
{}
Expected result. After a minute the status is still empty. The Service is valid; there is simply no load balancer provider.
Example 2: Install MetalLB and get a real external IP
Goal. Install MetalLB and get a real external IP.
metallb-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: kind-pool
namespace: metallb-system
spec:
addresses:
- 172.18.255.200-172.18.255.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: kind-l2
namespace: metallb-system
spec:
ipAddressPools:
- kind-pool
What each line does
Pool range— Taken from the top of the kind Docker network (docker network inspect kind), away from node IPs.L2Advertisement— Speakers answer ARP for these IPs, so other machines on the network find them.controller— MetalLB pod that assigns IPs.speaker— DaemonSet pod on each node that announces the IP.Result— EXTERNAL-IP is filled in within seconds.
Green output
$ docker network inspect kind -f '{{(index .IPAM.Config 0).Subnet}}'
172.18.0.0/16
$ kubectl apply -f metallb-native-v0.14.8.yaml
$ kubectl wait -n metallb-system --for=condition=Ready pod -l app=metallb --timeout=120s
pod/controller-5f9b8d9c7-xk2lp condition met
pod/speaker-7wq4m condition met
$ kubectl apply -f metallb-pool.yaml
$ kubectl get svc -n net store-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
store-lb LoadBalancer 10.96.183.77 172.18.255.200 80:31402/TCP
$ docker run --rm --network kind curlimages/curl:8.10.1 -sI http://172.18.255.200 | head -1
HTTP/1.1 200 OK
Expected result. The same Service now has an external IP on port 80, and a client on the kind network reaches it without knowing any node IP.
Example 3: Read an EKS NLB Service for the AWS Load Balancer Controller
Goal. Read an EKS NLB Service for the AWS Load Balancer Controller.
store-nlb.yaml
apiVersion: v1
kind: Service
metadata:
name: store-nlb
namespace: net
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
loadBalancerSourceRanges:
- 203.0.113.0/24
selector:
app: store
ports:
- name: http
port: 80
targetPort: http
What each line does
aws-load-balancer-type: external— Hands the Service to the AWS Load Balancer Controller.nlb-target-type: ip— NLB targets pod IPs directly; no extra node hop.scheme: internet-facing— Public NLB; use internal for private ones.loadBalancerClass— Makes sure only the AWS controller handles it.loadBalancerSourceRanges— Only the office range 203.0.113.0/24 may connect.
Green output
$ kubectl apply -f store-nlb.yaml
$ kubectl get svc -n net store-nlb
NAME TYPE EXTERNAL-IP PORT(S)
store-nlb LoadBalancer k8s-net-storenlb-1a2b3c4d5e-0123456789.elb.ap-south-1.amazonaws.com 80:30612/TCP
$ kubectl describe svc -n net store-nlb | grep -A2 Events
Normal SuccessfullyReconciled 45s service Successfully reconciled
Expected result. On EKS the EXTERNAL-IP is a DNS name of the NLB. It can take a few minutes before the name resolves and targets become healthy. This one needs a real EKS cluster with the controller installed.
Every command, step by step (kind lab with kubectl)
-
Create store-lb
kubectl apply -f store-lb.yamlWhat it does: LoadBalancer type.
service/store-lb created -
See pending
kubectl get svc -n net store-lbWhat it does: No provider yet.
store-lb LoadBalancer 10.96.183.77 <pending> 80:31402/TCP -
Find the kind subnet
docker network inspect kind -f '{{(index .IPAM.Config 0).Subnet}}'What it does: Pick a pool from its top.
172.18.0.0/16 -
Install MetalLB
kubectl apply -f metallb-native-v0.14.8.yamlWhat it does: Pinned version.
namespace/metallb-system created -
Wait
kubectl wait -n metallb-system --for=condition=Ready pod -l app=metallb --timeout=120sWhat it does: Controller and speakers.
pod/controller-5f9b8d9c7-xk2lp condition met -
Add the pool
kubectl apply -f metallb-pool.yamlWhat it does: IPAddressPool plus L2Advertisement.
ipaddresspool.metallb.io/kind-pool created l2advertisement.metallb.io/kind-l2 created -
Read the IP
kubectl get svc -n net store-lb -o jsonpath='{.status.loadBalancer.ingress[0].ip}{"\n"}'What it does: Assigned from the pool.
172.18.255.200 -
Curl it
docker run --rm --network kind curlimages/curl:8.10.1 -sI http://172.18.255.200 | head -1What it does: Port 80, no node IP.
HTTP/1.1 200 OK -
Request a fixed IP
kubectl annotate svc -n net store-lb metallb.universe.tf/loadBalancerIPs=172.18.255.210 --overwriteWhat it does: Pins the address.
service/store-lb annotated -
MetalLB events
kubectl describe svc -n net store-lb | tail -3What it does: IPAllocated and nodeAssigned.
Normal IPAllocated 5s metallb-controller Assigned IP ["172.18.255.210"] -
Clean up
kubectl delete svc -n net store-lbWhat it does: Keep MetalLB installed.
service "store-lb" deleted
When it is red 1
EXTERNAL-IP <pending> forever
Cause. No load balancer controller, or MetalLB has no pool.
Fix, step by step
- Install MetalLB or the AWS controller; check IPAddressPool.
When it is red 2
Failed to allocate IP for "net/store-lb": no available IPs
Cause. Pool is empty or too small.
Fix, step by step
- Add more addresses or free unused Services.
When it is red 3
Failed build model due to unable to resolve at least one subnet
Cause. AWS controller cannot find tagged subnets.
Fix, step by step
- Tag public subnets kubernetes.io/role/elb=1 and private ones kubernetes.io/role/internal-elb=1.
When it is red 4
NLB targets unhealthy
Cause. Security group blocks the NLB or the health check path fails.
Fix, step by step
- Allow traffic from the NLB, fix healthcheck-path.
Practice tasks
Easy Task 1: Pending
Why: See.
What to do: Create a LoadBalancer on bare kind.
- <pending>.
Solution
kubectl get svc -n net store-lbEasy Task 2: MetalLB
Why: Install.
What to do: Install MetalLB.
- Native manifest.
Solution
kubectl get pods -n metallb-systemEasy Task 3: Pool
Why: Configure.
What to do: Add a pool.
- IPAddressPool.
Solution
kubectl get ipaddresspool -AMedium Task 4: Curl
Why: Test.
What to do: Curl the external IP.
- kind network.
Solution
docker run --rm --network kind curlimages/curl:8.10.1 -sI http://172.18.255.200Medium Task 5: Fixed IP
Why: Pin.
What to do: Pin an IP.
- Annotation.
Solution
kubectl describe svc -n net store-lbMedium Task 6: Sources
Why: Restrict.
What to do: Add loadBalancerSourceRanges.
- CIDR list.
Solution
kubectl get svc -n net store-lb -o yamlMedium Task 7: Speaker
Why: Read.
What to do: Read speaker logs.
- app=metallb.
Solution
kubectl logs -n metallb-system -l component=speaker --tail=5Medium Task 8: NLB
Why: Read.
What to do: Explain each annotation.
- Controller docs.
Solution
kubectl explain service.spec.loadBalancerClassHard Task 9: Internal
Why: Main task.
What to do: Write the internal NLB version.
- scheme internal.
Solution
kubectl apply --dry-run=client -f store-nlb.yamlHard Task 10: Cost
Why: Design.
What to do: Plan one Ingress LB for many apps.
- 62 -> Ingress chapter.
Solution
kubectl get svc -A | grep LoadBalancerHomework: try at home
For Swiggy's staging on kind, expose the storefront with a MetalLB IP fixed at 172.18.255.210, limited to the kind network with loadBalancerSourceRanges, and write the EKS NLB version of the same Service for an internal (not public) load balancer.
Recap
- type: LoadBalancer asks a controller for an external load balancer.
- Without one, EXTERNAL-IP stays
. - MetalLB gives kind and bare metal real IPs from a pool.
- On EKS, the AWS Load Balancer Controller creates an NLB; ip targets skip node ports.
- Next: ExternalName and headless Services.
FAQ
Q. What is BGP mode in MetalLB?
Instead of answering ARP, speakers announce the IPs to your routers over BGP, which spreads traffic across nodes and fails over faster. It needs router support.
Q. What happens if the MetalLB speaker node dies in L2 mode?
Another speaker takes over the IP and sends a new ARP announcement. Clients see a short pause of a few seconds.
Q. How much does each NLB cost?
Each NLB has an hourly charge plus capacity units. Ten Services with ten NLBs cost ten times one Ingress setup, which is why teams consolidate.
Q. Does deleting the Service delete the NLB?
Yes, the controller removes it. Be careful in production: the DNS name changes when it is recreated.
Q. Where do I get metallb-native-v0.14.8.yaml?
From the Installation by manifest page of the official MetalLB docs. Save the file, read it, pin the version, and keep it in your repo so installs are repeatable.
Q. Can two Services share one MetalLB IP?
Yes, with the metallb.universe.tf/allow-shared-ip annotation, if their ports do not clash.
Q. Is MetalLB used in production?
Yes, on bare-metal clusters, often in BGP mode.
Q. Why is the EKS EXTERNAL-IP a hostname?
AWS load balancers are addressed by DNS name; IPs can change.
Q. What is the in-tree AWS provider?
The old cloud provider creates Classic or NLB load balancers; the AWS Load Balancer Controller is the recommended way now.
Q. Can I turn off the node port?
Yes, allocateLoadBalancerNodePorts: false, when the LB targets pod IPs.
Q. Does sourceRanges work with MetalLB?
Yes, kube-proxy enforces it.
Q. How do I get HTTPS?
Add a TLS certificate on the NLB, or terminate TLS in an Ingress controller.
Interview questions
-
What happens when you create a LoadBalancer Service?
Model answer: A ClusterIP and node port are allocated, then a controller provisions an external LB and writes its address to status.
-
Why does EXTERNAL-IP stay pending on bare metal?
Model answer: No cloud controller; you need MetalLB or similar.
-
Instance vs ip target type?
Model answer: Instance sends to node ports on nodes; ip sends directly to pod IPs.
-
How do you limit who can reach a LoadBalancer?
Model answer: loadBalancerSourceRanges, security groups, or an internal scheme.
-
Why put many apps behind one Ingress?
Model answer: One load balancer costs less and centralises TLS and routing.
-
How would you debug a LoadBalancer Service stuck in pending on EKS?
Model answer: Check that the AWS Load Balancer Controller pods run, read their logs, confirm the IAM role, check subnet tags, and read the Service events for reconcile errors.
Quick quiz
Q1. What does <pending> mean?
No controller has created a load balancer.
Q2. What does MetalLB need?
An IP pool and an advertisement.
Q3. NLB target type ip means?
Traffic goes straight to pod IPs.
Q4. Does a LoadBalancer Service have a node port?
Yes, by default.
Q5. Why not one LB per app?
Cost; use an Ingress controller instead.
Samajla ka?