Cilium Study Networking Bgp Control Plane
Introduction
Kubernetes 클러스터에서 외부 네트워크로 통신을 위한 방법으로 이전 포스팅에서는 1. Native Routing 모드에서 직접 Static Routing을 추가하는 방법과 2. 캡슐화를 통한 OverLay Network를 이용하는 방법에 대해서 알아보았다.
이번에는 대규모 네트워크 환경에서 BGP 기반 네트워크가 구성된 경우 Cilium의 BGP Control Plane을 통해 Routing을 제어하는 부분에 대해서 알아보려고한다.
실습환경구성

- Kubernetes Cluster : k8s-ctr, k8s-w1 / k8s-w0
- router : 192.168.10.0/24 ↔ 192.168.20.0/24 대역 라우팅 역할, k8s 에 join 되지 않은 서버, BGP 동작을 위한 frr 툴 설치됨
FRRouting (frr)

FRRouting은 Quagga 프로젝트에서 포크된 이후 커뮤니티 주도로 발전해 온 고성능 라우팅 데몬 모음이다.
- Native Linux 네트워킹 스택과 밀접히 통합되어 호스트, VM, 컨테이너 접속부터 ISP·클라우드·대규모 데이터센터 라우팅까지 폭넓은 용례를 지원
- BGP, OSPFv2/v3, RIP/RIPng, IS-IS, PIM, LDP, BFD, Babel, PBR, VRRP 등을 포함하며 일부 프로토콜은 알파 단계 지원도 제공
- GPLv2 라이선스로 배포되며 다양한 하드웨어 수준에서 풀 인터넷 라우팅 테이블을 처리할 수 있도록 설계
- 각 프로토콜별 데몬(bgpd, ospfd, isisd 등)과
vtysh인터페이스를 통해 구성·운영
활용사례
- (클라우드·컨테이너 환경에서) FRR은 Kubernetes/OpenShift의 L2/L3 로드밸런싱 구성과 특히 MetalLB의 BGP 백엔드로 채택되어 노드를 BGP 라우터로 동작시키는 데 활용
- (전통적인 엔터프라이즈·서비스사업자 망에서는) iBGP/EBGP, 경로 반사기, 멀티에어리어 OSPF, IS-IS 기반 코어, PIM 기반 멀티캐스트, LDP 기반 MPLS, BFD 기반 고속 장애 감지 등 다양한 기능 요구에 대응
- NVIDIA Cumulus 등 상용 네트워킹 스택에서도 FRR이 핵심 라우팅 구성요소로 사용
BGP

**BGP(Border Gateway Protocol)**는 인터넷에서 사용되는 핵심 라우팅 프로토콜로, 자율 시스템(AS, Autonomous System) 간에 라우팅 정보를 교환하는 데 사용된다. 또한 BGP는 AS 간뿐만 아니라 대규모 기업 네트워크에서도 외부와 내부 라우팅을 위해 활용
특징
- TCP(179) Port : BGP 라우터간 통신간 TCP 사용
- Peer(Neighbor)와 세션 구성: 라우터 간에 직접 연결되지 않아도 피어 관계를 맺고 정보를 교환할 수 있음.
- 라우트 광고: AS의 엣지(Edge)에서 각 네트워크가 “A 네트워크로 가는 경로는 나를 통해!”라고 광고하며, 이를 바탕으로 모든 인터넷 경로 선정을 진행
- Incremental Update: 전체 정보를 주기적으로 보내지 않고, 변경된 정보만 업데이트
구성요소
- eBGP(External BGP): 서로 다른 AS 간 라우팅 정보를 교환. ISP 간 트래픽/인터넷 연결에 필수적 요소
- iBGP(Internal BGP): 같은 AS 내부의 라우터들 간 라우팅 정보를 전달. 대규모 AS 내부의 내부 트래픽 라우팅에 사용
Cilium BGP Control Plane
BGP Control Plane Resources — Cilium 1.18.0 documentation

Cilium BGP Control Plane은 쿠버네티스 클러스터 내 Pod 네트워크 및 서비스(IP)를 외부 네트워크(예: 데이터센터 라우터, 온프레미스 라우터 등)와 직접 연결·라우팅하기 위해 BGP(Border Gateway Protocol)를 활용하는 네트워크 제어 기능이다.
CNI 계층에서 네이티브 라우팅/로드밸런싱과 인프라 통합을 목적으로 고안되었다.
역할
- BGP 세션 자동 관리
- 각 노드에서 Cilium Agent가 동작하며, 외부 라우터 또는 다른 노드와 BGP 피어링을 수립하여 Pod/Service 네트워크의 경로 정보를 **광고(advertise)**한다.
- 외부 라우팅 및 LB IP 할당
- Pod, Node, 또는 LoadBalancer 서비스의 IP(예: Ingress VIP, Pod CIDR)를 외부 라우터에 동적 광고해, LoadBalancer 없이도 외부에서 바로 클러스터 내 Pod/Service로 접근이 가능하다.
구성
Custom Resources로 선언적 정책 사용
Cilium에서 Custom Resources를 통해서 BGP 설정을 선언적으로 관리하는게 가능해 GitOps/자동화와 결합 용이하다.
CiliumBGPClusterConfig: (다수의) 노드에 적용되는 BGP 인스턴스와 피어 설정에 대한 정의CiliumBGPPeerConfig: BGP 피어에 대한 일반적 설정. (다수의) 피어에서 사용될 수 있음CiliumBGPAdvertisement: BGP 라우팅 테이블에 주입(Injected)되는 PrefixCiliumBGPNodeConfigOverride: 세밀한 조작을 위한 특정 노드에 BGP 설정을 정의
GoBGP 통합
내부적으로 GoBGP 엔진을 활용해 BGP 피어 세션 관리, Prefix 광고, 정책 적용 등 프로그램 유연성을 제공한다.
ECMP
Equal Cost Multi Path(ECMP) 설정 시 여러 노드에서 같은 서비스 VIP를 광고하여 상위 라우터에서 여러 노드로 트래픽을 분산, 고가용 및 로드밸런싱 구현이 가능하다.
ECMP?
ECMP (equal cost multipath) is a method to utilize multiple same-cost paths to route a packet to a destination

