[Cilium] #5์ฃผ์ฐจ (1) Cilium BGP + ECMP๋ฅผ ํ™œ์šฉํ•œ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ํŠธ๋ž˜ํ”ฝ ๋ผ์šฐํŒ… ์‹ค์Šต

25๋…„๋„ Cilium Study 1๊ธฐ ์ •๋ฆฌ ๊ธ€์ž…๋‹ˆ๋‹ค.

 

์‹ค์Šต ์„ธํŒ…

์ƒ˜ํ”Œ ์•ฑ ๋ฐฐํฌ

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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:
EOFype: ClusterIP0rPort: 80miernetes.io/hostname"
deployment.apps/webpod created
service/webpod created
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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
pod/curl-pod created
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get deploy,svc,ep webpod -owide
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES           SELECTOR
deployment.apps/webpod   3/3     3            3           19s   webpod       traefik/whoami   app=webpod

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
service/webpod   ClusterIP   10.96.180.250   <none>        80/TCP    19s   app=webpod

NAME               ENDPOINTS                                         AGE
endpoints/webpod   172.20.0.205:80,172.20.1.135:80,172.20.1.157:80   19s

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get endpointslices -l app=webpod
NAME           ADDRESSTYPE   PORTS   ENDPOINTS                                AGE
webpod-8kmfd   IPv4          80      172.20.0.205,172.20.1.135,172.20.1.157   26s

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints
NAME                      SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
curl-pod                  15614               ready            172.20.0.187
webpod-697b545f57-42lfb   31003               ready            172.20.1.135
webpod-697b545f57-8r5sl   31003               ready            172.20.1.157
webpod-697b545f57-fnz6w   31003               ready            172.20.0.205

 

 

 

๋ผ์šฐํ„ฐ ์„ค์ • ํ™•์ธ

root@router:~# ss -tnlp | grep -iE 'zebra|bgpd'
LISTEN 0      3          127.0.0.1:2601      0.0.0.0:*    users:(("zebra",pid=4467,fd=25))
LISTEN 0      3          127.0.0.1:2605      0.0.0.0:*    users:(("bgpd",pid=4472,fd=18))
LISTEN 0      4096         0.0.0.0:179       0.0.0.0:*    users:(("bgpd",pid=4472,fd=22))
LISTEN 0      4096            [::]:179          [::]:*    users:(("bgpd",pid=4472,fd=23))



root@router:~# ps -ef |grep frr
root        4454       1  0 21:50 ?        00:00:01 /usr/lib/frr/watchfrr -d -F traditional zebra bgpd staticd
frr         4467       1  0 21:50 ?        00:00:00 /usr/lib/frr/zebra -d -F traditional -A 127.0.0.1 -s 90000000
frr         4472       1  0 21:50 ?        00:00:00 /usr/lib/frr/bgpd -d -F traditional -A 127.0.0.1
frr         4479       1  0 21:50 ?        00:00:00 /usr/lib/frr/staticd -d -F traditional -A 127.0.0.1
root        4795    4776  0 22:33 pts/1    00:00:00 grep --color=auto frr

 

ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค

 

  • zebra: ์ปค๋„ ๋ผ์šฐํŒ… ํ…Œ์ด๋ธ”๊ณผ FRR ๋ผ์šฐํŒ… ์ •๋ณด๋ฅผ ๋™๊ธฐํ™”
  • bgpd: BGP ํ”ผ์–ด์™€ ์„ธ์…˜์„ ๋งบ๊ณ  ๊ฒฝ๋กœ๋ฅผ ๊ตํ™˜
  • staticd: ์„ค์ •๋œ ์ •์  ๋ผ์šฐํŠธ ๊ด€๋ฆฌ
  • watchfrr: ์œ„ ๋ฐ๋ชฌ๋“ค ์ฃฝ์œผ๋ฉด ์ž๋™ ์žฌ์‹œ์ž‘
root@router:~# vtysh -c 'show running'
Building configuration...

