본 내용은 gasida님의 Cilium 스터디를 진행 후 정리한 내용입니다.
꾸준히 업데이트 하면서 작성됩니다

환경구성
router : 192.168.10.0/24 ↔ 192.168.20.0/24 대역 라우팅 역할, k8s 에 join 되지 않은 서버, BGP 동작을 위한 frr 툴 설치
https://docs.frrouting.org/en/stable-10.4/about.html
--set autoDirectNodeRoutes=false
: bgp 사용으로 불필요
--set bgpControlPlane.enabled=true
-> =bgp 사용을 위해 true 로 옵션 변경
helm install cilium cilium/cilium --version $2 --namespace kube-system \
--set k8sServiceHost=192.168.10.100 --set k8sServicePort=6443 \
--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16 \
--set routingMode=native --set autoDirectNodeRoutes=false --set bgpControlPlane.enabled=true \
--set kubeProxyReplacement=true --set bpf.masquerade=true --set installNoConntrackIptablesRules=true \
--set endpointHealthChecking.enabled=false --set healthChecking=false \
--set hubble.enabled=true --set hubble.relay.enabled=true --set hubble.ui.enabled=true \
--set hubble.ui.service.type=NodePort --set hubble.ui.service.nodePort=30003 \
--set prometheus.enabled=true --set operator.prometheus.enabled=true --set hubble.metrics.enableOpenMetrics=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \
--set operator.replicas=1 --set debug.enabled=true >/dev/null 2>&1
router 서버
frr 설치 후 기본적인 bgp 설정 구성을 진행하였습니다.
echo "[TASK 7] Configure FRR"
apt install frr -y >/dev/null 2>&1
sed -i "s/^bgpd=no/bgpd=yes/g" /etc/frr/daemons
NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
cat << EOF >> /etc/frr/frr.conf
!
router bgp 65000
bgp router-id $NODEIP
bgp graceful-restart
no bgp ebgp-requires-policy
bgp bestpath as-path multipath-relax
maximum-paths 4
network 10.10.1.0/24
EOF
systemctl daemon-reexec >/dev/null 2>&1
systemctl restart frr >/dev/null 2>&1
systemctl enable frr >/dev/null 2>&1

cilium 상태 정보에 bgp 환경 구성을 확인할 수 있습니다.
"enable-bgp-control-plane": "true",
각 노드들의 route 정보를 확인해봅니다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c route
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
172.20.0.0/24 via 172.20.0.251 dev cilium_host proto kernel src 172.20.0.251
172.20.0.251 dev cilium_host proto kernel scope link
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100
192.168.20.0/24 via 192.168.10.200 dev eth1 proto static
>> node : k8s-w1 <<
Warning: Permanently added 'k8s-w1' (ED25519) to the list of known hosts.
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
172.20.1.0/24 via 172.20.1.115 dev cilium_host proto kernel src 172.20.1.115
172.20.1.115 dev cilium_host proto kernel scope link
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101
192.168.20.0/24 via 192.168.10.200 dev eth1 proto static
>> node : k8s-w0 <<
Warning: Permanently added 'k8s-w0' (ED25519) to the list of known hosts.
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
172.20.2.0/24 via 172.20.2.116 dev cilium_host proto kernel src 172.20.2.116
172.20.2.116 dev cilium_host proto kernel scope link
192.168.10.0/24 via 192.168.20.200 dev eth1 proto static
192.168.20.0/24 dev eth1 proto kernel scope link src 192.168.20.100
상대방의 PodCIDR이 보이지 않는 것을 확인할 수 있습니다.
-> autoDirectNodeRoutes=false
아래 샘플어플리케이션을 배포해서 실제로 노드 내에서만 통신이 되는지 확인해보겠습니다.
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webpod
spec:
replicas: 3
selector:
matchLabels:
app: webpod
template:
metadata:
labels:
app: webpod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- sample-app
topologyKey: "kubernetes.io/hostname"
containers:
- name: webpod
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: webpod
labels:
app: webpod
spec:
selector:
app: webpod
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
EOF
# k8s-ctr 노드에 curl-pod 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
labels:
app: curl
spec:
nodeName: k8s-ctr
containers:
- name: curl
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
ctr 노드에서 보낸 통신은 해당 노드에 있는 파드에만 전달되는 것을 확인할 수 있습니다
(파드CIDR가 라우팅 정보에 없으므로)

Cilium BGP Control Plane
https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane-v2/
Control Plane Resource : BGP v2
Legacy : BGP v1