위의 예시에서 [ R1 ] → [ R6 ]로 가기위해서는 2가지 방법이 있는데,
- R1 → R2 → R3 → R6
- R1 → R4 → R5 → R6
ECMP를 적용하면 동일한 목적지까지 비용(예: hop 수, bandwidth, delay 등)이 같은 여러 경로가 있을 때, 그 경로들을 동시에 활용하여 트래픽을 분산된다.
- Per-Packet보다는 “Per-Destination/Per-Flow 해시 기반” 알고리즘을 사용
eBGP Multi Hop 지원
복잡하고 다층적인 네트워크 토폴로지를 처리하는 기업에 더욱 정교하고 유용한 기능을 제공
- 표준 네트워킹 설정에서 eBGP는 단일 홉으로 제한되어 있어 더 크고 복잡한 환경에서는 유연성이 제한된다
- Cilium의 BGP 멀티홉 지원은 이러한 제약을 극복하여 여러 홉에 걸쳐 BGP 세션을 설정할 수 있도록 지원하여 직접 연결되지 않은 BGP 라우터와도 상호 작용할 수 있도록한다.
- 라우팅 유연성을 향상시키고, 네트워크 관리 작업을 간소화하여 복잡한 네트워크 인프라를 유지하는데 따르는 운영상 복잡성과 비용을 줄여줌.

BGP Graceful Restart: 고가용성 지원
BGP 세션이 중단이나 시스템 장애 발생 시 데이터 전달에 즉각적인 영향을 미치지 않고 원활하게 복구될 수 있도록하는 기능을 제공한다.
- 롤링 업데이트(Rolling Update) 또는 계획되지 않은 다운타임 중에도 라우팅 정보의 안정성을 유지하고 트래픽 흐름이 중단 없이 유지되도록 보장

Cilium BGP Daemon과 Router-A간 BGP Session이 구성된 상황에서
- (좌측) Graceful Restart가 구성되지 않는 경우 Cilium을 재시작하면 BGP 세션이 손실된다.
- (우측) Graceful Restart가 구성된 경우 Cilium을 재시작하더라도 Router-A에서 일정기간동안 경로를 보관하기때문에 제거되지 않음.