Current configuration:
!
frr version 8.4.4
frr defaults traditional
hostname router
log syslog informational
no ipv6 forwarding
service integrated-vtysh-config
!
router bgp 65000
 bgp router-id 192.168.10.200 # BGP์—์„œ ์‹๋ณ„์ž๋กœ ์“ฐ๋Š” ๊ณ ์œ  IP
 no bgp ebgp-requires-policy # eBGP์—์„œ route-map ์ •์ฑ… ์—†์ด๋„ ๊ฒฝ๋กœ ๊ด‘๊ณ  ํ—ˆ์šฉ
 bgp graceful-restart # ์„ธ์…˜์ด ์ž ์‹œ ๋Š๊ฒจ๋„ ๊ธฐ์กด ๋ผ์šฐํŒ… ์ •๋ณด ์œ ์ง€
 bgp bestpath as-path multipath-relax # AS ๊ฒฝ๋กœ ๊ธธ์ด๊ฐ€ ๋‹ฌ๋ผ๋„ multipath ๋กœ๋“œ๋ฐธ๋Ÿฐ์‹ฑ ํ—ˆ์šฉ
 !
 address-family ipv4 unicast
  network 10.10.1.0/24
  maximum-paths 4
 exit-address-family
exit
!
end
root@router:~# cat /etc/frr/frr.conf
# default to using syslog. /etc/rsyslog.d/45-frr.conf places the log in
# /var/log/frr/frr.log
#
# Note:
# FRR's configuration shell, vtysh, dynamically edits the live, in-memory
# configuration while FRR is running. When instructed, vtysh will persist the
# live configuration to this file, overwriting its contents. If you want to
# avoid this, you can edit this file manually before starting FRR, or instruct
# vtysh to write configuration to a different file.
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
  network 10.10.1.0/24
root@router:~# ip -c route
default via 192.168.163.2 dev eth0 proto dhcp src 192.168.163.168 metric 100
10.10.1.0/24 dev loop1 proto kernel scope link src 10.10.1.200
10.10.2.0/24 dev loop2 proto kernel scope link src 10.10.2.200
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.200
192.168.20.0/24 dev eth2 proto kernel scope link src 192.168.20.200
192.168.163.0/24 dev eth0 proto kernel scope link src 192.168.163.168 metric 100
192.168.163.2 dev eth0 proto dhcp scope link src 192.168.163.168 metric 100


root@router:~# vtysh -c 'show ip route'
Codes: K - kernel route, C - connected, S - static, R - RIP, # ์ปค๋„์—์„œ ๊ฐ€์ ธ์˜จ ๊ธฐ๋ณธ ๊ฒฝ๋กœ / ์ง์ ‘ ์—ฐ๊ฒฐ๋จ / ์ •์  / ๋ฆฌ์–ผ Ip
       O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
       T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
       f - OpenFabric,
       > - selected route, * - FIB route, q - queued, r - rejected, b - backup
       t - trapped, o - offload failure

K>* 0.0.0.0/0 [0/100] via 192.168.163.2, eth0, src 192.168.163.168, 01:05:55
C>* 10.10.1.0/24 is directly connected, loop1, 01:05:55
C>* 10.10.2.0/24 is directly connected, loop2, 01:05:55
C>* 192.168.10.0/24 is directly connected, eth1, 01:05:55
C>* 192.168.20.0/24 is directly connected, eth2, 01:05:55
C>* 192.168.163.0/24 [0/100] is directly connected, eth0, 01:05:55
K>* 192.168.163.2/32 [0/100] is directly connected, eth0, 01:05:55

 

Cilium BGP ํ”ผ์–ด ๋งŒ๋“ค๊ธฐ

root@router:~# 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


