Kubernetes Anycast with BGP Controller
This guide covers deploying globally distributed anycast services on NetActuate Managed Kubernetes using the open-source NetActuate BGP Controller. The controller automates BGP session provisioning, prefix announcement, and health-based failover across your clusters.
What This Guide Covers
The NetActuate BGP Controller is a Kubernetes DaemonSet that:
- Provisions BGP sessions automatically via the NetActuate API
- Configures your BGP backend (MetalLB, Calico, Cilium, or BIRD) to peer with NetActuate routers
- Announces your anycast prefixes from each cluster location
- Withdraws prefixes when health checks fail, shifting traffic to the next nearest POP
- Re-announces prefixes when services recover
The result is a globally distributed anycast architecture where traffic routes to the nearest healthy cluster automatically, with no manual BGP session management.
Prerequisites
- A NetActuate account with:
- A BGP group and group ID (visible in the portal under Networking > Anycast)
- An allocated ASN
- At least one anycast prefix (e.g., a /24)
- VM IP-based API authentication enabled (contact your account manager to enable this)
- Kubernetes clusters deployed on NetActuate VMs at one or more locations
- A supported BGP backend installed on each cluster:
- MetalLB v0.13+ in CRD mode (recommended for most deployments)
- Calico v3.x with BGP dataplane enabled
- Cilium v1.13+ with
--enable-bgp-control-plane=true
Note: The controller also supports a BIRD backend for bare-metal and VM deployments outside Kubernetes. See the BIRD backend section below.
Step 1: Install a BGP Backend
If you do not already have a BGP backend installed, MetalLB is the recommended starting point.
MetalLB (recommended)
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
Wait for MetalLB pods to be ready:
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90s
Calico
If your cluster already uses Calico for networking with BGP enabled, no additional installation is needed. Set controller.backend: calico in the Helm values.
Cilium
If your cluster uses Cilium, ensure BGP control plane is enabled (--enable-bgp-control-plane=true). Set controller.backend: cilium in the Helm values.
Step 2: Configure Authentication
The controller needs to communicate with the NetActuate API to discover the node it's running on and provision BGP sessions.
VM IP-Based Authentication (recommended)
When running as a DaemonSet with hostNetwork: true, the NetActuate API identifies the calling VM by its source IP address. No API key is required.
This method requires VM IP-based authentication to be enabled on your NetActuate account. Contact your account manager to enable it if you have not already.
Leave netactuate.existingSecret empty in the Helm values when using this method.
API Key Authentication (fallback)
If VM IP-based auth is unavailable, create a Kubernetes secret with your API key:
kubectl create secret generic netactuate-api-key \
--from-literal=api-key=YOUR_VAPI2_KEY \
-n kube-system
Then set netactuate.existingSecret: "netactuate-api-key" in the Helm values.
Step 3: Install the BGP Controller
Clone the repository and install with Helm:
git clone https://github.com/netactuate/netactuate-bgp-controller.git
cd netactuate-bgp-controller
helm install bgp-controller ./deploy/helm/bgp-controller \
-n kube-system \
--set bgp.localASN=YOUR_ASN \
--set bgp.groupId=YOUR_GROUP_ID \
--set "bgp.prefixes[0]=YOUR.PREFIX.0.0/24"
Replace YOUR_ASN, YOUR_GROUP_ID, and the prefix with your actual values from the NetActuate portal.
For more complex configurations, use a values file:
helm install bgp-controller ./deploy/helm/bgp-controller \
-n kube-system \
-f my-values.yaml
Example Values File
bgp:
localASN: 64512
groupId: 100
prefixes:
- "198.51.100.0/24"
redundantSessions: false
controller:
backend: metallb
reconcileInterval: 30s
withdrawDelay: 30s
reannounceDelay: 60s
healthChecks:
- name: my-app
url: "http://my-app.default.svc.cluster.local:8080/health"
interval: 10s
timeout: 5s
failureThreshold: 3
successThreshold: 2
Step 4: Deploy a Service
Deploy a service that uses a LoadBalancer IP from your anycast prefix:
kubectl apply -f examples/hello-world.yaml
Or create your own LoadBalancer service. The controller's IPAddressPool makes your anycast prefix available to any service of type LoadBalancer.
Verify the service received an IP from your prefix:
kubectl get svc hello-anycast
Step 5: Verify
Check controller status
kubectl get pods -n kube-system -l app.kubernetes.io/name=bgp-controller
kubectl logs -n kube-system -l app.kubernetes.io/name=bgp-controller --tail=50
Check BGP resources (MetalLB example)
kubectl get bgppeers,ipaddresspools,bgpadvertisements -n metallb-system
Check controller health endpoint
kubectl exec -n kube-system deploy/bgp-controller -- wget -qO- http://localhost:8080/status | jq .
The status response shows the controller state, node info, BGP session status, health check results, and announced prefixes.
Verify in the NetActuate portal
Navigate to Networking > Anycast > BGP Sessions and confirm:
- Sessions show "Established" status
- The customer IP matches your node IP
- The group matches your configured
groupId
Test externally
curl http://198.51.100.1/
traceroute -n 198.51.100.1
Traffic should route to the nearest POP announcing the prefix.
Health-Based Failover
The controller monitors HTTP endpoints you configure and automatically withdraws the anycast prefix when all health checks fail. This shifts traffic to the next nearest healthy POP.
How It Works
- When all configured health checks exceed their
failureThreshold, the controller deletes the BGP advertisement resource. - The BGP backend stops announcing the prefix. Routes withdraw across the internet (typically 30-90 seconds).
- Anycast traffic shifts to the next nearest POP still announcing.
- When any health check reaches its
successThreshold, the controller recreates the advertisement and re-announces the prefix.
Route Dampening Prevention
BGP routers penalize prefixes that flap rapidly, potentially suppressing them for 30-60 minutes. The controller includes safeguards to prevent this:
| Setting | Default | Purpose |
|---|---|---|
controller.withdrawDelay | 30s | Absorbs transient failures before withdrawing |
controller.reannounceDelay | 60s | Prevents re-announcing into unstable conditions |
controller.minAdvertisementDuration | 5m | Prevents rapid flap cycles |
Test Failover
Scale down your service to trigger withdrawal:
kubectl scale deploy my-service --replicas=0
Watch the controller logs to see the health check failures and prefix withdrawal:
kubectl logs -n kube-system -l app.kubernetes.io/name=bgp-controller -f
Verify the advertisement was removed:
kubectl get bgpadvertisements -n metallb-system
Scale back up to trigger recovery:
kubectl scale deploy my-service --replicas=1
Multi-Location Deployment
For global anycast, deploy the BGP controller on Kubernetes clusters at multiple NetActuate locations. Each cluster announces the same prefix from its location. The controller handles BGP session provisioning and failover independently at each site.
Architecture
Users worldwide
|
[Anycast DNS / IP]
|
+---+---+---+---+
| | |
LAX AMS SIN
K8s K8s K8s
cluster cluster cluster
| | |
BGP BGP BGP
ctrl ctrl ctrl
Each BGP controller instance:
- Discovers which NetActuate VM it's running on
- Provisions BGP sessions for that location
- Announces the shared anycast prefix
- Independently monitors local service health
- Withdraws the prefix if local services fail
No coordination between sites is needed. Each controller operates independently, and BGP routing handles traffic distribution globally.
Per-Location Configuration
Use the same Helm values at every location. The controller automatically discovers the correct node and location via the NetActuate API. The only required settings are your ASN, group ID, and prefix, which are the same across all locations.
BIRD Backend (Bare Metal and VMs)
For deployments outside Kubernetes, the controller supports BIRD 2.x as a backend. It writes configuration files directly to disk and signals BIRD to reload.
Set controller.backend: bird in the Helm values (or run the controller binary directly on the VM).
The controller manages these BIRD config files:
/etc/bird/peers.d/peer_{sessionID}.conffor each BGP peer/etc/bird/filters.d/anycast.conffor the export filter/etc/bird/static.d/anycast_routes.conffor static routes
Your bird.conf must include these directories. The controller calls birdc configure to reload after changes.
For VM-based anycast without Kubernetes, also see the Configuring Anycast guide for manual BGP session setup with BIRD2.
Configuration Reference
For the full configuration reference, including all Helm values, backend-specific settings, and status reporting options, see the BGP Controller README on GitHub.
Key Helm Values
| Value | Required | Default | Description |
|---|---|---|---|
bgp.localASN | Yes | Your allocated BGP ASN | |
bgp.groupId | Yes | NetActuate BGP group ID | |
bgp.prefixes | Yes | IPv4 anycast prefixes (e.g., ["198.51.100.0/24"]) | |
bgp.prefixesV6 | No | [] | IPv6 anycast prefixes |
bgp.redundantSessions | No | false | Enable multi-router HA sessions |
controller.backend | No | metallb | BGP backend: metallb, calico, cilium, or bird |
netactuate.existingSecret | No | "" | Kubernetes secret for API key auth (empty = IP-based auth) |
Troubleshooting
Controller stuck in DISCOVERING state
The controller cannot identify itself via the NetActuate API.
- Verify VM IP-based authentication is enabled on your account
- Confirm the DaemonSet has
hostNetwork: true - Ensure the controller is running on NetActuate infrastructure
BGP sessions created but MetalLB not peering
- Verify the node hostname label matches the
nodeSelectorson the BGPPeer resource:kubectl get nodes --show-labels | grep hostname - Confirm MetalLB was installed in CRD mode
- Check that MetalLB speaker pods are running in
metallb-system - Ensure TCP port 179 (BGP) is open between the node and the NetActuate router
Health checks always failing
- Use full Kubernetes DNS names for health check URLs:
http://my-service.my-namespace.svc.cluster.local:8080/health - Increase
failureThresholdif the service takes time to become ready - Increase
healthChecks[].timeoutfor slow-responding endpoints
Enable debug logging
Set LOG_LEVEL=debug in the controller environment to see detailed API calls and resource reconciliation.
Related Documentation
- Anycast Overview - anycast concepts and portal management
- Configuring Anycast - manual BGP session setup with BIRD2
- Redundant Anycast Groups - primary/secondary/tertiary group design
- ECMP Load Balancing - distributing traffic across servers at a single location
- DDoS Best Practices - anycast group design for attack resilience
- Managed Kubernetes - deploying and managing K8s clusters on NetActuate
Need Help?
Contact support@netactuate.com or open a support ticket from the portal.