실습 1 : BGP 설정 후 통신 확인
실습환경 네트워크 확인
현재 실습환경의 네트워크 정보를 확인한다.
- k8s Cluster
autoDirectNodeRoute가 비활성화1# Native Routing Mode 2**cilium config view | grep routing-mode** 3routing-mode native 4 5# autoDirectNodeRoutes=false 6**cilium config view | grep auto-direct** 7auto-direct-node-routes falsek8s node 네트워크 인터페이스 정보 확인
1# [k8s-ctr] 2**ip -c -4 addr show dev eth1** 33: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 4 altname enp0s9 5 inet 192.168.10.100/24 brd 192.168.10.255 scope global eth1 6 valid_lft forever preferred_lft forever 7 8# worker node도 확인 9**for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i ip -c -4 addr show dev eth1; echo; done** 10>> node : k8s-w1 << 113: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 12 altname enp0s9 13 inet 192.168.10.101/24 brd 192.168.10.255 scope global eth1 14 valid_lft forever preferred_lft forever 15 16>> node : k8s-w0 << 173: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 18 altname enp0s9 19 inet 192.168.20.100/24 brd 192.168.20.255 scope global eth1 20 valid_lft forever preferred_lft forever라우팅 정보 확인 (→ 노드별 PodCIDR이 없음)
1# [k8s-ctr] 2**ip -c route** 3default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 410.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 510.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 610.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 7172.20.0.0/24 via 172.20.0.104 dev cilium_host proto kernel src 172.20.0.104 8172.20.0.104 dev cilium_host proto kernel scope link 9192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100 10192.168.20.0/24 via 192.168.10.200 dev eth1 proto static 11 12# worker node도 확인 13**for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i ip -c route; echo; done** 14 15>> node : k8s-w1 << 16default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 1710.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 1810.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 1910.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 20172.20.1.0/24 via 172.20.1.181 dev cilium_host proto kernel src 172.20.1.181 21172.20.1.181 dev cilium_host proto kernel scope link 22192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101 23192.168.20.0/24 via 192.168.10.200 dev eth1 proto static 24 25>> node : k8s-w0 << 26default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 2710.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 2810.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 2910.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 30172.20.2.0/24 via 172.20.2.137 dev cilium_host proto kernel src 172.20.2.137 31172.20.2.137 dev cilium_host proto kernel scope link 32192.168.10.0/24 via 192.168.20.200 dev eth1 proto static 33192.168.20.0/24 dev eth1 proto kernel scope link src 192.168.20.100
→ 현재 ‘Native Routing’ 모드, autoDirecNodeRoute 는 비활성화 되어있고 따라서 각 노드들은 PodCIDR의 Routing 경로에 대해서 없는상태이다.
- router
네트워크 인터페이스 확인
1**ip -br -c -4 addr** 2lo UNKNOWN 127.0.0.1/8 3eth0 UP 10.0.2.15/24 metric 100 4eth1 UP 192.168.10.200/24 5eth2 UP 192.168.20.200/24 6loop1 UNKNOWN 10.10.1.200/24 7loop2 UNKNOWN 10.10.2.200/24라우팅 정보 확인 (→ PodCIDR에 대한 정보가 없음)
1**ip -c route** 2default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 310.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 410.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 510.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 610.10.1.0/24 dev loop1 proto kernel scope link src 10.10.1.200 710.10.2.0/24 dev loop2 proto kernel scope link src 10.10.2.200 8192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.200 9192.168.20.0/24 dev eth2 proto kernel scope link src 192.168.20.200
→ router도 역시 k8s Cluster의 PodCIDR에 대해서는 모르는 상태이다.
현재 통신상태에 문제가 있는부분을 확인한다.
샘플 애플리케이션 배포
1# 샘플 애플리케이션 배포 2cat << EOF | kubectl apply -f - 3apiVersion: apps/v1 4kind: Deployment 5metadata: 6 name: webpod 7spec: 8 replicas: 3 9 selector: 10 matchLabels: 11 app: webpod 12 template: 13 metadata: 14 labels: 15 app: webpod 16 spec: 17 affinity: 18 podAntiAffinity: 19 requiredDuringSchedulingIgnoredDuringExecution: 20 - labelSelector: 21 matchExpressions: 22 - key: app 23 operator: In 24 values: 25 - sample-app 26 topologyKey: "kubernetes.io/hostname" 27 containers: 28 - name: webpod 29 image: traefik/whoami 30 ports: 31 - containerPort: 80 32--- 33apiVersion: v1 34kind: Service 35metadata: 36 name: webpod 37 labels: 38 app: webpod 39spec: 40 selector: 41 app: webpod 42 ports: 43 - protocol: TCP 44 port: 80 45 targetPort: 80 46 type: ClusterIP 47EOF 48 49# k8s-ctr 노드에 curl-pod 파드 배포 50cat <<EOF | kubectl apply -f - 51apiVersion: v1 52kind: Pod 53metadata: 54 name: curl-pod 55 labels: 56 app: curl 57spec: 58 nodeName: k8s-ctr 59 containers: 60 - name: curl 61 image: nicolaka/netshoot 62 command: ["tail"] 63 args: ["-f", "/dev/null"] 64 terminationGracePeriodSeconds: 0 65EOF통신 문제 확인
1# webpod 서비스에 반복문으로 호출 -> k8s-ctr에 배포된 webpod만 응답이 온다. 2**kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'** 3--- 4Hostname: webpod-697b545f57-gqmg8 5--- 6--- 7Hostname: webpod-697b545f57-gqmg8 8---cilium-dbg, map을 통해서 상세히 조회가능
1kubectl exec -n kube-system ds/cilium -- cilium-dbg ip list 2kubectl exec -n kube-system ds/cilium -- cilium-dbg endpoint list 3kubectl exec -n kube-system ds/cilium -- cilium-dbg service list 4kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf lb list 5kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf nat list 6kubectl exec -n kube-system ds/cilium -- cilium-dbg map list | grep -v '0 0' 7kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_services_v2 8kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_backends_v3 9kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_reverse_nat 10kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_ipcache_v2
BGP 설정 확인
[ router ] 에서 frr의 데몬이 실행되고 있다. zebra는 TCP(2601)으로 오픈되어있고, bgpd는 내부통신을 위해 2605 포트와 다른 AS간 BGP통신을 위한 TCP(179) 포트를 오픈한다.
1# zebra (2601), bgpd(2605, 179) 포트 오픈
2**ss -tnlp | grep -iE 'zebra|bgpd'**
3LISTEN 0 3 127.0.0.1:2605 0.0.0.0:* users:(("bgpd",pid=4221,fd=18))
4LISTEN 0 3 127.0.0.1:2601 0.0.0.0:* users:(("zebra",pid=4216,fd=23))
5LISTEN 0 4096 0.0.0.0:179 0.0.0.0:* users:(("bgpd",pid=4221,fd=22))
6LISTEN 0 4096 [::]:179 [::]:* users:(("bgpd",pid=4221,fd=23))
1# frr 데몬 정보 (zebra,bgpd, watchfrr, staticd)
2**ps -ef |grep frr**
3root 4203 1 0 17:58 ? 00:00:01 /usr/lib/frr/watchfrr -d -F traditional zebra bgpd staticd
4frr 4216 1 0 17:58 ? 00:00:00 /usr/lib/frr/zebra -d -F traditional -A 127.0.0.1 -s 90000000
5frr 4221 1 0 17:58 ? 00:00:00 /usr/lib/frr/bgpd -d -F traditional -A 127.0.0.1
6frr 4228 1 0 17:58 ? 00:00:00 /usr/lib/frr/staticd -d -F traditional -A 127.0.0.1
그리고 현재 BGP에 대해서 등록된 이웃(neighbor)은 없는 상황이다.
1# 현재 frr 설정 확인
2**vtysh -c 'show running' # 혹은 cat /etc/frr/frr.conf**
3Current configuration:
4...
5!
6router bgp 65000
7 bgp router-id 192.168.10.200
8 no bgp ebgp-requires-policy
9 bgp graceful-restart
10 bgp bestpath as-path multipath-relax
11 !
12 address-family ipv4 unicast
13 network 10.10.1.0/24
14 maximum-paths 4
15...
16!
17end
18
19# 현재 Neighbor이 등록되어 있지 않음
20**vtysh -c 'show ip bgp summary'**
21% No BGP neighbors found in VRF default
22
23# BGP 정보 확인
24**vtysh -c 'show ip bgp'**
25BGP table version is 1, local router ID is 192.168.10.200, vrf id 0
26Default local pref 100, local AS 65000
27Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
28 i internal, r RIB-failure, S Stale, R Removed
29Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
30Origin codes: i - IGP, e - EGP, ? - incomplete
31RPKI validation codes: V valid, I invalid, N Not found
32
33 Network Next Hop Metric LocPrf Weight Path
34*> 10.10.1.0/24 0.0.0.0 0 32768 i
35
36# 라우팅 테이블과 BGP 라우팅
37**ip -c route
38...**
39192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.200
40192.168.20.0/24 dev eth2 proto kernel scope link src 192.168.20.200
41**vtysh -c 'show ip route'**
42K>* 0.0.0.0/0 [0/100] via 10.0.2.2, eth0, src 10.0.2.15, 01:16:53
43...
44C>* 192.168.10.0/24 is directly connected, eth1, 01:16:53
45C>* 192.168.20.0/24 is directly connected, eth2, 01:16:53
BGP 설정 후 통신확인 (통신 문제 있음)
FRR 설정에 ‘k8s cluster’의 노드들을 CILIUM 그룹명으로 추가한다. 연동 방법은 2가지로 설정이 가능하다.
▶ 방법 1 : /etc/frr/frr.conf 에 추가
1**cat << EOF >> /etc/frr/frr.conf**
2 neighbor CILIUM peer-group
3 neighbor CILIUM remote-as external
4 neighbor 192.168.10.100 peer-group CILIUM
5 neighbor 192.168.10.101 peer-group CILIUM
6 neighbor 192.168.20.100 peer-group CILIUM
7EOF
8
9# frr 데몬 재시작
10**systemctl daemon-reexec && systemctl restart frr**
11systemctl status frr --no-pager --full
12● frr.service - FRRouting
13 Loaded: loaded (/usr/lib/systemd/system/frr.service; enabled; preset: enabled)
14 Active: active (running) since Fri 2025-08-15 19:40:57 KST; 10s ago
15 Docs: https://frrouting.readthedocs.io/en/latest/setup.html
16 Process: 4739 ExecStart=/usr/lib/frr/frrinit.sh start (code=exited, status=0/SUCCESS)
17 Main PID: 4749 (watchfrr)
18 Status: "FRR Operational"
19 Tasks: 13 (limit: 553)
20 Memory: 19.2M (peak: 26.7M)
21 CPU: 176ms
22 CGroup: /system.slice/frr.service
23 ├─4749 /usr/lib/frr/watchfrr -d -F traditional zebra bgpd staticd
24 ├─4762 /usr/lib/frr/zebra -d -F traditional -A 127.0.0.1 -s 90000000
25 ├─4767 /usr/lib/frr/bgpd -d -F traditional -A 127.0.0.1
26 └─4774 /usr/lib/frr/staticd -d -F traditional -A 127.0.0.1
27
28Aug 15 19:40:57 router watchfrr.sh[4760]: Cannot stop zebra: pid file not found
29Aug 15 19:40:57 router zebra[4762]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
30Aug 15 19:40:57 router frrinit.sh[4739]: * Started watchfrr
31Aug 15 19:40:57 router bgpd[4767]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
32Aug 15 19:40:57 router staticd[4774]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
33Aug 15 19:40:57 router watchfrr[4749]: [QDG3Y-BY5TN] zebra state -> up : connect succeeded
34Aug 15 19:40:57 router watchfrr[4749]: [QDG3Y-BY5TN] bgpd state -> up : connect succeeded
35Aug 15 19:40:57 router watchfrr[4749]: [QDG3Y-BY5TN] staticd state -> up : connect succeeded
36Aug 15 19:40:57 router watchfrr[4749]: [KWE5Q-QNGFC] all daemons up, doing startup-complete notify
37Aug 15 19:40:57 router systemd[1]: Started frr.service - FRRouting.
▶ 방법 2 : vtysh 이용
1**vtysh**
2---------------------------
3?
4show ?
5show running
6show ip route
7
8# config 모드 진입
9**conf**
10?
11
12## bgp 65000 설정 진입
13**router bgp 65000**
14?
15neighbor CILIUM peer-group
16neighbor CILIUM remote-as external
17neighbor 192.168.10.100 peer-group CILIUM
18neighbor 192.168.10.101 peer-group CILIUM
19neighbor 192.168.20.100 peer-group CILIUM
20end
21
22# 설정파일에 쓰기
23**write memory
24
25exit**
BGP 설정 변경사항에 대해서 한번 더 확인한다.
1# 현재 frr 설정 확인
2vtysh -c 'show running' # 혹은 cat /etc/frr/frr.conf
3...
4router bgp 65000
5 bgp router-id 192.168.10.200
6 no bgp ebgp-requires-policy
7 bgp graceful-restart
8 bgp bestpath as-path multipath-relax
9 neighbor CILIUM peer-group # <-
10 neighbor CILIUM remote-as external # <-
11 neighbor 192.168.10.100 peer-group CILIUM # <-
12 neighbor 192.168.10.101 peer-group CILIUM # <-
13 neighbor 192.168.20.100 peer-group CILIUM # <-
14 !
15 address-family ipv4 unicast
16 network 10.10.1.0/24
17 maximum-paths 4
18 exit-address-family
19exit
20!
21
22# neighbor이 추가됨
23vtysh -c 'show ip bgp summary'
24...
25Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
26192.168.10.100 4 0 0 0 0 0 0 never Active 0 N/A
27192.168.10.101 4 0 0 0 0 0 0 never Active 0 N/A
28192.168.20.100 4 0 0 0 0 0 0 never Active 0 N/A
29
30# neighbor은 추가되었지만, 라우팅은 추가되지 않음
31vtysh -c 'show ip route'
32K>* 0.0.0.0/0 [0/100] via 10.0.2.2, eth0, src 10.0.2.15, 00:06:47
33...
34C>* 192.168.10.0/24 is directly connected, eth1, 00:06:47
35C>* 192.168.20.0/24 is directly connected, eth2, 00:06:47
Cilium에 BGP를 설정한다. 먼저 CiliumBGPClusterConfig 에서 Selector로 지정이 되어야하기때문에 Node에 레이블(Label)을 추가한다. 그리고 Cilium BGP에 필요한 리소스를 배포한다.
1# BGP 동작할 노드를 위한 label 설정
2kubectl label nodes k8s-ctr k8s-w0 k8s-w1 enable-bgp=true
3# 확인
4kubectl get node -l enable-bgp=true
5NAME STATUS ROLES AGE VERSION
6k8s-ctr Ready control-plane 120m v1.33.2
7k8s-w0 Ready <none> 113m v1.33.2
8k8s-w1 Ready <none> 117m v1.33.2
graph TD
A[CiliumBGPClusterConfig<br>cilium-bgp] --> B(CiliumBGPPeerConfig<br>cilium-peer)
B --> C(CiliumBGPAdvertisement<br>bgp-advertisements)
각 리소스는 아래와 같은 의존성 관계를 가진다.
CiliumBGPClusterConfig의 peer에 대한 정보는CiliumBGPPeerConfig에 명세되어 있고CiliumBGPPeerConfig의 advertise에 대한 정보든CiliumBGPAdvertisement에 명세되어 있어서 상호 참조(Ref)한다.
▶ CiliumBGPClusterConfig , CiliumBGPPeerConfig , CiliumBGPAdvertisement 리소스 배포
1# Config Cilium BGP
2cat << EOF | kubectl apply -f -
3apiVersion: cilium.io/v2
4kind: CiliumBGPAdvertisement
5metadata:
6 name: bgp-advertisements
7 labels:
8 advertise: bgp
9spec:
10 advertisements:
11 - advertisementType: "PodCIDR"
12---
13apiVersion: cilium.io/v2
14kind: CiliumBGPPeerConfig
15metadata:
16 name: cilium-peer
17spec:
18 timers:
19 holdTimeSeconds: 9
20 keepAliveTimeSeconds: 3
21 ebgpMultihop: 2
22 gracefulRestart:
23 enabled: true
24 restartTimeSeconds: 15
25 families:
26 - afi: ipv4
27 safi: unicast
28 advertisements:
29 matchLabels:
30 advertise: "bgp"
31---
32apiVersion: cilium.io/v2
33kind: CiliumBGPClusterConfig
34metadata:
35 name: cilium-bgp
36spec:
37 nodeSelector:
38 matchLabels:
39 enable-bgp: true
40 bgpInstances:
41 - name: "instance-65001"
42 localASN: 65001
43 peers:
44 - name: "tor-switch"
45 peerASN: 65000
46 peerAddress: 192.168.10.200 # router ip address
47 peerConfigRef:
48 name: "cilium-peer"
49EOF
Cilium BGP 리소스 배포 후 확인
BGP 연결 확인 (Listening 상태는 없음)
1# 2ss -tnp | grep 179 3ESTAB 0 0 192.168.10.100:57457 192.168.10.200:179 users:(("cilium-agent",pid=5807,fd=62)) 4 5for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i 'ss -tnp | grep 179'; echo; done 6>> node : k8s-w1 << 7ESTAB 0 0 192.168.10.101:47755 192.168.10.200:179 8 9>> node : k8s-w0 << 10ESTAB 0 0 192.168.20.100:38965 192.168.10.200:179Cilium BGP 정보 확인
1cilium bgp peers 2Node Local AS Peer AS Peer Address Session State Uptime Family Received Advertised 3k8s-ctr 65001 65000 192.168.10.200 established 5m2s ipv4/unicast 4 2 4k8s-w0 65001 65000 192.168.10.200 established 5m2s ipv4/unicast 4 2 5k8s-w1 65001 65000 192.168.10.200 established 5m2s ipv4/unicast 4 2 6 7# 현재 이용 가능한 BGP IPv4 Unicast 라우팅 정보 8cilium bgp routes available ipv4 unicast 9Node VRouter Prefix NextHop Age Attrs 10k8s-ctr 65001 172.20.0.0/24 0.0.0.0 5m19s [{Origin: i} {Nexthop: 0.0.0.0}] 11k8s-w0 65001 172.20.2.0/24 0.0.0.0 5m19s [{Origin: i} {Nexthop: 0.0.0.0}] 12k8s-w1 65001 172.20.1.0/24 0.0.0.0 5m19s [{Origin: i} {Nexthop: 0.0.0.0}] 13 14# CR 배포 확인 15kubectl get ciliumbgpadvertisements,ciliumbgppeerconfigs,ciliumbgpclusterconfigs 16 17# Peering 상태확인 18kubectl get ciliumbgpnodeconfigs -o yaml 19... 20 bgpInstances: 21 - localASN: 65001 22 name: instance-65001 23 peers: 24 - establishedTime: "2025-08-16T04:50:14Z" 25 name: tor-switch 26 peerASN: 65000 27 peerAddress: 192.168.10.200 28 peeringState: **established** 29...frr 로그
1# [router] 2journalctl -u frr -f 3Aug 16 13:50:15 router bgpd[4767]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.101 in vrf default 4Aug 16 13:50:15 router bgpd[4767]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.100 in vrf default 5Aug 16 13:50:15 router bgpd[4767]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.20.100 in vrf default[ router ]에 BGP 관련 라우팅 추가 확인
1ip -c route | grep bgp 2172.20.0.0/24 nhid 31 via 192.168.10.100 dev eth1 proto bgp metric 20 3172.20.1.0/24 nhid 30 via 192.168.10.101 dev eth1 proto bgp metric 20 4172.20.2.0/24 nhid 32 via 192.168.20.100 dev eth2 proto bgp metric 20vtysh를 통한 frr BGP 설정 확인1**vtysh -c 'show ip bgp summary'** 2Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc 3192.168.10.100 4 65001 422 424 0 0 0 00:20:55 1 4 N/A 4192.168.10.101 4 65001 422 424 0 0 0 00:20:55 1 4 N/A 5192.168.20.100 4 65001 422 424 0 0 0 00:20:55 1 4 N/A 6 7**vtysh -c 'show ip bgp'** 8 Network Next Hop Metric LocPrf Weight Path 9*> 10.10.1.0/24 0.0.0.0 0 32768 i 10*> 172.20.0.0/24 192.168.10.100 0 65001 i 11*> 172.20.1.0/24 192.168.10.101 0 65001 i 12*> 172.20.2.0/24 192.168.20.100 0 65001 iBGP 정보 전달 확인 (tcpdump)
1# [k8s-ctr] 2tcpdump -i eth1 tcp port 179 -w /tmp/bgp.pcap 3 4# [router] : frr 재시작 5**systemctl restart frr** && journalctl -u frr -ftermshark를 통해서
bgp.type == 2로 필터링 해서 확인해보았을때, PodCIDR과 LoopBack에 대해서 Advertisement가 정상적으로 잘된걸 확인할 수 있다.
Cilium BGP 설정 후 다시 통신요청을 해보아도 정상적으로 동작하지 않는다.