root@router:~# cat /etc/frr/frr.conf
# default to using syslog. /etc/rsyslog.d/45-frr.conf places the log in
# /var/log/frr/frr.log
#
# Note:
# FRR's configuration shell, vtysh, dynamically edits the live, in-memory
# configuration while FRR is running. When instructed, vtysh will persist the
# live configuration to this file, overwriting its contents. If you want to
# avoid this, you can edit this file manually before starting FRR, or instruct
# vtysh to write configuration to a different file.
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
  network 10.10.1.0/24
  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
systemctl daemon-reexec && systemctl restart frr
systemctl status frr --no-pager --full

 

์ปจํŠธ๋กค ํ”Œ๋ ˆ์ธ BGP ๋™์ž‘ํ•  ๋…ธ๋“œ ์„ค์ •

# ๋ผ๋ฒจ์ด ๋‹ฌ๋ฆฐ ๋…ธ๋“œ์—์„œ๋งŒ BGP ๊ธฐ๋Šฅ์„ ํ‚ด
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl label nodes k8s-ctr k8s-w0 k8s-w1 enable-bgp=true
node/k8s-ctr labeled
node/k8s-w0 labeled
node/k8s-w1 labeled


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get node -l enable-bgp=true
NAME      STATUS     ROLES           AGE   VERSION
k8s-ctr   Ready      control-plane   77m   v1.33.2
k8s-w0    NotReady   <none>          70m   v1.33.2
k8s-w1    Ready      <none>          74m   v1.33.2
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

 

์ปจํŠธ๋กค ํ”Œ๋ ˆ์ธ์—์„œ ๋ผ๋ฒจ ๋ถ™์€ ๋…ธ๋“œ๋“ค์—์„œ BGP ์Šคํ”ผ์ปค๋ฅผ ์ผœ๊ณ ,

๋กœ์ปฌ AS๋Š” 65001๋กœ, ๋ผ์šฐํ„ฐ(AS65000, 192.168.10.200)์™€ ํ”ผ์–ด๋งํ•˜๊ณ , ๊ฐ ๋…ธ๋“œ์˜ PodCIDR๋ฅผ ๊ด‘๊ณ ํ•˜๋Š” CRD๋ฅผ ์ ์šฉํ•œ๋‹ค.

 

 

๋ผ์šฐํ„ฐ์—์„œ

journalctl -u frr -f

 

์ปจํŠธ๋กค ํ”Œ๋ ˆ์ธ์— BGP ํ”ผ์–ด ์—ฐ๊ฒฐ์ด ์„ฑ๊ณตํ–ˆ๊ณ , ์ดˆ๊ธฐ ๋ผ์šฐํŠธ ๊ตํ™˜์ด ์™„๋ฃŒ๋๋‹ค

 

  • 192.168.10.101 (Cilium BGP ๋…ธ๋“œ1) → IPv4 Unicast
  • 192.168.10.100 (Cilium BGP ๋…ธ๋“œ2) → IPv4 Unicast
  • 192.168.20.100 (Cilium BGP ๋…ธ๋“œ3) → IPv4 Unicast 

 

์ ์šฉ๋œ ๋‚ด์šฉ ํ™•์ธ

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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     15m0s    ipv4/unicast   4          2
k8s-w0    65001      65000     192.168.10.200   established     13m51s   ipv4/unicast   4          2
k8s-w1    65001      65000     192.168.10.200   established     15m8s    ipv4/unicast   4          2


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# cilium bgp routes available ipv4 unicast
Node      VRouter   Prefix          NextHop   Age      Attrs
k8s-ctr   65001     172.20.0.0/24   0.0.0.0   15m13s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.20.2.0/24   0.0.0.0   13m58s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.20.1.0/24   0.0.0.0   15m13s   [{Origin: i} {Nexthop: 0.0.0.0}]

 

 