CR을 동작하면 BGP 데몬이 동작한다.
- CiliumBGPClusterConfig: Defines BGP instances and peer configurations that are applied to multiple nodes.
- CiliumBGPPeerConfig: A common set of BGP peering setting. It can be used across multiple peers.
- CiliumBGPAdvertisement: Defines prefixes that are injected into the BGP routing table.
- CiliumBGPNodeConfigOverride: Defines node-specific BGP configuration to provide a finer control.
주의사항
BGP 설정 후 통신 확인 : Cilium의 BGP는 기본적으로 외부 경로를 커널 라우팅 테이블에 주입하지 않음.
router 노드 접근 후 설정
# k8s-ctr 에서 router 로 ssh 접속
sshpass -p 'vagrant' ssh vagrant@router
###
ss -tnlp | grep -iE 'zebra|bgpd'
ps -ef |grep frr
root 4127 1 0 13:38 ? 00:00:00 /usr/lib/frr/watchfrr -d -F traditional zebra bgpd staticd
frr 4140 1 0 13:38 ? 00:00:00 /usr/lib/frr/zebra -d -F traditional -A 127.0.0.1 -s 90000000
frr 4145 1 0 13:38 ? 00:00:00 /usr/lib/frr/bgpd -d -F traditional -A 127.0.0.1
frr 4152 1 0 13:38 ? 00:00:00 /usr/lib/frr/staticd -d -F traditional -A 127.0.0.1
### frr 의 설정정보
vtysh -c 'show running'
### cat /etc/frr/frr.conf
### bgp AS 65000 (router)
...
log syslog informational
!
router bgp 65000
bgp router-id 192.168.10.200
bgp graceful-restart
no bgp ebgp-requires-policy
bgp bestpath as-path multipath-relax
maximum-paths 4
###
vtysh -c 'show running'
vtysh -c 'show ip bgp summary'
% No BGP neighbors found in VRF default
### 광고한 정보를 확인 가능
### loopback1 interface를 광고
### vtysh -c 'show ip bgp'
BGP table version is 1, local router ID is 192.168.10.200, vrf id 0
Default local pref 100, local AS 65000
Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found
Network Next Hop Metric LocPrf Weight Path
*> 10.10.1.0/24 0.0.0.0 0 32768 i
### ip -c route
### vtysh -c 'show ip route'
...
K>* 0.0.0.0/0 [0/100] via 10.0.2.2, eth0, src 10.0.2.15, 00:34:45
...
C>* 192.168.10.0/24 is directly connected, eth1, 00:34:45
C>* 192.168.20.0/24 is directly connected, eth2, 00:34:45
### Cilium node 연동 설정 방안 1
### 연결되어야할 노드는 3개
### external 옵션 -> 같은 AS가 아닐때 설정
cat << EOF >> /etc/frr/frr.conf
neighbor CILIUM peer-group
neighbor CILIUM remote-as external
neighbor 192.168.10.100 peer-group CILIUM
neighbor 192.168.10.101 peer-group CILIUM
neighbor 192.168.20.100 peer-group CILIUM
EOF
cat /etc/frr/frr.conf
systemctl daemon-reexec && systemctl restart frr
systemctl status frr --no-pager --full
# 모니터링 걸어두기!
journalctl -u frr -f
### Cilium node 연동 설정 방안 2
### 직접설정
vtysh
---------------------------
?
show ?
show running
show ip route
# config 모드 진입
conf
?
## bgp 65000 설정 진입
router bgp 65000
?
neighbor CILIUM peer-group
neighbor CILIUM remote-as external
neighbor 192.168.10.100 peer-group CILIUM
neighbor 192.168.10.101 peer-group CILIUM
neighbor 192.168.20.100 peer-group CILIUM
end
# Write configuration to the file (same as write file)
write memory
exit
---------------------------
cat /etc/frr/frr.conf
모니터링을 걸어두고 cilium에 bgp를 설정합니다.
### 신규 터미널 1 (router) : 모니터링 걸어두기!
journalctl -u frr -f
### 신규 터미널 2 (k8s-ctr) : 반복 호출
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
### BGP 동작할 노드를 위한 label 설정
kubectl label nodes k8s-ctr k8s-w0 k8s-w1 enable-bgp=true
### kubectl get node -l enable-bgp=true
NAME STATUS ROLES AGE VERSION
k8s-ctr Ready control-plane 3h37m v1.33.2
k8s-w0 Ready <none> 3h32m v1.33.2
k8s-w1 Ready <none> 3h35m v1.33.2
### Config Cilium BGP
cat << EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumBGPAdvertisement
metadata:
name: bgp-advertisements
labels:
advertise: bgp
spec:
advertisements:
- advertisementType: "PodCIDR"
---
apiVersion: cilium.io/v2
kind: CiliumBGPPeerConfig
metadata:
name: cilium-peer
spec:
timers:
holdTimeSeconds: 9
keepAliveTimeSeconds: 3
ebgpMultihop: 2
gracefulRestart:
enabled: true
restartTimeSeconds: 15
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: "bgp"
---
apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata:
name: cilium-bgp
spec:
nodeSelector:
matchLabels:
"enable-bgp": "true"
bgpInstances:
- name: "instance-65001"
localASN: 65001
peers:
- name: "tor-switch"
peerASN: 65000
peerAddress: 192.168.10.200 # router ip address
peerConfigRef:
name: "cilium-peer"
EOF
모니터링 중인 router 노드에서 아래 정보를 확인할 수 있다.