이유는?
- Cilium의 BGP는 기본적으로 외부 경로를 커널 라우팅 테이블에 주입하지 않는다.
- 왜 Cilium이 받은 BGP 경로가 k8s 노드의 OS 커널 라우팅 테이블에 주입되지 않는지?
- Cilium BGP는 “Control Plane” 만 동작
- Cilium BGP Speaker(GoBGP 기반)는 BGP 세션을 맺고 prefix를 광고하거나 수신
- 하지만 수신한 경로를 Linux 커널(FIB) 에 바로 주입하지 않음.
- 대신 Cilium 내부에서 LoadBalancer 서비스 광고, PodCIDR 전파 같은 용도로만 사용
- Pod/Service 네트워크 경로는 Cilium eBPF가 처리
- Cilium은 kube-proxy 대체 모드에서 eBPF datapath로 패킷을 라우팅
- 외부 경로 학습이 커널 라우팅 테이블에 없어도, eBPF map에 저장된 다음 홉 정보로 처리 가능.
- GoBGP 기본 설정도 FIB 설치 비활성화
- Cilium이 사용하는 GoBGP 라이브러리는
disable-telemetry,disable-fib상태로 빌드된다. - 즉, 외부 라우터에서 들어온 BGP NLRI(Network Layer Reachability Information)는 커널에 반영되지 않고, Cilium 내부 정책/광고 로직에서만 사용된다.
- Cilium이 사용하는 GoBGP 라이브러리는
- Cilium BGP는 “Control Plane” 만 동작
- (스터디의 자료에서 언급되었지만 설득력이 느껴졌던 생각으로는) 실제 운영환경에서 서버는 네트워크 장비를 Default Gateway로 두게 되어서, K8S Cilium Node가 굳이 BGP로 상세 라우팅 정보를 가질 필요가 없다.
통신 문제 해결 후 확인