3๊ฐœ์˜ ๋…ธ๋“œ(k8s-ctr, k8s-w0, k8s-w1)๊ฐ€ ๋ชจ๋‘ ๋ผ์šฐํ„ฐ์™€ BGP ์„ธ์…˜์„ ๋งบ๊ณ  ์žˆ์œผ๋ฉฐ IPv4 ๊ฒฝ๋กœ๋ฅผ ์„œ๋กœ ์ฃผ๊ณ ๋ฐ›๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumbgpadvertisements,ciliumbgppeerconfigs,ciliumbgpclusterconfigs
NAME                                                  AGE
ciliumbgpadvertisement.cilium.io/bgp-advertisements   19m # ๋ฌด์—‡์„ ๊ด‘๊ณ ํ• ์ง€ ์ •์˜

NAME                                        AGE
ciliumbgppeerconfig.cilium.io/cilium-peer   19m # BGP ํ”ผ์–ด(๋ผ์šฐํ„ฐ)์™€์˜ ์„ธ์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ •์˜

NAME                                          AGE
ciliumbgpclusterconfig.cilium.io/cilium-bgp   19m # ์–ด๋–ค ๋…ธ๋“œ์—์„œ BGP๋ฅผ ์ผœ๊ณ  ์–ด๋–ค AS/ํ”ผ์–ด ์„ค์ •์„ ์“ธ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๋ฆฌ์†Œ์Šค
root@router:~# ip -c route | grep bgp
172.20.0.0/24 nhid 28 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 26 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 30 via 192.168.20.100 dev eth2 proto bgp metric 20
# bgp ํ”ผ์–ด ์ƒํƒœ
root@router:~# vtysh -c 'show ip bgp summary'
IPv4 Unicast Summary (VRF default):
BGP router identifier 192.168.10.200, local AS number 65000 vrf-id 0
BGP table version 4
RIB entries 7, using 1344 bytes of memory
Peers 3, using 2172 KiB of memory
Peer groups 1, using 64 bytes of memory

Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
192.168.10.100  4      65001       467       470        0    0    0 00:23:12            1        4 N/A
192.168.10.101  4      65001       470       473        0    0    0 00:23:20            1        4 N/A
192.168.20.100  4      65001       445       447        0    0    0 00:22:04            1        4 N/A

Total number of neighbors 3


root@router:~# vtysh -c 'show ip bgp'
BGP table version is 4, 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        # ๋ผ์šฐํ„ฐ ๋กœ์ปฌ์— ์žˆ๋Š” ๋„คํŠธ์›Œํฌ (BGP๋กœ๋„ ๊ด‘๊ณ ์ค‘)
*> 172.20.0.0/24    192.168.10.100                         0 65001 i  # k8s-ctr ๋…ธ๋“œ์˜ PodCIDR
*> 172.20.1.0/24    192.168.10.101                         0 65001 i  # k8s-w1 ๋…ธ๋“œ์˜ PodCIDR
*> 172.20.2.0/24    192.168.20.100                         0 65001 i  # k8s-w0 ๋…ธ๋“œ์˜ PodCIDR

Displayed  4 routes and 4 total paths

 

 

๋ผ์šฐํ„ฐ(FRR, 192.168.10.200)์™€ Cilium ๋…ธ๋“œ(192.168.10.100) ๊ฐ„ BGP ์„ธ์…˜์ด Established๊ฐ€ ๋œ ์ƒํƒœ์ด๋ฉฐ, ๋ผ์šฐํ„ฐ๊ฐ€ ํ”ผ์–ด์—๊ฒŒ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฒฝ๋กœ๋ฅผ ํ•œ ๋ฒˆ์— UPDATE ๋ฉ”์‹œ์ง€๋กœ ์ „์†กํ•˜๊ฒŒ๋œ๋‹ค.

 

๋…ธ๋“œ์— ๋ผ์šฐํŒ… ์„ค์ • ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# ip route add 172.20.0.0/16 via 192.168.10.200
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# sshpass -p 'vagrant' ssh vagrant@k8s-w1 sudo ip route add 172.20.0.0/16 via 192.168.10.200
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# sshpass -p 'vagrant' ssh vagrant@k8s-w0 sudo ip route add 172.20.0.0/16 via 192.168.20.200

 

 