위 3개의 CRD 가 합쳐서 라우터 노드(192.168.10.200)와 BGP 세션을 맺고 각 노드의 PodCIDR을 라우터에 광고하도록 합니다.
CiliumBGPAdvertisement
광고할 대상을 지정합니다.
kind: CiliumBGPAdvertisement
spec:
advertisements:
- advertisementType: "PodCIDR"
metadata.labels: advertise=bgp
- 광고 대상을 정의: 여기선 각 노드의 PodCIDR(예: 172.20.0.0/24, 172.20.1.0/24 …)을 라우터에 전파
- 라벨 advertise=bgp를 붙여두고, 아래 PeerConfig에서 이 라벨에 매칭된 광고만 사용합니다.
CiliumBGPPeerConfig
BGP 피어링 정책 설정합니다.
kind: CiliumBGPPeerConfig
spec:
timers:
holdTimeSeconds: 9
keepAliveTimeSeconds: 3
ebgpMultihop: 2
gracefulRestart:
enabled: true
restartTimeSeconds: 15
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: "bgp"
- 세션 파라미터:
- keepalive 3s / hold 9s → 세션 끊김 빠르게 감지
- eBGP multihop=2: L3 홉이 1개 이상일 때도 eBGP 성립 가능(같은 서브넷이 아니어도 됨)
- Graceful Restart: 재시작 시 학습 경로를 잠시 유지(플랩 완화)
- 주소 패밀리: IPv4 unicast
- 어떤 광고를 쓸지: matchLabels: advertise=bgp → 위의 Advertisement 객체와 연결.
CiliumBGPClusterConfig
세션 맺을 대상을 지정합니다.
kind: CiliumBGPClusterConfig
spec:
nodeSelector:
matchLabels:
enable-bgp: "true"
bgpInstances:
- name: "instance-65001"
localASN: 65001
peers:
- name: "tor-switch"
peerASN: 65000
peerAddress: 192.168.10.200
peerConfigRef:
name: "cilium-peer"
- 라벨 enable-bgp=true 가 붙은 노드들만 BGP 데몬 활성화합니다.
- 로컬 ASN / 피어
- 로컬 ASN: 65001 (K8s 노드 쪽)
- 피어 ASN: 65000 (외부 라우터)
- 피어 주소: 192.168.10.200 (router IP)
- 피어링 정책은 위의 cilium-peer 참고
통신확인
# BGP 연결 확인
ss -tnlp | grep 179
ss -tnp | grep 179
# cilium bgp 정보 확인
cilium bgp peers
cilium bgp routes available ipv4 unicast