결론적으로 Cilium BGP Control Plane으로 BGP 이용 시, 2개 이상의 NIC를 사용하는 경우 Node에 직접 라우팅 설정 및 관리가 필요하다.
- 현재 실습환경에서는 2개의 NIC(eth0, eth1)를 사용하고 있으므로, default GW가 eth0 경로로 설정되어 있다.
- eth0 : Vagrant(VirtualBox)에서 필요한 통신에 사용되는 네트워크 인터페이스
- eth1 : K8S 통신 용도로 사용. 즉, 현재 Pod 사용 대역 통신 전체는 eth1를 통해서 라우팅을 설정하면된다.
- 해당 라우팅을 상단에 네트워크 장비가 받게 되고, 해당 장비는 Cilium Node를 통해 모든 PodCIDR 정보를 알고 있기에, 목적지로 전달 가능함.
1# k8s 파드 사용 대역 통신 전체는 eth1을 통해서 라우팅 설정
2ip route add 172.20.0.0/16 via 192.168.10.200
3# Worker Node (w1, w0)에도 설정
4sshpass -p 'vagrant' ssh vagrant@k8s-w1 sudo ip route add 172.20.0.0/16 via 192.168.10.200
5sshpass -p 'vagrant' ssh vagrant@k8s-w0 sudo ip route add 172.20.0.0/16 via 192.168.20.200
6
7# 통신 확인
8kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
9Hostname: webpod-697b545f57-gqmg8
10---
11Hostname: webpod-697b545f57-nj2pc
12---
13Hostname: webpod-697b545f57-hb9th
14---
15...
실습 2 : 노드 유지보수
Cilium BGP Control Plane 모드 사용중 Node에 대한 유지보수가 필요한 상황을 시나리오로 가정한다. 아래의 시나리오 단계별로 진행시 패킷 손실을 최대한으로 방지할 수 있다.
- 대상노드 : [k8s-w0]
Node의 Workload를 제거하기위해 Drain 진행한다.
1**kubectl drain k8s-w0 --ignore-daemonsets**Node에
CiliumBGPPeerConfig혹은CiliumBGPClusterConfig의 NodeSelector로 선택하지 못하게 레이블을 수정한다. 수정 후 Node의 BGP 세션은 종료된다.1**kubectl label nodes k8s-w0 enable-bgp=false --overwrite**BGP 피어가 제거될때까지 기다린다.
1# Drain 확인 2**kubectl get node** 3NAME STATUS ROLES AGE VERSION 4k8s-ctr Ready control-plane 21h v1.33.2 5k8s-w0 Ready,SchedulingDisabled <none> 21h v1.33.2 6k8s-w1 Ready <none> 21h v1.33.2 7 8# Cilium BGP Node에서 제거 확인 9**kubectl get ciliumbgpnodeconfigs** 10NAME AGE 11k8s-ctr 118m 12k8s-w1 118m 13 14# Cilium BGP Route에서 제거 확인 15**cilium bgp routes** 16Node VRouter Prefix NextHop Age Attrs 17k8s-ctr 65001 172.20.0.0/24 0.0.0.0 1h59m9s [{Origin: i} {Nexthop: 0.0.0.0}] 18k8s-w1 65001 172.20.1.0/24 0.0.0.0 1h59m9s [{Origin: i} {Nexthop: 0.0.0.0}] 19 20# Cilium BGP Peer에서 제거 확인 21**cilium bgp peers** 22Node Local AS Peer AS Peer Address Session State Uptime Family Received Advertised 23k8s-ctr 65001 65000 192.168.10.200 established 1h35m6s ipv4/unicast 3 2 24k8s-w1 65001 65000 192.168.10.200 established 1h35m7s ipv4/unicast 3 2 25 26# [ router ] k8s-w0으로의 라우팅 제거 확인 27**ip -c route | grep bgp** 28172.20.0.0/24 nhid 96 via 192.168.10.100 dev eth1 proto bgp metric 20 29172.20.1.0/24 nhid 94 via 192.168.10.101 dev eth1 proto bgp metric 20
유지보수가 끝나고 다시 [k8s-w0] 노드를 피어에 등록하기 위해서는 진행했던 부분을 반대로 진행하면된다.
1# 원복 설정
2**kubectl label nodes k8s-w0 enable-bgp=true --overwrite**
3**kubectl uncordon k8s-w0
4
5# 확인**
6kubectl get node
7kubectl get ciliumbgpnodeconfigs
8cilium bgp routes
9**cilium bgp peers**
10
11# [ router ] k8s-w0으로의 라우팅 확인
12**ip -c route | grep bgp**
13
14# 노드별 파드 분배 실행
15kubectl get pod -owide
16kubectl scale deployment webpod --replicas 0
17kubectl scale deployment webpod --replicas 3
Disabling CRD Status Report
CRD Status Reporting은 트러블슈팅(TroubleShooting)에 매우 유용하다. (기본값 enable ) 하지만, 노드가 많이 운용되는 대규모 클러스터의 경우나 BGP Policy가 많은 경우 CRD Status Reporting은 API Server에 많은 부하를 발생시킨다.
1# 현재 status Report로 출력되고 있음.
2**kubectl get ciliumbgpnodeconfigs k8s-ctr -o yaml | grep -A10 status:**
3status:
4 bgpInstances:
5 - localASN: 65001
6 name: instance-65001
7 peers:
8 - establishedTime: "2025-08-16T06:55:05Z"
9 name: tor-switch
10 peerASN: 65000
11 peerAddress: 192.168.10.200
12 peeringState: established
13 routeCount:
14
15# Helm Chart Upgrade
16helm upgrade cilium cilium/cilium --version 1.18.0 --namespace kube-system --reuse-values \
17 **--set bgpControlPlane.statusReport.enabled=false
18
19kubectl -n kube-system rollout restart ds/cilium**
20
21# 확인 -> Status 정보가 없어짐
22**kubectl get ciliumbgpnodeconfigs k8s-ctr -o yaml | grep -A10 status:**
23status: {}
실습 3 : Service Virtual IPs (LoadBalancer, External IP) 를 BGP로 광고