์›Œ์ปค ๋…ธ๋“œ ์ปค๋„ ๋ผ์šฐํŒ… ํ…Œ์ด๋ธ”์— 172.20.0.0/16 ๊ฒฝ๋กœ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•œ ํ›„ ๋ชจ๋“  Pod CIDR๋กœ ๊ฐ€๋Š” ํŠธ๋ž˜ํ”ฝ์ด NIC(eth1)์„ ํ†ตํ•ด ๋ผ์šฐํ„ฐ๋กœ ๋‚˜๊ฐ€๊ฒŒ ๋œ๋‹ค.

 

 

root@router:~# ip -c route | grep bgp
172.20.0.0/24 nhid 60 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 59 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 56 via 192.168.20.100 dev eth2 proto bgp metric 20

 

 

์„ค์ • ์ฃผ์ž… ์ดํ›„ ์ •์ƒ์ ์œผ๋กœ ์ปฌ์„ ๋ชจ๋‘ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

Disabling CRD Status Report

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# helm upgrade cilium cilium/cilium --version 1.18.0 --namespace kube-system --reuse-values \
  --set bgpControlPlane.statusReport.enabled=false
Release "cilium" has been upgraded. Happy Helming!
NAME: cilium
LAST DEPLOYED: Fri Aug 15 00:40:19 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.

Your release version is 1.18.0.

For any further help, visit https://docs.cilium.io/en/v1.18/gettinghelp

 

bgpControlPlane.statusReport.enabled=false ์„ค์ •์ด ์ ์šฉ๋˜์–ด CiliumBGPNodeConfig์˜ status ํ•„๋“œ๊ฐ€ ๋” ์ด์ƒ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ ๋˜์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.

 

์„œ๋น„์Šค ip๋กœ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ์ƒ์„ฑํ•˜๊ธฐ

์„œ๋น„์Šค์— ํ• ๋‹นํ•  LoadBalancer IP ๋Œ€์—ญ์„ Cilium์— ๋“ฑ๋ก

# Cilium์ด Service=LoadBalancer์— ํ• ๋‹นํ•  ์™ธ๋ถ€ IP ํ’€(172.16.1.0/24)์„ ์ƒ์„ฑํ•œ๋‹ค.
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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

ciliumloadbalancerippool.cilium.io/cilium-pool created


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get ippool
NAME          DISABLED   CONFLICTING   IPS AVAILABLE   AGE
cilium-pool   false      False         254             5s

 

์›น ํŒŒ๋“œ ์„œ๋น„์Šคํƒ€์ž…์„ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ๋กœ ๋ณ€๊ฒฝ

# ์„œ๋น„์Šค๋ฅผ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ๋กœ ์ „ํ™˜ํ•œ๋‹ค
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl patch svc webpod -p '{"spec": {"type": "LoadBalancer"}}'
service/webpod patched


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get svc webpod
NAME     TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
webpod   LoadBalancer   10.96.180.250   172.16.1.1    80:30921/TCP   26h


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get ippool
NAME          DISABLED   CONFLICTING   IPS AVAILABLE   AGE
cilium-pool   false      False         253             22s


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl describe svc webpod | grep 'Traffic Policy'
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list
ID   Frontend                Service Type   Backend
1    10.96.0.1:443/TCP       ClusterIP      1 => 192.168.10.100:6443/TCP (active)
2    10.96.77.222:443/TCP    ClusterIP      1 => 192.168.20.100:4244/TCP (active)
3    10.96.170.16:80/TCP     ClusterIP      1 => 172.20.0.103:4245/TCP (active)
4    0.0.0.0:30003/TCP       NodePort       1 => 172.20.0.191:8081/TCP (active)
7    10.96.170.247:80/TCP    ClusterIP      1 => 172.20.0.191:8081/TCP (active)
8    10.96.0.10:53/TCP       ClusterIP      1 => 172.20.0.30:53/TCP (active)
                                            2 => 172.20.0.84:53/TCP (active)