- bgp peers : bgp 세션 상태와 송수신 개수
- bgp routes available : 노드별 PodCIDR 경로와 넥스트홉 정보 확인
kubectl get ciliumbgpadvertisements,ciliumbgppeerconfigs,ciliumbgpclusterconfigs
kubectl get ciliumbgpnodeconfigs -o yaml | yq
...
"peeringState": "established",
"routeCount": [
{
"advertised": 2,
"afi": "ipv4",
"received": 1,
"safi": "unicast"
}
...
# 신규 터미널 1 (router) : 모니터링 걸어두기!
journalctl -u frr -f
Aug 09 14:31:40 router bgpd[4665]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.20.100 in vrf default
Aug 09 14:31:40 router bgpd[4665]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.101 in vrf default
Aug 09 14:31:40 router bgpd[4665]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.100 in vrf default
ip -c route | grep bgp
172.20.0.0/24 nhid 32 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 30 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 31 via 192.168.20.100 dev eth2 proto bgp metric 20
vtysh -c 'show ip bgp summary'
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
192.168.10.100 4 65001 509 511 0 0 0 00:25:15 1 4 N/A
192.168.10.101 4 65001 508 511 0 0 0 00:25:15 1 4 N/A
192.168.20.100 4 65001 509 511 0 0 0 00:25:15 1 4 N/A
vtysh -c 'show ip bgp'
Network Next Hop Metric LocPrf Weight Path
*> 10.10.1.0/24 0.0.0.0 0 32768 i
*> 172.20.0.0/24 192.168.10.100 0 65001 i
*> 172.20.1.0/24 192.168.10.101 0 65001 i
*> 172.20.2.0/24 192.168.20.100 0 65001 i
# 신규 터미널 2 (k8s-ctr) : 반복 호출???
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
BGP 통신확인

### k8s-ctr
### tcpdump 해두기
tcpdump -i eth1 tcp port 179 -w /tmp/bgp.pcap
### router : frr 재시작
systemctl restart frr && journalctl -u frr -f
### termshark 검색
bgp.type == 2
termshark -r /tmp/bgp.pcap
# 분명 Router 장비를 통해 BGP UPDATE로 받음을 확인.
cilium bgp routes
ip -c route
Cilium의 BGP는 기본적으로 외부 경로(router에게 받은정보)를 커널 라우팅 테이블에 주입하지 않음.
- 왜 Cilium이 받은 BGP 경로가 K8s 노드 OS 커널 라우팅 테이블에 안 들어오나?
- Cilium의 BGP는 "컨트롤 플레인"만 동작
- 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는 커널에 반영되지 않고, Cilium 내부 정책/광고 로직에서만 사용.
- Cilium의 BGP는 "컨트롤 플레인"만 동작
서버는 네트워크 장비를 Default Gateway 로 두게 되어서, K8S Cilium Node가 굳이 BGP로 상세 라우팅 정보를 가질 필요가 없다!
-> 라우팅은 Default Gateway로 보내면 되기 때문에
직접 수동으로 라우팅을 잡아주어야한다.
-> Cilium 으로 BGP 사용 시, 2개 이상의 NIC 사용할 경우에는 Node에 직접 라우팅 설정 및 관리가 필요
- 현재 실습 환경은 2개의 NIC(eth0, eth1)을 사용하고 있는 상황으로, default GW가 eth0 경로로 설정 되어 있습니다.
- eth1은 k8s 통신 용도로 사용 중. 즉, 현재 k8s 파드 사용 대역 통신 전체는 eth1을 통해서 라우팅 설정하면됨.
- 해당 라우팅을 상단에 네트워크 장비가 받게 되고, 해당 장비는 Cilium Node를 통해 모든 PodCIDR 정보를 알고 있기에, 목적지로 전달 가능함.
- 결론은 Cilium 으로 BGP 사용 시, 2개 이상의 NIC 사용할 경우에는 Node에 직접 라우팅 설정 및 관리가 필요합니다.
# k8s 파드 사용 대역 통신 전체는 eth1을 통해서 라우팅 설정
ip route add 172.20.0.0/16 via 192.168.10.200
sshpass -p 'vagrant' ssh vagrant@k8s-w1 sudo ip route add 172.20.0.0/16 via 192.168.10.200
sshpass -p 'vagrant' ssh vagrant@k8s-w0 sudo ip route add 172.20.0.0/16 via 192.168.20.200
# router 가 bgp로 학습한 라우팅 정보 한번 더 확인 :
sshpass -p 'vagrant' ssh vagrant@router ip -c route | grep bgp
172.20.0.0/24 nhid 64 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 60 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 62 via 192.168.20.100 dev eth2 proto bgp metric 20
# 정상 통신 확인!
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# hubble relay 포트 포워딩 실행
cilium hubble port-forward&
hubble status
# flow log 모니터링
hubble observe -f --protocol tcp --pod curl-pod
노드 유지보수
노드 유지보수를 위한 설정
# 모니터링 : 반복 호출
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# (참고) BGP Control Plane logs
kubectl logs -n kube-system -l name=cilium-operator -f | grep "subsys=bgp-cp-operator"
kubectl logs -n kube-system -l k8s-app=cilium -f | grep "subsys=bgp-control-plane"
# 유지보수를 위한 설정
kubectl drain k8s-w0 --ignore-daemonsets
kubectl label nodes k8s-w0 enable-bgp=false --overwrite
# 확인
kubectl get node
kubectl get ciliumbgpnodeconfigs
cilium bgp routes
cilium bgp peers
Node Local AS Peer AS Peer Address Session State Uptime Family Received Advertised
k8s-ctr 65001 65000 192.168.10.200 established 2h13m35s ipv4/unicast 3 2
k8s-w1 65001 65000 192.168.10.200 established 2h13m36s ipv4/unicast 3 2
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp summary'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip route bgp'"
sshpass -p 'vagrant' ssh vagrant@router ip -c route | grep bgp
172.20.0.0/24 nhid 64 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 60 via 192.168.10.101 dev eth1 proto bgp metric 20
-> bgp 노드 config 에서 제외됩니다.
통신 상 별다른 이상이 발생하지않는 것을 확인할 수 있습니다.
원복
# 원복 설정
kubectl label nodes k8s-w0 enable-bgp=true --overwrite
kubectl uncordon k8s-w0
# 확인
kubectl get node
kubectl get ciliumbgpnodeconfigs
cilium bgp routes
cilium bgp peers
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp summary'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip route bgp'"
sshpass -p 'vagrant' ssh vagrant@router ip -c route | grep bgp
# 노드별 파드 분배 실행
kubectl get pod -owide
kubectl scale deployment webpod --replicas 0
kubectl scale deployment webpod --replicas 3
Descheduler for Kubernetes : Pod의 상태를 확인하여 조건에 성립하는 Pod를 Eviction하여 원하는 상태로 만듬
Descheduler 활용
https://github.com/kubernetes-sigs/descheduler GitHub - kubernetes-sigs/descheduler: Descheduler for Kubernetes Descheduler for Kubernetes. Contribute to kubernetes-sigs/descheduler development by creating an account on GitHub. github.com Kubernetes Schedu
ybchoi.com

Disabling CRD Status Report
노드가 많은 대규모 클러스터의 경우, api 서버에 부하 유발할 수 있으니
bgp status reporting off 권장
# 확인
kubectl get ciliumbgpnodeconfigs -o yaml | yq
# 설정
helm upgrade cilium cilium/cilium --version 1.18.0 --namespace kube-system --reuse-values \
--set bgpControlPlane.statusReport.enabled=false
kubectl -n kube-system rollout restart ds/cilium
# 확인 : CiliumBGPNodeConfig Status 정보가 없다!
kubectl get ciliumbgpnodeconfigs -o yaml | yq
...
"status": {}

Service(LoadBalancer - ExternalIP) IPs 를 BGP로 광고

### LB IPAM Announcement over BGP 설정 예정으로,
### 노드의 네트워크 대역이 아니여도 가능!
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
name: "cilium-pool"
spec:
allowFirstLastIPs: "No"
blocks:
- cidr: "172.16.1.0/24"
EOF
### /24 -> 254개
### kubectl get ippool
NAME DISABLED CONFLICTING IPS AVAILABLE AGE
cilium-pool false False 254 8s
###
kubectl patch svc webpod -p '{"spec": {"type": "LoadBalancer"}}'
kubectl get svc webpod
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webpod LoadBalancer 10.96.39.92 172.16.1.1 80:30800/TCP 3h56m
### 172.16.1.1 로 1개 발급 254->253
### kubectl get ippool
NAME DISABLED CONFLICTING IPS AVAILABLE AGE
cilium-pool false False 253 2m23s
### kubectl describe svc webpod | grep 'Traffic Policy'
External Traffic Policy: Cluster
Internal Traffic Policy: Cluster
### kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list
ID Frontend Service Type Backend
1 10.96.73.155:443/TCP ClusterIP 1 => 172.20.0.136:10250/TCP (active)
2 10.96.0.1:443/TCP ClusterIP 1 => 192.168.10.100:6443/TCP (active)
3 10.96.14.33:443/TCP ClusterIP 1 => 192.168.10.101:4244/TCP (active)
4 10.96.196.30:80/TCP ClusterIP 1 => 172.20.0.205:4245/TCP (active)
5 0.0.0.0:30003/TCP NodePort 1 => 172.20.0.54:8081/TCP (active)
8 10.96.85.141:80/TCP ClusterIP 1 => 172.20.0.54:8081/TCP (active)
9 10.96.0.10:53/TCP ClusterIP 1 => 172.20.0.92:53/TCP (active)
2 => 172.20.0.170:53/TCP (active)
10 10.96.0.10:53/UDP ClusterIP 1 => 172.20.0.92:53/UDP (active)
2 => 172.20.0.170:53/UDP (active)
11 10.96.0.10:9153/TCP ClusterIP 1 => 172.20.0.92:9153/TCP (active)
2 => 172.20.0.170:9153/TCP (active)
12 10.96.2.163:80/TCP ClusterIP 1 => 172.20.0.229:80/TCP (active)
2 => 172.20.1.158:80/TCP (active)
3 => 172.20.2.219:80/TCP (active)
13 0.0.0.0:31906/TCP NodePort 1 => 172.20.0.229:80/TCP (active)
2 => 172.20.1.158:80/TCP (active)
3 => 172.20.2.219:80/TCP (active)
16 172.16.1.1:80/TCP LoadBalancer 1 => 172.20.0.229:80/TCP (active)
2 => 172.20.1.158:80/TCP (active)
3 => 172.20.2.219:80/TCP (active)
# LBIP로 curl 요청 확인
kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
LBIP=$(kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -s $LBIP
curl -s $LBIP | grep Hostname
curl -s $LBIP | grep RemoteAddr
# 모니터링
watch "sshpass -p 'vagrant' ssh vagrant@router ip -c route"
# LB EX-IP를 BGP로 광고 설정
cat << EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumBGPAdvertisement
metadata:
name: bgp-advertisements-lb-exip-webpod
labels:
advertise: bgp
spec:
advertisements:
- advertisementType: "Service"
service:
addresses:
- LoadBalancerIP
selector:
matchExpressions:
- { key: app, operator: In, values: [ webpod ] }
EOF
kubectl get CiliumBGPAdvertisement
NAME AGE
bgp-advertisements 2m1s
bgp-advertisements-lb-exip-webpod 3s
# 확인
kubectl exec -it -n kube-system ds/cilium -- cilium-dbg bgp route-policies
VRouter Policy Name Type Match Peers Match Families Match Prefixes (Min..Max Len) RIB Action Path Actions
65001 allow-local import accept
65001 tor-switch-ipv4-PodCIDR export 192.168.10.200/32 172.20.1.0/24 (24..24) accept
65001 tor-switch-ipv4-Service-webpod-default-LoadBalancerIP export 192.168.10.200/32 172.16.1.1/32 (32..32) accept
### 모든 노드가 172.16.1.1 광고한다
### cilium bgp routes available ipv4 unicast
Node VRouter Prefix NextHop Age Attrs
k8s-ctr 65001 172.16.1.1/32 0.0.0.0 32s [{Origin: i} {Nexthop: 0.0.0.0}]
65001 172.20.0.0/24 0.0.0.0 24m41s [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0 65001 172.16.1.1/32 0.0.0.0 32s [{Origin: i} {Nexthop: 0.0.0.0}]
65001 172.20.2.0/24 0.0.0.0 24m56s [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1 65001 172.16.1.1/32 0.0.0.0 32s [{Origin: i} {Nexthop: 0.0.0.0}]
65001 172.20.1.0/24 0.0.0.0 24m56s [{Origin: i} {Nexthop: 0.0.0.0}]
### 현재 BGP가 동작하는 모든 노드로 전달 가능!
sshpass -p 'vagrant' ssh vagrant@router ip -c route
...
172.16.1.1 nhid 71 proto bgp metric 20
nexthop via 192.168.10.101 dev eth1 weight 1
nexthop via 192.168.10.100 dev eth1 weight 1
nexthop via 192.168.20.100 dev eth2 weight 1
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip route bgp'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp summary'"
sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp'"
Network Next Hop Metric LocPrf Weight Path # * valid, > best, = multipath
*> 172.16.1.1/32 192.168.10.100 0 65001 i
*= 192.168.20.100 0 65001 i
*= 192.168.10.101 0 65001 i
### sshpass -p 'vagrant' ssh vagrant@router "sudo vtysh -c 'show ip bgp 172.16.1.1/32'"
BGP routing table entry for 172.16.1.1/32, version 7
Paths: (3 available, best #1, table default)
Advertised to non peer-group peers:
192.168.10.100 192.168.10.101 192.168.20.100
65001
192.168.10.100 from 192.168.10.100 (192.168.10.100)
Origin IGP, valid, external, multipath, best (Router ID)
Last update: Sat Aug 9 17:50:29 2025
65001
192.168.20.100 from 192.168.20.100 (192.168.20.100)
Origin IGP, valid, external, multipath
Last update: Sat Aug 9 17:50:29 2025
65001
192.168.10.101 from 192.168.10.101 (192.168.10.101)
Origin IGP, valid, external, multipath
Last update: Sat Aug 9 17:50:29 2025
router 에서 LB EX-IP 호출 확인
#
LBIP=172.16.1.1
curl -s $LBIP
curl -s $LBIP | grep Hostname
curl -s $LBIP | grep RemoteAddr
# 반복 접속
for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done

분산이 잘 되는 것으로 생각할 수도 있다.
# k8s-ctr 에서 replicas=2 로 줄여보자
kubectl scale deployment webpod --replicas 2
kubectl get pod -owide
cilium bgp routes
# router 에서 정보 확인 : k8s-ctr 노드에 대상 파드가 배치되지 않았지만, 라우팅 경로 설정이 되어 있다.
ip -c route
vtysh -c 'show ip bgp summary'
vtysh -c 'show ip bgp'
vtysh -c 'show ip bgp 172.16.1.1/32'
vtysh -c 'show ip route bgp'
파드가 없는데 경로가 라우터에서 잡힙니다.
# 반복 접속 : ??? RemoteAddr 이 왜 10.100???
for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
Hostname: webpod-697b545f57-swtdz
RemoteAddr: 192.168.10.100:40460
Hostname: webpod-697b545f57-87lf2
RemoteAddr: 192.168.10.100:40474
...
# 신규터미널 (3개) : k8s-w1, k8s-w2, k8s-w0
tcpdump -i eth1 -A -s 0 -nn 'tcp port 80'
# k8s-ctr 를 경유하거나 등 확인 : ExternalTrafficPolicy 설정 확인
LBIP=172.16.1.1
curl -s $LBIP
curl -s $LBIP
curl -s $LBIP
curl -s $LBIP
이유는 ExternalPolicy : Cluster 라서
External Traffic Policy (Local) : 소스 IP 보존


# 모니터링
watch "sshpass -p 'vagrant' ssh vagrant@router ip -c route"
# k8s-ctr
kubectl patch service webpod -p '{"spec":{"externalTrafficPolicy":"Local"}}'
# router(frr) : 서비스에 대상 파드가 배치된 노드만 BGP 경로에 출력!
vtysh -c 'show ip bgp'
vtysh -c 'show ip bgp 172.16.1.1/32'
vtysh -c 'show ip route bgp'
ip -c route
# 신규터미널 (3개) : k8s-w1, k8s-w2, k8s-w0
tcpdump -i eth1 -A -s 0 -nn 'tcp port 80'
# 현재 실습 환경 경우 반복 접속 시 한쪽 노드로 선택되고, 소스IP가 보존!
LBIP=172.16.1.1
curl -s $LBIP
for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
## 아래 실행 시 tcpdump 에 다른 노드 선택되는지 확인! 안될수도 있음!
curl -s $LBIP --interface 10.10.1.200
curl -s $LBIP --interface 10.10.2.200

Linux ECMP Hash Policy 설정
ECMP(Equal-Cost Multi-Path) 는 같은 비용(코스트)의 여러 경로를 동시에 사용하는 라우팅 기법
## 기본 부하분산
root@router:~# for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
100 Hostname: webpod-697b545f57-lppz4
### router 서버
# 리눅스 커널은 기본적으로 L3(목적지 IP 기반) 해시를 사용합니다. 보다 정교한 부하분산을 원하면 L4 해시 (IP + 포트) 기반으로 설정
# 1 : source IP, dest IP, source port, dest port 기반 hash (more granular)로 변경
sudo sysctl -w net.ipv4.fib_multipath_hash_policy=1
echo "net.ipv4.fib_multipath_hash_policy=1" >> /etc/sysctl.conf
#
for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
59 Hostname: webpod-697b545f57-87lf2
41 Hostname: webpod-697b545f57-swtdz
# k8s-ctr
kubectl scale deployment webpod --replicas 3
kubectl get pod -owide
# router
ip -c route
for i in {1..100}; do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
37 Hostname: webpod-697b545f57-bgpv9
35 Hostname: webpod-697b545f57-87lf2
28 Hostname: webpod-697b545f57-swtdz
ExternalTrafficPolicy(Local) 설정 시, Router 의 ECMP 에서 Hash Policy 경로 결정과
요청 트래픽 환경(소스 IP, 포트 등)으로 특정 동일 노드로만 라우팅 될 수 있음
→ 대체로 Hash Policy 를 L4 수준 설정 권장.
비효율적인 트래픽이 발생하지 않는다. (cluster -> local)
인입 시 다양한 방식 비교
- BGP(ECMP) + Service(LB EX-IP, ExternalTrafficPolicy:Local) + SNAT + Random vs.
- BGP(ECMP) + Service(LB EX-IP, ExternalTrafficPolicy:Cluster) + DSR + Maglev 2가지 외부 인입 방식 장단점(성능, Failover 등) 비교
- BGP(ECMP) + Service(LB EX-IP, ExternalTrafficPolicy:Local) + SNAT + Random → 권장 방식!

(1) Router 에서 BGP ECMP MultiPath로 파드가 있는 노드로만 전달
(2) 해당 Service 가 ExternalTrafficPolicy:Local 로, 소스IP 보존되고 대상 파드가 응답 후 Router로 바로 리턴
- BGP(ECMP) + Service(LB EX-IP, ExternalTrafficPolicy:Cluster) + SNAT → 비권장 방식

(1) Router 에서 BGP ECMP MultiPath로 Cilium BGP Peer인 모든 노드로 전달
(2) 해당 Service 가 ExternalTrafficPolicy:Cluster로, 하필 다른 노드(k8s-ctr)의 파드로 요청을 전달
(3) 해당 노드의 파드가 요청을 처리하고 응답 리턴을 위해서, NAT를 수행했던 노드(k8s-w1)로 다시 전달
(4) 외부 인입을 받아서 NAT를 수행했던 연결 정보를 확인해서, Reverse NAT를 수행해서 최종 응답을 리턴
- BGP(ECMP) + Service(LB EX-IP, ExternalTrafficPolicy:Cluster) + DSR + Maglev - Docs → 그나마 괜찮은 방식

(1) Router 에서 BGP ECMP MultiPath로 Cilium BGP Peer인 모든 노드로 전달
(2) 해당 Service 가 ExternalTrafficPolicy:Cluster로, 하필 다른 노드(k8s-ctr)의 파드로 요청을 전달
- 이때 DSR 동작으로 전달된 노드에서 바로 리턴하기 위해서, 최초 접속했던 클라이언트의 정보를 GENEVE 헤더를 활용해서 전달
- 장애 및 백엔드 대상 변경에서 클라이언트의 세션에 대한 똑똑한 유지를 위해서 Maglev 알고리즘을 사용
(3) 해당 노드의 파드가 요청을 처리하고 GEVEVE 헤더 정보를 활용하여 Reverse NAT를 수행해서 최종 응답을 바로 리턴
# 현재 설정 확인
kubectl exec -it -n kube-system ds/cilium -- cilium status --verbose
...
Mode: SNAT
Backend Selection: Random
Session Affinity: Enabled
...
# 현재 실습 환경에서 설정
modprobe geneve # modprobe geneve
lsmod | grep -E 'vxlan|geneve'
for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo modprobe geneve ; echo; done
for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo lsmod | grep -E 'vxlan|geneve' ; echo; done
helm upgrade cilium cilium/cilium --version 1.18.0 --namespace kube-system --reuse-values \
--set tunnelProtocol=geneve --set loadBalancer.mode=dsr --set loadBalancer.dsrDispatch=geneve \
--set loadBalancer.algorithm=maglev
kubectl -n kube-system rollout restart ds/cilium
# 설정 확인
kubectl exec -it -n kube-system ds/cilium -- cilium status --verbose
...
Mode: DSR
DSR Dispatch Mode: Geneve
Backend Selection: Maglev (Table Size: 16381)
Session Affinity: Enabled
...
# Cluster 로 기본 설정 원복
kubectl patch svc webpod -p '{"spec":{"externalTrafficPolicy":"Cluster"}}'
# k8s-ctr, k8s-w1, k8s-w0 모두 tcpdump 실행
tcpdump -i eth1 -w /tmp/dsr.pcap
# router
curl -s $LBIP
curl -s $LBIP
curl -s $LBIP
curl -s $LBIP
curl -s $LBIP
# Host 에 pcap 다운로드 후 wireshark 로 열여서 확인
vagrant plugin install vagrant-scp
vagrant scp k8s-ctr:/tmp/dsr.pcap .
ps.
어렵다!
'devops > cilium' 카테고리의 다른 글
| 6주차 - Cilium ServiceMesh (1) | 2025.08.22 |
|---|---|
| 5주차 - ClusterMesh (1) | 2025.08.16 |
| 4주차 - Networking#2 (3) | 2025.08.09 |
| 3주차 - Networking#1 - CoreDNS, NodeLocal DNSCache (5) | 2025.08.02 |
| 3주차 - Networking#1 - IPAM, Routing, Masquerading (0) | 2025.08.01 |