BGP Control Plane은 BGP 피어의 Virtual IP 주소를 BGP로 광고할 수 있다. 이렇게 설정을 한다면 K8S 클러스터 외부에서 Service로 직접접근이 가능하다.
광고가능한 종류 (advertisementType)으로는 아래의 종류가 있다.
- ClusterIP
- LoadBalancerIP
- ExternalIP
✅ Cilium BGP Control Plane VIPs에 대해서 개별 주소에 대해서 라우팅만 광고된다. (Subnetting)
- IPv4 : /32
- IPv6 : /128
LoadBalancerIP
LoadBalancer IP를 BGP로 광고하기 이전에 LB IPAM를 설정한다.
{{ details “Cilium LoadBalancer IPAM 설정” }}
1cat << EOF | kubectl apply -f -
2apiVersion: "cilium.io/v2"
3kind: CiliumLoadBalancerIPPool
4metadata:
5 name: "cilium-pool"
6spec:
7 allowFirstLastIPs: "No"
8 blocks:
9 - cidr: "172.16.1.0/24" # 노드의 네트워크 대역이 아니여도 괜찮다.
10EOF
1# 확인
2kubectl get ippool
3NAME DISABLED CONFLICTING IPS AVAILABLE AGE
4cilium-pool false False 254 5s
webpod의 Service를 LoadBalancer 타입으로 변경한다.
1kubectl patch svc webpod -p '{"spec": {"type": "LoadBalancer"}}'
2# External IP 할당됨
3kubectl get svc webpod
4NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
5webpod LoadBalancer 10.96.255.83 172.16.1.1 80:30681/TCP 21h
6
7# ippool을 조회해보면 `IPS AVAILABLE` 값이 1 줄어든것을 확인
8kubectl get ippool
9NAME DISABLED CONFLICTING IPS AVAILABLE AGE
10cilium-pool false False 253 93s
11
12# Ciliu의 서비스 목록 조회
13kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list
14ID Frontend Service Type Backend
15...
1617 172.16.1.1:80/TCP LoadBalancer 1 => 172.20.0.170:80/TCP (active)
17 2 => 172.20.1.2:80/TCP (active)
18 3 => 172.20.1.68:80/TCP (active)
통신 확인
1# [k8s-ctr] -> 정상
2kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
3LBIP=$(kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
4**curl -s $LBIP**
5Hostname: webpod-697b545f57-tc9wb
6IP: 127.0.0.1
7IP: ::1
8IP: 172.20.1.68
9IP: fe80::7cbc:baff:fe8b:5b50
10RemoteAddr: 192.168.10.100:40958
11GET / HTTP/1.1
12Host: 172.16.1.1
13User-Agent: curl/8.5.0
14Accept: */*
15
16# [router] -> 외부요청은 동작 X
17LBIP=172.16.1.1
18curl -s $LBIP
{{ /details}}
IPAM 설정 후 LB의 External IP를 BGP를 통해 광고한다.
1cat << EOF | kubectl apply -f -
2apiVersion: cilium.io/v2
3kind: **CiliumBGPAdvertisement**
4metadata:
5 name: **bgp-advertisements-lb-exip-webpod**
6 labels:
7 advertise: bgp
8spec:
9 advertisements:
10 - advertisementType: "**Service**"
11 service:
12 addresses:
13 - **LoadBalancerIP
14 selector:
15 matchExpressions:
16 - { key: app, operator: In, values: [ webpod ] }**
17EOF
확인
CiliumBGPAdvertisement 생성 확인
1**kubectl get CiliumBGPAdvertisement** 2NAME AGE 3bgp-advertisements 164m 4bgp-advertisements-lb-exip-webpod 108sCilium BGP의 Route 정책 조회
1 2**kubectl exec -it -n kube-system ds/cilium -- cilium-dbg bgp route-policies** 3... 4*65001 tor-switch-ipv4-PodCIDR export 192.168.10.200/32 172.20.1.0/24 (24..24) accept* 565001 tor-switch-ipv4-Service-webpod-default-LoadBalancerIP export 192.168.10.200/32 172.16.1.1/32 (32..32) accept각 노드마다 LB External IP가 라우팅 설정 확인
1**cilium bgp routes available ipv4 unicast** 2Node VRouter Prefix NextHop Age Attrs 3k8s-ctr 65001 172.16.1.1/32 0.0.0.0 6m47s [{Origin: i} {Nexthop: 0.0.0.0}] 4 65001 172.20.0.0/24 0.0.0.0 2h49m13s [{Origin: i} {Nexthop: 0.0.0.0}] 5k8s-w0 65001 172.16.1.1/32 0.0.0.0 56s [{Origin: i} {Nexthop: 0.0.0.0}] 6 65001 172.20.2.0/24 0.0.0.0 56s [{Origin: i} {Nexthop: 0.0.0.0}] 7k8s-w1 65001 172.16.1.1/32 0.0.0.0 6m47s [{Origin: i} {Nexthop: 0.0.0.0}] 8 65001 172.20.1.0/24 0.0.0.0 2h49m13s [{Origin: i} {Nexthop: 0.0.0.0}][ router ]
vtysh확인1# frr의 bgp에도 추가 확인 2vtysh -c 'show ip bgp' 3 Network Next Hop Metric LocPrf Weight Path 4*> 10.10.1.0/24 0.0.0.0 0 32768 i 5*= 172.16.1.1/32 192.168.20.100 0 65001 i 6... 7 8# LB External IP 대상으로 BGP 경로 조회 9vtysh -c 'show ip bgp 172.16.1.1/32' 10BGP routing table entry for 172.16.1.1/32, version 8 11Paths: (3 available, best #3, table default) 12 Advertised to non peer-group peers: 13 192.168.10.100 192.168.10.101 192.168.20.100 14 65001 15 192.168.20.100 from 192.168.20.100 (192.168.20.100) 16 Origin IGP, valid, external, multipath 17 Last update: Sat Aug 16 16:38:34 2025 18 65001 19 192.168.10.100 from 192.168.10.100 (192.168.10.100) 20 Origin IGP, valid, external, multipath 21 Last update: Sat Aug 16 16:32:39 2025 22 65001 23 192.168.10.101 from 192.168.10.101 (192.168.10.101) 24 Origin IGP, valid, external, multipath, best (Older Path) 25 Last update: Sat Aug 16 16:32:39 2025LB External IP의 BGP 광고 전후 [ router ]의 라우팅 테이블 비교
1# External IP에 대해서 nexthop이 생성됨 2diff before.txt after.txt 3> 172.16.1.1 nhid 102 proto bgp metric 20 4> nexthop via 192.168.10.101 dev eth1 weight 1 5> nexthop via 192.168.10.100 dev eth1 weight 1
LB의 External IP를 BGP를 통해서 광고 후 외부에서 호출되는지 한번 더 확인
1# [router] -> 정상적으로 동작
2LBIP=172.16.1.1
3curl -s $LBIP
4Hostname: webpod-697b545f57-ggqwg
5IP: 127.0.0.1
6IP: ::1
7IP: 172.20.1.114
8IP: fe80::80c0:5aff:febc:74c
9RemoteAddr: 192.168.10.100:52812
10GET / HTTP/1.1
11Host: 172.16.1.1
12User-Agent: curl/8.5.0
13Accept: */*
14
15# 반복호출로 로드밸런서 동작 확인 -> 분산되는것처럼 보임
16for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
17 34 Hostname: webpod-697b545f57-tsvkg
18 34 Hostname: webpod-697b545f57-ggqwg
19 32 Hostname: webpod-697b545f57-5dgz7
광고된 LB의 External IP에 대한 요청의 부하분산은 정상적인가?
위의 시나리오에서는 각 Node마다 webpod가 배포된 상태(repilicas=3)에서 부하 분산되는 것처럼 보였다.
replicas를 2로 줄여보고 부하분산은 또 어떻게 되는지 확인해보면1kubectl scale deployment webpod --replicas 2 2 3# [k8s-ctr]에 배포되었던 Pod가 삭제됨 4kubectl get pod -owide 5webpod-697b545f57-5mfm6 1/1 Running 0 2m26s 172.20.2.171 k8s-w0 <none> <none> 6webpod-697b545f57-p59p6 1/1 Running 0 2m26s 172.20.1.191 k8s-w1 <none> <none>[ k8s-ctr ]에 배포되었던 Pod는 삭제가 되었지만 BGP Routing에는 계속 존재한다.
1cilium bgp routes 2k8s-ctr 65001 172.16.1.1/32 0.0.0.0 4h43m43s [{Origin: i} {Nexthop: 0.0.0.0}] 3 65001 172.20.0.0/24 0.0.0.0 7h26m9s [{Origin: i} {Nexthop: 0.0.0.0}] 4k8s-w0 65001 172.16.1.1/32 0.0.0.0 4h37m51s [{Origin: i} {Nexthop: 0.0.0.0}] 5 65001 172.20.2.0/24 0.0.0.0 4h37m51s [{Origin: i} {Nexthop: 0.0.0.0}] 6k8s-w1 65001 172.16.1.1/32 0.0.0.0 4h43m42s [{Origin: i} {Nexthop: 0.0.0.0}] 7 65001 172.20.1.0/24 0.0.0.0 7h26m8s [{Origin: i} {Nexthop: 0.0.0.0}]그리고 FRR에 설정된 BGP Routing도 여전히 [ k8s-ctr ] 대상으로 남아있는것으로 확인된다.
1# [router]에서 확인 2route -n 3172.16.1.1 nhid 108 proto bgp metric 20 4172.20.0.0/24 nhid 96 via 192.168.10.100 dev eth1 proto bgp metric 20 # <- 5172.20.1.0/24 nhid 94 via 192.168.10.101 dev eth1 proto bgp metric 20 6172.20.2.0/24 nhid 107 via 192.168.20.100 dev eth2 proto bgp metric 20 7 8# BGP Routing 9vtysh -c 'show ip route bgp' 10B>* 172.16.1.1/32 [20/0] via 192.168.10.100, eth1, weight 1, 00:18:00 # <- 11 * via 192.168.10.101, eth1, weight 1, 00:18:00 12 * via 192.168.20.100, eth2, weight 1, 00:18:00 # <- 13B>* 172.20.0.0/24 [20/0] via 192.168.10.100, eth1, weight 1, 00:18:00 # <- 14B>* 172.20.1.0/24 [20/0] via 192.168.10.101, eth1, weight 1, 03:09:18 15B>* 172.20.2.0/24 [20/0] via 192.168.20.100, eth2, weight 1, 00:45:27 # <-
External Traffic Policy와 ECMP 설정
curl-pod에서 webpod의 (BGP Announced) LB의 External IP로 요청 보내 요청을 받은 RemoteAddr 값을 확인해본다. 현재 webpod는 [ k8s-w1 ]과 [ k8s-w0 ]에 각각 Pod로 배포되어있는데 요청을 응답받은 RemoteAddr 의 값이 이상하다.
- [ k8s-w1 ]의 eth1은
192.168.10.101이고 [ k8s-w0 ]의 eth1은192.168.20.100이다. - 하지만, 현재 요청에 응답받은
RemoteAddr은 [ k8s-ctr ]의 eth1 주소인192.168.10.100을 출력한다.
1# RemoteAddr이 192.168.10.100으로 출력
2while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
3Hostname: webpod-697b545f57-p59p6
4RemoteAddr: 192.168.10.100:54810
5...
6Hostname: webpod-697b545f57-5mfm6
7RemoteAddr: 192.168.10.100:54838
1# [k8s-w1], [k8s-w0]에 tcpdump를 통해 확인 -> Source IP가 192.168.10.100
2tcpdump -i eth1 -A -s 0 -nn 'tcp port 80'
3...
421:43:29.928248 IP 192.168.10.100.35696 > 172.20.2.171.80: Flags [F.], seq 74, ack 328, win 1002, options [nop,nop,TS val 1735956835 ecr 1103763912], length 0
5E..4..@.>.....
6d.....p.P.....].A....j4.....
7gx.cA...
External Traffic Policy = Local
기본적으로 Service는 externalTrafficPolicy 값이 Cluster 로 설정되어 있는데 이는 Cilium BGP Control Plane에서 해당 노드에 Endpoint가 없는 경우에도 Routing이 살아있어 추가 Network Hop을 가지게 된다.
externalTrafficPolicy: Local 로 설정한 경우 노드 Local에서 해당 서비스의 엔드포인트를 추적하고 없으면 광고를 중단하게 된다.
1kubectl patch service webpod -p '{"spec":{"externalTrafficPolicy":"Local"}}'
2
3kubectl patch service webpod -p '{"spec":{"externalTrafficPolicy":"Cluster"}}'
[ router ]에서
vtysh로 확인1# LB External IP(172.16.1.1/32) 대상으로 [ k8s-ctr ](192.168.10.100)의 Next Hop이 없어짐 2vtysh -c 'show ip bgp' 3 Network Next Hop Metric LocPrf Weight Path 4*= 172.16.1.1/32 192.168.20.100 0 65001 i 5*> 192.168.10.101 0 65001 i 6... 7 8# (동일 내용) 다른 확인 명령어 9vtysh -c 'show ip bgp 172.16.1.1/32' 10vtysh -c 'show ip route bgp' 11ip -c route
externalTrafficPolicy: Local 설정을 하면 다음 2가지 변경사항이 발생한다.
리눅스 커널 라우팅에서 캐싱되면서 한쪽 노드로 몰리는 현상이 발생한다.
기존
externalTrafficPolicy: Cluster에서는 k8s-ctr 으로 캐싱되면서 다른 노드로 라우팅되었지만, 엔드포인트가 제거되면서 첫번째로 라우팅 되는 노드 ([ k8s-w1 ] or [ k8s-w0 ])에 리눅스 커널 라우팅 캐시가 설정된다.1# [ router ] 2LBIP=172.16.1.1 3curl -s $LBIP 4for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr 5# 한쪽 Pod로 요청이 몰림 6 100 Hostname: webpod-697b545f57-5mfm6 7 8# 리눅스 커널 라우팅 캐싱 상태 9ip route get 172.16.1.1 10172.16.1.1 via 192.168.10.101 dev eth1 src 192.168.10.200 uid 0 11 cache # <-
기존 엔드포인트가 없던 노드에서 다시 라우팅되는 경우 SNAT이 발생하면서 Source IP가 변경되었지만, 현재 엔드포인트가 활성화된 노드로 직접 통신을 하기때문에 최초의 요청이 바로 해당 Pod가 실행중인 노드로 요청된다.
ECMP Hash Policy
externalTrafficPolicy: Local 를 설정했을때의 문제점 ‘리눅스 커널 라우팅 캐싱으로 한쪽 노드에 몰리는 문제’를 해결할 수 있는 방법은?
이전에도 언급했던 **ECMP(Equal Cost MultiPath)**를 통해서 동일 비용의 경로에 Hashing으로 요청을 분산시킬 수 있다.