9    10.96.0.10:53/UDP       ClusterIP      1 => 172.20.0.30:53/UDP (active)
                                            2 => 172.20.0.84:53/UDP (active)
10   10.96.0.10:9153/TCP     ClusterIP      1 => 172.20.0.30:9153/TCP (active)
                                            2 => 172.20.0.84:9153/TCP (active)
11   10.96.176.103:443/TCP   ClusterIP      1 => 172.20.0.128:10250/TCP (active)
12   10.96.180.250:80/TCP    ClusterIP      1 => 172.20.0.205:80/TCP (active)
                                            2 => 172.20.1.135:80/TCP (active)
                                            3 => 172.20.1.157:80/TCP (active)
20   0.0.0.0:30921/TCP       NodePort       1 => 172.20.0.205:80/TCP (active)
                                            2 => 172.20.1.135:80/TCP (active)
                                            3 => 172.20.1.157:80/TCP (active)
23   172.16.1.1:80/TCP       LoadBalancer   1 => 172.20.0.205:80/TCP (active)  # ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ํ™•์ธ
                                            2 => 172.20.1.135:80/TCP (active)
                                            3 => 172.20.1.157:80/TCP (active)
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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
172.16.1.1Hostname: webpod-697b545f57-8r5sl
IP: 127.0.0.1
IP: ::1
IP: 172.20.1.157
IP: fe80::b042:54ff:fea3:7ccc
RemoteAddr: 192.168.10.100:40224
GET / HTTP/1.1
Host: 172.16.1.1
User-Agent: curl/8.5.0
Accept: */*

 

CiliumBGPAdvertisement  ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

BGP Advertisement ์ ์šฉ ์ „์—๋Š” ๋ผ์šฐํ„ฐ๊ฐ€ 172.16.1.1๋กœ ๊ฐ€๋Š” ๊ฒฝ๋กœ๋ฅผ ๋ชจ๋ฅด๋Š” ์ƒํƒœ์ด๋‹ค.

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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
ciliumbgpadvertisement.cilium.io/bgp-advertisements-lb-exip-webpod created


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~#
kubectl get CiliumBGPAdvertisement
NAME                                AGE
bgp-advertisements                  25h
bgp-advertisements-lb-exip-webpod   4s
root@router:~# ip -c route
default via 192.168.163.2 dev eth0 proto dhcp src 192.168.163.168 metric 100
10.10.1.0/24 dev loop1 proto kernel scope link src 10.10.1.200
10.10.2.0/24 dev loop2 proto kernel scope link src 10.10.2.200
172.16.1.1 nhid 117 proto bgp metric 20                               # ์ถ”๊ฐ€
	nexthop via 192.168.20.100 dev eth2 weight 1                      # ์ถ”๊ฐ€
	nexthop via 192.168.10.101 dev eth1 weight 1                      # ์ถ”๊ฐ€
	nexthop via 192.168.10.100 dev eth1 weight 1                      # ์ถ”๊ฐ€
172.20.0.0/24 nhid 60 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 59 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 56 via 192.168.20.100 dev eth2 proto bgp metric 20
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.200
192.168.20.0/24 dev eth2 proto kernel scope link src 192.168.20.200
192.168.163.0/24 dev eth0 proto kernel scope link src 192.168.163.168 metric 100
192.168.163.2 dev eth0 proto dhcp scope link src 192.168.163.168 metric 100
(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# 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.2.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


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# cilium bgp routes available ipv4 unicast
Node      VRouter   Prefix          NextHop   Age      Attrs
k8s-ctr   65001     172.16.1.1/32   0.0.0.0   7m47s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.0.0/24   0.0.0.0   23m20s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.16.1.1/32   0.0.0.0   7m47s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.2.0/24   0.0.0.0   23m20s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.16.1.1/32   0.0.0.0   7m47s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.1.0/24   0.0.0.0   23m7s    [{Origin: i} {Nexthop: 0.0.0.0}]

 

์ด ๊ฒฝ๋กœ๊ฐ€ ์ƒ๊น€์œผ๋กœ์จ ์™ธ๋ถ€ ๋ผ์šฐํ„ฐ๊ฐ€ 172.16.1.1๋กœ ํŠธ๋ž˜ํ”ฝ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ์ด๋‹ค.

 

root@router:~# sudo vtysh -c 'show ip bgp'
BGP table version is 8, 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
*= 172.16.1.1/32    192.168.10.101                         0 65001 i   # ๊ด‘๊ณ ํ•œ ์„œ๋น„์Šค ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ Ip
*>                  192.168.10.100                         0 65001 i   # ๋ฉ”์ธ ๊ฒฝ๋กœ
*=                  192.168.20.100                         0 65001 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

Displayed  5 routes and 7 total paths


root@router:~# sudo vtysh -c 'show ip bgp 172.16.1.1/32'
BGP routing table entry for 172.16.1.1/32, version 8
Paths: (3 available, best #2, table default)
  Advertised to non peer-group peers:
  192.168.10.100 192.168.10.101 192.168.20.100
  65001
    192.168.10.101 from 192.168.10.101 (192.168.10.101)
      Origin IGP, valid, external, multipath
      Last update: Fri Aug 15 00:56:05 2025
  65001
    192.168.10.100 from 192.168.10.100 (192.168.10.100)
      Origin IGP, valid, external, multipath, best (Router ID)
      Last update: Fri Aug 15 00:56:05 2025
  65001
    192.168.20.100 from 192.168.20.100 (192.168.20.100)
      Origin IGP, valid, external, multipath
      Last update: Fri Aug 15 00:56:05 2025

 

 

172.16.1.1/32๊ฐ€ ์™ธ๋ถ€ ๋ผ์šฐํ„ฐ์˜ BGP ํ…Œ์ด๋ธ”์— ๋“ฑ๋ก๋˜์–ด ์™ธ๋ถ€ ๋„คํŠธ์›Œํฌ์—์„œ ์ด IP๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค. 3๊ฐœ์˜ ๋…ธ๋“œ๊ฐ€ ๋ชจ๋‘ next hop์œผ๋กœ ๊ด‘๊ณ ๋˜์–ด ECMP๋กœ ๋ถ€ํ•˜๋ถ„์‚ฐ ๋™์ž‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

 

Best Path๋Š” 192.168.10.100์ด์ง€๋งŒ, multipath ์„ค์ •์ด ์žˆ์œผ๋ฉด ๋‚˜๋จธ์ง€๋„ ๋™์‹œ์— ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋ผ์šฐํ„ฐ์—์„œ ํ†ต์‹  ํ™•์ธ

root@router:~# LBIP=172.16.1.1
curl -s $LBIP
Hostname: webpod-697b545f57-42lfb
IP: 127.0.0.1
IP: ::1
IP: 172.20.1.135
IP: fe80::f89a:ebff:fecd:683a
RemoteAddr: 192.168.20.100:53018
GET / HTTP/1.1
Host: 172.16.1.1
User-Agent: curl/8.5.0
Accept: */*

 

 