리눅스 커널은 기본적으로 L3(목적지 IP 기반) 해시를 사용한다. 보다 정교한 부하분산을 원하면 L4 해시 (IP + 포트) 기반으로 설정한다.
ExternalTrafficPolicy(Local) 설정 시, Router 의 ECMP 에서 Hash Policy 경로 결정과 요청 트래픽 환경(소스 IP, 포트 등)으로 특정 동일 노드로만 라우팅 될 수 있음 → 대체로 Hash Policy 를 L4 수준 설정 권장.
1# [ router ]
2sudo sysctl -w net.ipv4.fib_multipath_hash_policy=1
3# echo "net.ipv4.fib_multipath_hash_policy=1" >> /etc/sysctl.conf
요청이후 ‘한쪽 노드로 요청이 몰리는 현상없이’ 정상적으로 부하분산되는걸 확인할 수 있다.
1for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
2 54 Hostname: webpod-697b545f57-5mfm6
3 46 Hostname: webpod-697b545f57-p59p6
다시 replicas=3 으로 원복하고 요청을 하면
1# [ k8s-ctr ]
2kubectl scale deployment webpod --replicas 3
3kubectl get pod -owide
4
5# 유사한 비중으로 부하분산
6for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
7 35 Hostname: webpod-697b545f57-p59p6
8 34 Hostname: webpod-697b545f57-5mfm6
9 31 Hostname: webpod-697b545f57-vdc4j
Ref
- https://isovalent.com/blog/post/connecting-your-kubernetes-island-to-your-network-with-cilium-bgp/
- ECMP
- Node Maintenence
- Disabling CRD Status Report
- https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane-v2/#service-virtual-ips
- https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane-v2/#service-virtual-ips
- https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane-v2/#externaltrafficpolicy-internaltrafficpolicy