๋ฌธ์ œ ํ™•์ธ

(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl scale deployment webpod --replicas 2
deployment.apps/webpod scaled


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -o wide
NAME                      READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
curl-pod                  1/1     Running   0          26h   172.20.0.187   k8s-ctr   <none>           <none>
webpod-697b545f57-42lfb   1/1     Running   0          26h   172.20.1.135   k8s-w1    <none>           <none>
webpod-697b545f57-fnz6w   1/1     Running   0          26h   172.20.0.205   k8s-ctr   <none>           <none>


(โŽˆ|HomeLab:N/A) root@k8s-ctr:~# cilium bgp routes
(Defaulting to `available ipv4 unicast` routes, please see help for more options)

Node      VRouter   Prefix          NextHop   Age      Attrs
k8s-ctr   65001     172.16.1.1/32   0.0.0.0   15m0s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.0.0/24   0.0.0.0   30m33s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.16.1.1/32   0.0.0.0   15m0s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.2.0/24   0.0.0.0   30m33s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.16.1.1/32   0.0.0.0   15m0s    [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.1.0/24   0.0.0.0   30m20s   [{Origin: i} {Nexthop: 0.0.0.0}]

 

Cilium BGP๋ฅผ ํ†ตํ•ด LoadBalancer IP๋ฅผ ๋ชจ๋“  ๋…ธ๋“œ๊ฐ€ ๊ด‘๊ณ ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ์„œ๋น„์Šค ํŒŒ๋“œ๊ฐ€ ์—†๋Š” ๋…ธ๋“œ(w0)๋„ ๊ด‘๊ณ ๋ฅผ ํ•˜๊ณ  ์žˆ์–ด์„œ ๋ถˆํ•„์š”ํ•œ ํŠธ๋ž˜ํ”ฝ์ด ํ•ด๋‹น ๋…ธ๋“œ๋กœ ์œ ์ž…๋  ์ˆ˜ ์žˆ๋‹ค.

 

External Traffic Policy (Local)

 

Cilium์ด ํŒŒ๋“œ๊ฐ€ ์žˆ๋Š” ๋…ธ๋“œ๋งŒ BGP ๊ด‘๊ณ ํ•˜์—ฌ 192.168.20.100(k8s-w0)์€ ๊ด‘๊ณ  ๋Œ€์ƒ์—์„œ ์ œ์™ธ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ๋ผ์šฐํ„ฐ๊ฐ€ ํ•ด๋‹น ๋…ธ๋“œ๋กœ๋Š” ๋” ์ด์ƒ ํŠธ๋ž˜ํ”ฝ์„ ๋ณด๋‚ด์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.

 

externalTrafficPolicy: Local์€ ์„œ๋น„์Šค์˜ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ IP๋ฅผ ๊ด‘๊ณ ํ•  ๋•Œ ์‹ค์ œ ํŒŒ๋“œ๊ฐ€ ์žˆ๋Š” ๋…ธ๋“œ๋งŒ ํƒ€๊ฒŸํ•˜์—ฌ BGP ๋ผ์šฐํŒ…์—์„œ ๋ถˆํ•„์š”ํ•œ ๊ฒฝ๋กœ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์„œ๋น„์Šค ์ ‘๊ทผ ๊ฒฝ๋กœ๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ECMP hash policy

root@router:~# sudo sysctl -w net.ipv4.fib_multipath_hash_policy=1
echo "net.ipv4.fib_multipath_hash_policy=1" >> /etc/sysctl.conf
net.ipv4.fib_multipath_hash_policy = 1


root@router:~# for i in {1..100};  do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
     36 Hostname: webpod-697b545f57-22gbp
     33 Hostname: webpod-697b545f57-42lfb
     31 Hostname: webpod-697b545f57-fnz6w

 

ECMP ๋ผ์šฐํŒ…์€ ๋™์ผํ•œ ๋ชฉ์ ์ง€๋กœ ๊ฐ€๋Š” ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋ฅผ ๋™์‹œ์— ์‚ฌ์šฉํ•˜์—ฌ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐํ•˜๋Š”๋ฐ, ๋ฆฌ๋ˆ…์Šค์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ (์†Œ์ŠคIP, ๋ชฉ์ ์ง€IP, L4ํฌํŠธ)๋ฅผ ํ•ด์‹œ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ํ™‰์„ ๊ฒฐ์ •ํ•˜์ง€๋งŒ 'fib_multipath_hash_policy=1'๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ํฌํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  (์†Œ์ŠคIP, ๋ชฉ์ ์ง€IP)๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.


์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ํฌํŠธ ๊ธฐ๋ฐ˜ ํ•ด์‹œ ํŽธํ–ฅ์„ ์ค„์—ฌ ๋ถ€ํ•˜ ๋ถ„์‚ฐ์„ ๋” ๊ท ๋“ฑํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.