a story

[7] Cilium - BGP Control Plane 본문

Cilium

[7] Cilium - BGP Control Plane

한명 2025. 8. 14. 22:54

이번 게시물에서는 Cilium에서 BGP Control Plane을 사용하는 방식을 살펴보겠습니다. 네트워크 환경에 따라 Cilium이 가지고 있는 PodCIDR이나 External IP를 외부에서 접근해야 하는 경우가 있을 수 있습니다.

이러한 경우 Cilium에서 BGP Control Plane을 활용하여 외부 라우터와 BGP 연동을 할 수 있습니다.

실습을 통해서 Cilium의 PodCIDR과 External IP를 Advertise해 보도록 하겠습니다.

 

목차

  1. 실습 환경 구성
  2. BPG Control Plane 적용
  3. External IP에 대한 BGP Advertisement

 

실습 환경 구성

vagrant를 통해서 실습 환경을 구성하겠습니다.

mkdir cilium-lab && cd cilium-lab

curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/5w/Vagrantfile

vagrant up

 

명령이 완료되면 vagrant status로 구성된 VM을 확인할 수 있습니다.

PS C:\cilium-lab\w5> vagrant status
Current machine states:

k8s-ctr                   running (virtualbox)
k8s-w1                    running (virtualbox)
router                    running (virtualbox)
k8s-w0                    running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.

 

컨트롤 플레인에 2대의 워커 노드와, 추가로 router라는 VM이 생성되어 있으며, 아래와 같이 각 네트워크에 연결되어 있습니다.

해당 실습 환경에서는 서로 다른 네트워크에 워커노드를 위치하고, router라는 VM을 통해서 서로 연결되도록 구성되어 있습니다. 각 네트워크에 위치한 VM들은 상태 네트워크에 대해서 라우팅을 router를 통해 처리하도록 static route가 적용되어 있습니다.

 

router는 192.168.10.0/24 대역과 192.168.20.0/24에 연결된 2개의 network Interface를 가지고, 각 네트워크에 대한 라우팅을 처리하는 역할을 합니다. 이번 실습에서 router라는 VM에는 FRR이라는 라우팅 서비스를 추가하였습니다.

 

컨트롤 플레인 노드를 vagrant ssh k8s-ctr로 접근하여 기본 정보를 확인하겠습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get no
NAME      STATUS   ROLES           AGE     VERSION
k8s-ctr   Ready    control-plane   21m     v1.33.2
k8s-w0    Ready    <none>          7m35s   v1.33.2
k8s-w1    Ready    <none>          15m     v1.33.2
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^ipam
ipam                                              cluster-pool
ipam-cilium-node-update-rate                      15s
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^routing
routing-mode                                      native

 

이번 실습에는 Cilium에서 --set bgpControlPlane.enabled=true를 사용하여 Cilium에서 BPG 연동이 가능하도록 구성하였습니다. 이를 아래와 같이 확인할 수 있습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep -i bgp
bgp-router-id-allocation-ip-pool
bgp-router-id-allocation-mode                     default
bgp-secrets-namespace                             kube-system
enable-bgp-control-plane                          true
enable-bgp-control-plane-status-report            true

 

또한 해당 환경에서는 autoDirectNodeRoutes=false로 설정되어 PodCIDR에 대한 기본 라우팅이 적용되어 있지 않습니다.

autoDirectNodeRoutes=true가 설정되면 같은 네트워크에 있는 노드에 대해서 PodCidr에 대한 static routing이 추가됩니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# ip 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.66 dev cilium_host proto kernel src 172.20.0.66
172.20.0.66 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
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route |grep static
192.168.20.0/24 via 192.168.10.200 dev eth1 proto static
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode
NAME      CILIUMINTERNALIP   INTERNALIP       AGE
k8s-ctr   172.20.0.66        192.168.10.100   25m
k8s-w0    172.20.2.75        192.168.20.100   13m
k8s-w1    172.20.1.25        192.168.10.101   21m

 

샘플 애플리케이션을 배포하여 어떤 문제가 있는지 확인해보겠습니다.

# 샘플 애플리케이션 배포
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

 

배포가 완료되었고, curl-pod에서 webpod로 호출은 하겠습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get po -owide
NAME                      READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
curl-pod                  1/1     Running   0          11m   172.20.0.218   k8s-ctr   <none>           <none>
webpod-697b545f57-fgxbm   1/1     Running   0          11m   172.20.1.196   k8s-w1    <none>           <none>
webpod-697b545f57-rtmv9   1/1     Running   0          11m   172.20.0.145   k8s-ctr   <none>           <none>
webpod-697b545f57-wr4tf   1/1     Running   0          11m   172.20.2.246   k8s-w0    <none>           <none>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
---
Hostname: webpod-697b545f57-rtmv9
---
---
---
---
Hostname: webpod-697b545f57-rtmv9
---
---

반복 호출을 해보면 실제로 같은 노드에 위치한 파드인 webpod-697b545f57-rtmv9를 제외하면 제대로 호출이 되지 않는 것을 알 수 있습니다.

 

 

2. BPG Control Plane 적용

문제를 해결하기 위해서 Cilium의 BPG Control Plane을 살펴보겠습니다.

실습 환경에서 router는 frr이라는 라우팅 서비스를 제공하고 있습니다. 여기서는 BGP Control Plane의 Custom Resource를 구성하여 각 Cilium 노드를 frr과 BGP 연동 하도록 설정합니다.

 

먼저 router에 접속하여 frr을 설정을 확인하고 Cilium 노드를 Neigbor로 추가 하겠습니다.

# frr의 기존 설정 확인
vtysh -c 'show running'
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
 no bgp ebgp-requires-policy
 bgp graceful-restart
 bgp bestpath as-path multipath-relax
 !
 address-family ipv4 unicast
  network 10.10.1.0/24
  maximum-paths 4
 exit-address-family
exit
!
end

# frr 설정 파일
cat /etc/frr/frr.conf 

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

# BPG 연동 정보 확인
vtysh -c 'show ip bgp summary'

root@router:~# vtysh -c 'show ip bgp summary'
% No BGP neighbors found in VRF default

# BGP 광고 정보 확인
vtysh -c 'show ip bgp'

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

Displayed  1 routes and 1 total paths

 

아래와 같이 frr의 설정 파일에 Cilium 노드를 Neighbor로 추가합니다.

# Cilium node 연동 설정 (각 노드를 neighbor로 추가함)
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

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

# 모니터링 걸어두기!
journalctl -u frr -f

root@router:~# journalctl -u frr -f
Aug 13 20:42:39 router watchfrr[6427]: [YFT0P-5Q5YX] Forked background command [pid 6428]: /usr/lib/frr/watchfrr.sh restart all
Aug 13 20:42:39 router zebra[6440]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
Aug 13 20:42:39 router staticd[6452]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
Aug 13 20:42:39 router bgpd[6445]: [VTVCM-Y2NW3] Configuration Read in Took: 00:00:00
Aug 13 20:42:39 router watchfrr[6427]: [QDG3Y-BY5TN] zebra state -> up : connect succeeded
Aug 13 20:42:39 router systemd[1]: Started frr.service - FRRouting.
Aug 13 20:42:39 router frrinit.sh[6417]:  * Started watchfrr
Aug 13 20:42:39 router watchfrr[6427]: [QDG3Y-BY5TN] bgpd state -> up : connect succeeded
Aug 13 20:42:39 router watchfrr[6427]: [QDG3Y-BY5TN] staticd state -> up : connect succeeded
Aug 13 20:42:39 router watchfrr[6427]: [KWE5Q-QNGFC] all daemons up, doing startup-complete notify
<대기>

 

이제 Cilium 쪽에서 BGP 설정을 이어서 진행하겠습니다.

먼저 상태가 변경되기 위해서 frr 로그와, curl pod에서 webpod에 대한 호출을 모니터링을 수행합니다.

# 신규 터미널 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 설정 (이후 BGP가 동작할 노드를 label로 지정)
kubectl label nodes k8s-ctr k8s-w0 k8s-w1 enable-bgp=true
kubectl get node -l enable-bgp=true

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get node -l enable-bgp=true
NAME      STATUS   ROLES           AGE   VERSION
k8s-ctr   Ready    control-plane   22h   v1.33.2
k8s-w0    Ready    <none>          22h   v1.33.2
k8s-w1    Ready    <none>          22h   v1.33.2

 

이후 Cilium에서 Custom Resource 를 생성합니다.

# 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" # PodCIDR를 BGP로 광고한다.
---
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" # CiliumBGPAdvertisement의 label과 일치
---
apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata:
  name: cilium-bgp
spec:
  nodeSelector: # nodeSelector로 이전에 지정한 노드 label을 사용
    matchLabels:
      "enable-bgp": "true"
  bgpInstances:
  - name: "instance-65001"
    localASN: 65001
    peers: # 해당 실습에서는 router에서 정의한 AS 정보를 입력
    - name: "tor-switch"
      peerASN: 65000
      peerAddress: 192.168.10.200  # router ip address
      peerConfigRef:
        name: "cilium-peer" # CiliumBGPPeerConfig 로 생성한 Object를 참조
EOF

# 확인
(⎈|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

NAME                                          AGE
ciliumbgpclusterconfig.cilium.io/cilium-bgp   19m

 

Cilium의 BGP Control Plane을 enable하면 이후 아래와 같은 Custom Resource를 통해서 BPG를 관리해줄 수 있습니다.

  • CiliumBGPClusterConfig: Defines BGP instances and peer configurations that are applied to multiple nodes. (Cilium의 BGP instance 정보와 Peer/PeerConfig를 정의)
  • CiliumBGPPeerConfig: A common set of BGP peering setting. It can be used across multiple peers. (BGP Instance와 Peer간의 BGP 속성을 정의)
  • CiliumBGPAdvertisement: Defines prefixes that are injected into the BGP routing table. (어떤 대상을 광고하는지 정의)
  • CiliumBGPNodeConfigOverride: Defines node-specific BGP configuration to provide a finer control.

 

각 리소스의 참조 관계는 아래의 그림을 통해서 확인할 수 있습니다. CiliumBGPClusterConfig에서 BGP Instance는 CiliumBGPPeerConfig의 Peers를 Name으로 참조하고, Peers는 CiliumBGPAdvertisement의 Advertisement를 Label로 참조합니다.

출처: https://docs.cilium.io/en/stable/network/bgp-control-plane/bgp-control-plane-v2/

 

해당 리소스를 생성하면 아래와 같이 frr에서 BGP Update를 받은 것을 알 수 있습니다.

root@router:~# journalctl -u frr -f
...
Aug 13 21:30:02 router bgpd[6445]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.20.100 in vrf default
Aug 13 21:30:02 router bgpd[6445]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.101 in vrf default
Aug 13 21:30:03 router bgpd[6445]: [M59KS-A3ZXZ] bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 192.168.10.100 in vrf default

 

노드에서 살펴보면 179 포트를 Listen하는 것은 아니며, cilium-agent에서 router의 179 포트로 연결하고 있는 것을 알 수 있습니다.

# BGP 연결 확인
ss -tnlp | grep 179
ss -tnp | grep 179

(⎈|HomeLab:N/A) root@k8s-ctr:~# ss -tnlp | grep 179
(⎈|HomeLab:N/A) root@k8s-ctr:~# ss -tnp | grep 179
ESTAB 0      0               192.168.10.100:49941          192.168.10.200:179   users:(("cilium-agent",pid=5626,fd=57))

# cilium bgp 정보 확인
cilium bgp peers
cilium bgp routes available ipv4 unicast

# 각 노드가 router와 정상적으로 연결된 것을 확인할 수 있음
(⎈|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     17m41s   ipv4/unicast   4          2
k8s-w0    65001      65000     192.168.10.200   established     17m41s   ipv4/unicast   4          2
k8s-w1    65001      65000     192.168.10.200   established     17m40s   ipv4/unicast   4          2

# 각 노드가 PodCIDR를 광고하고 있음
(⎈|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   17m48s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.20.2.0/24   0.0.0.0   17m48s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.20.1.0/24   0.0.0.0   17m48s   [{Origin: i} {Nexthop: 0.0.0.0}]


# BGP 상태 정보를 확인할 수 있음
kubectl get ciliumbgpnodeconfigs -o yaml | yq

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumbgpnodeconfigs -o yaml | yq
{
  "apiVersion": "v1",
  "items": [
    {
      "apiVersion": "cilium.io/v2",
      "kind": "CiliumBGPNodeConfig",
      "metadata": {
        "creationTimestamp": "2025-08-13T12:30:13Z",
        "generation": 1,
        "name": "k8s-ctr",
        "ownerReferences": [
          {
            "apiVersion": "cilium.io/v2",
            "controller": true,
            "kind": "CiliumBGPClusterConfig",
            "name": "cilium-bgp",
            "uid": "6ab5f53f-b4fd-428e-adfa-1e7fc1b3648d"
          }
        ],
        "resourceVersion": "37233",
        "uid": "f43979f2-7d3c-4d10-8a89-d9fc0cf409e6"
      },
      "spec": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peerConfigRef": {
                  "name": "cilium-peer"
                }
              }
            ]
          }
        ]
      },
      "status": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "establishedTime": "2025-08-13T12:30:16Z",
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peeringState": "established",
                "routeCount": [
                  {
                    "advertised": 2,
                    "afi": "ipv4",
                    "received": 3,
                    "safi": "unicast"
                  }
                ],
                "timers": {
                  "appliedHoldTimeSeconds": 9,
                  "appliedKeepaliveSeconds": 3
                }
              }
            ]
          }
        ]
      }
    },
    {
      "apiVersion": "cilium.io/v2",
      "kind": "CiliumBGPNodeConfig",
      "metadata": {
        "creationTimestamp": "2025-08-13T12:30:13Z",
        "generation": 1,
        "name": "k8s-w0",
        "ownerReferences": [
          {
            "apiVersion": "cilium.io/v2",
            "controller": true,
            "kind": "CiliumBGPClusterConfig",
            "name": "cilium-bgp",
            "uid": "6ab5f53f-b4fd-428e-adfa-1e7fc1b3648d"
          }
        ],
        "resourceVersion": "37226",
        "uid": "b570c84e-0520-43b7-85d0-2466df0c9e04"
      },
      "spec": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peerConfigRef": {
                  "name": "cilium-peer"
                }
              }
            ]
          }
        ]
      },
      "status": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "establishedTime": "2025-08-13T12:30:27Z",
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peeringState": "established",
                "routeCount": [
                  {
                    "advertised": 2,
                    "afi": "ipv4",
                    "received": 1,
                    "safi": "unicast"
                  }
                ],
                "timers": {
                  "appliedHoldTimeSeconds": 9,
                  "appliedKeepaliveSeconds": 3
                }
              }
            ]
          }
        ]
      }
    },
    {
      "apiVersion": "cilium.io/v2",
      "kind": "CiliumBGPNodeConfig",
      "metadata": {
        "creationTimestamp": "2025-08-13T12:30:13Z",
        "generation": 1,
        "name": "k8s-w1",
        "ownerReferences": [
          {
            "apiVersion": "cilium.io/v2",
            "controller": true,
            "kind": "CiliumBGPClusterConfig",
            "name": "cilium-bgp",
            "uid": "6ab5f53f-b4fd-428e-adfa-1e7fc1b3648d"
          }
        ],
        "resourceVersion": "37239",
        "uid": "2c2497ce-e5de-40d2-aa80-c6b24d0d9dcc"
      },
      "spec": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peerConfigRef": {
                  "name": "cilium-peer"
                }
              }
            ]
          }
        ]
      },
      "status": {
        "bgpInstances": [
          {
            "localASN": 65001,
            "name": "instance-65001",
            "peers": [
              {
                "establishedTime": "2025-08-13T12:27:51Z",
                "name": "tor-switch",
                "peerASN": 65000,
                "peerAddress": "192.168.10.200",
                "peeringState": "established",
                "routeCount": [
                  {
                    "advertised": 2,
                    "afi": "ipv4",
                    "received": 1,
                    "safi": "unicast"
                  }
                ],
                "timers": {
                  "appliedHoldTimeSeconds": 9,
                  "appliedKeepaliveSeconds": 3
                }
              }
            ]
          }
        ]
      }
    }
  ],
  "kind": "List",
  "metadata": {
    "resourceVersion": ""
  }
}

# router에는 전달 받은 IP 대역에 대한 라우팅이 추가됨
ip -c route | grep bgp

root@router:~# 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 29 via 192.168.20.100 dev eth2 proto bgp metric 20


# router에서 beigbor 정보 확인
vtysh -c 'show ip bgp summary'

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       422       426        0    0    0 00:21:00            1        4 N/A
192.168.10.101  4      65001       424       426        0    0    0 00:21:01            1        4 N/A
192.168.20.100  4      65001       424       426        0    0    0 00:21:01            1        4 N/A

Total number of neighbors 3

vtysh -c 'show ip bgp'

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
*> 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  4 routes and 4 total paths

 

이렇게 보면 router는 Cilium 노드의 PodCIDR을 정상적으로 전달받아 라우팅이 가능한 것을 알 수 있습니다. 다만 이 상황에서도 curl-pod -> webpod로의 통신은 전체가 이뤄지지 않습니다.

# 신규 터미널 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'
---
---
Hostname: webpod-697b545f57-rtmv9
---
Hostname: webpod-697b545f57-rtmv9
---
---
---
Hostname: webpod-697b545f57-rtmv9
---
---
---

 

CiliumBPGClusterconfig를 통해서 각 노드를 Peer로 등록해보려고 했지만 앞서 살펴본 바와 같이 cilium 자체가 직접 BGP를 받아주는 역할을 하지 않는 것으로 보이며, 테스트 결과 불가한 것으로 보입니다.

 

관련하여 Cilium에 대한 몇가지 Issue를 살펴보면, Cilium은 upstream으로 Advertising만 하는 역할을 하도록 구현된 것 같습니다.

https://github.com/cilium/cilium/issues/34296

 

또한 이미 살펴본 내용과 같이 auto-direct-node-routes를 통해서 L2 의 노드 연결은 가능하지만, L3는 불가합니다. Full Mesh BGP는 현재는 계획이 없는 것으로 확인됩니다.

https://github.com/cilium/cilium/issues/31124

 

아래 Issue에서 Kubernetes IPAM 모드에서 KubeRouter을 사용하는 경우 가능하다는 커맨트가 있기는 한데, 시간 관계상 추가 테스트를 진행하지는 못했습니다.

https://github.com/cilium/cilium/issues/31091#issuecomment-1976188804

 

한편 모든 노드에서 default Gateway를 tor-Router로 등록하고, tor-Router에서 PodCIDR에 대한 라우팅 정보를 알고 있다면, 실제로 각 노드에서 상대노드의 PodCIDR에 대한 정보를 직접 등록하지 않아도 통신이 가능할 수 있습니다.

 

다만 실습 환경의 특성상 default routing이 NAT를 수행하는 eth0로 되어 있기 때문에 별도의 라우팅이 필요합니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 # default
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.66 dev cilium_host proto kernel src 172.20.0.66
172.20.0.66 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

 

문제를 해결하기 위해서 아래와 같이 Static 라우팅을 추가해 보겠습니다. PodCIDR에 대해서 router(192.168.10.200)이 처리하도록 합니다.

# k8s 파드 사용 대역 통신 전체는 eth1을 통해서 라우팅 설정
ip route add 172.20.0.0/16 via 192.168.10.200
for i in k8s-w0 k8s-w1 router ; do echo ">> node : $i <<"; sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@$i hostname; echo; done
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'
---
---
Hostname: webpod-697b545f57-rtmv9
---
---
---
Hostname: webpod-697b545f57-wr4tf # 각 노드에 라우팅 등록 이후 정상 통신 가능
---
Hostname: webpod-697b545f57-wr4tf
---
Hostname: webpod-697b545f57-rtmv9
---
Hostname: webpod-697b545f57-wr4tf
---
Hostname: webpod-697b545f57-rtmv9
---
Hostname: webpod-697b545f57-bvm82
---

 

 

3. External IP에 대한 BGP Advertisement

앞선 게시글에서 Cilium LoadBalancer IPAM과 L2 Announcement를 통해서 Loadbalancer 유형의 서비스의 External IP를 외부에서 접근 가능하도록 설정하는 방식을 알봤습니다.

참고: https://a-person.tistory.com/54

 

Cilium의 BGP Control Plane에서 이러한 External IP를 BGP로 Advertisement할 수 있습니다.

출처: https://isovalent.com/blog/post/migrating-from-metallb-to-cilium/#l3-announcement-over-bgp

 

실습을 위해서 LoadBalancer IP Pool과 Service에 대한 기본적인 구성을 아래와 같이 진행합니다.

# 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

kubectl get ippool

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

# 기존 Service의 유형을 LoadBalancer로 변경
kubectl patch svc webpod -p '{"spec": {"type": "LoadBalancer"}}'
kubectl get svc webpod 


(⎈|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.120.235   172.16.1.1    80:31039/TCP   24h

# 확인
kubectl get ippool

# IP Avvailable 254 -> 253
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ippool
NAME          DISABLED   CONFLICTING   IPS AVAILABLE   AGE
cilium-pool   false      False         253             59s

# 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

(⎈|HomeLab:N/A) root@k8s-ctr:~# curl -s $LBIP | grep RemoteAddr
RemoteAddr: 172.20.0.66:53430

# router에서 라우팅 정보 확인
root@router:~# ip 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
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.20.0.0/24 nhid 108 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 106 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 107 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

 

이제 LoadBalancer EXternal IP에 대해서 BGP로 광고 설정을 진행하겠습니다.

# 지정된 서비스에 대한 LoadBalancer의 EXternal 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

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get CiliumBGPAdvertisement
NAME                                AGE
bgp-advertisements                  135m
bgp-advertisements-lb-exip-webpod   13s


# 확인
kubectl exec -it -n kube-system ds/cilium -- cilium-dbg bgp route-policies

(⎈|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.0.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


cilium bgp routes available ipv4 unicast

# 모든 노드로 광고가 된다.
(⎈|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   41s      [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.0.0/24   0.0.0.0   44m56s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.16.1.1/32   0.0.0.0   41s      [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.2.0/24   0.0.0.0   44m56s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.16.1.1/32   0.0.0.0   41s      [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.1.0/24   0.0.0.0   44m56s   [{Origin: i} {Nexthop: 0.0.0.0}]

# 현재 ExternalTrafficPolicy: Cluster이므로, 모든 노드에 대해서 연결이 가능하다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl describe svc webpod | grep 'Traffic Policy'
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster


# 현재 BGP가 동작하는 모든 노드로 전달 가능함
root@router:~# ip 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
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 113 proto bgp metric 20
        nexthop via 192.168.10.101 dev eth1 weight 1
        nexthop via 192.168.20.100 dev eth2 weight 1
        nexthop via 192.168.10.100 dev eth1 weight 1
172.20.0.0/24 nhid 108 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 106 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 107 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


sudo vtysh -c 'show ip route bgp'
sudo vtysh -c 'show ip bgp summary'
sudo vtysh -c 'show ip bgp'

root@router:~# sudo vtysh -c 'show ip route bgp'
Codes: K - kernel route, C - connected, S - static, R - RIP,
       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

B>* 172.16.1.1/32 [20/0] via 192.168.10.100, eth1, weight 1, 00:01:12
  *                      via 192.168.10.101, eth1, weight 1, 00:01:12
  *                      via 192.168.20.100, eth2, weight 1, 00:01:12
B>* 172.20.0.0/24 [20/0] via 192.168.10.100, eth1, weight 1, 00:45:24
B>* 172.20.1.0/24 [20/0] via 192.168.10.101, eth1, weight 1, 00:45:24
B>* 172.20.2.0/24 [20/0] via 192.168.20.100, eth2, weight 1, 00:45:24
root@router:~# sudo 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 17
RIB entries 9, using 1728 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      1447      1450        0    0    0 00:46:35            2        5 N/A
192.168.10.101  4      65001      1450      1454        0    0    0 00:46:36            2        5 N/A
192.168.20.100  4      65001      1450      1452        0    0    0 00:46:36            2        5 N/A

Total number of neighbors 3
root@router:~# sudo vtysh -c 'show ip bgp'
BGP table version is 17, 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.100                         0 65001 i
*=                  192.168.20.100                         0 65001 i
*=                  192.168.10.101                         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


sudo vtysh -c 'show ip bgp 172.16.1.1/32'

root@router:~# sudo vtysh -c 'show ip bgp 172.16.1.1/32'
BGP routing table entry for 172.16.1.1/32, version 17
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: Wed Aug 13 23:44:49 2025
  65001
    192.168.20.100 from 192.168.20.100 (192.168.20.100)
      Origin IGP, valid, external, multipath
      Last update: Wed Aug 13 23:44:49 2025
  65001
    192.168.10.101 from 192.168.10.101 (192.168.10.101)
      Origin IGP, valid, external, multipath
      Last update: Wed Aug 13 23:44:49 2025

 

통신 테스트를 수행해보겠습니다.

# 통신 테스트
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

# 접속이 잘 된다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# for i in {1..100};  do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
     41 Hostname: webpod-697b545f57-wr4tf
     30 Hostname: webpod-697b545f57-bvm82
     29 Hostname: webpod-697b545f57-rtmv9
(⎈|HomeLab:N/A) root@k8s-ctr:~# while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 172.20.0.66:57324
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.100:43910
Hostname: webpod-697b545f57-wr4tf
RemoteAddr: 192.168.10.100:45136
Hostname: webpod-697b545f57-wr4tf
RemoteAddr: 192.168.10.100:45144
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 172.20.0.66:57330
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 172.20.0.66:57332

 

앞서 확인해본 External Traffic Policy가 Cluster인 환경에 대해서 어떤 방식으로 동작하는지 추가로 실습해보겠습니다.

# k8s-ctr 에서 replicas=2 로 줄여보자
kubectl scale deployment webpod --replicas 2
kubectl get pod -owide

# 실제 파드는 k8s-ctr, k8s-w0 에 실행 중이다.
(⎈|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 -owide
NAME                      READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
curl-pod                  1/1     Running   0          25h   172.20.0.218   k8s-ctr   <none>           <none>
webpod-697b545f57-bvm82   1/1     Running   0          24h   172.20.2.129   k8s-w0    <none>           <none>
webpod-697b545f57-rtmv9   1/1     Running   0          25h   172.20.0.145   k8s-ctr   <none>           <none>

cilium bgp routes

# 파드는 2개이지만 여전히 모든 노드가 BGP로 advertise를 하고 있따.
(⎈|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   10m40s   [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.0.0/24   0.0.0.0   54m55s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w0    65001     172.16.1.1/32   0.0.0.0   10m40s   [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.2.0/24   0.0.0.0   54m55s   [{Origin: i} {Nexthop: 0.0.0.0}]
k8s-w1    65001     172.16.1.1/32   0.0.0.0   10m40s   [{Origin: i} {Nexthop: 0.0.0.0}]
          65001     172.20.1.0/24   0.0.0.0   54m55s   [{Origin: i} {Nexthop: 0.0.0.0}]

# router 에서 정보 확인 : k8s-w1 노드에 대상 파드가 배치되지 않았지만, 라우팅 경로 설정이 되어 있다.
ip -c route
vtysh -c 'show ip bgp 172.16.1.1/32'

root@router:~# ip -c route
172.16.1.1 nhid 113 proto bgp metric 20
        nexthop via 192.168.10.101 dev eth1 weight 1
        nexthop via 192.168.20.100 dev eth2 weight 1
        nexthop via 192.168.10.100 dev eth1 weight 1

root@router:~# vtysh -c 'show ip bgp 172.16.1.1/32'
BGP routing table entry for 172.16.1.1/32, version 17
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: Wed Aug 13 23:44:50 2025
  65001
    192.168.20.100 from 192.168.20.100 (192.168.20.100)
      Origin IGP, valid, external, multipath
      Last update: Wed Aug 13 23:44:50 2025
  65001
    192.168.10.101 from 192.168.10.101 (192.168.10.101)
      Origin IGP, valid, external, multipath
      Last update: Wed Aug 13 23:44:50 2025


# 반복 접속 
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

# 파드 분산은 잘 이뤄지고 있다.
root@router:~# for i in {1..100};  do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
     52 Hostname: webpod-697b545f57-rtmv9
     48 Hostname: webpod-697b545f57-bvm82

# 반복 접속 시 파드가 없는 RemoteAddr: 192.168.10.101도 확인됨. 모든 경우에 정확한 RemoteAddr이 확인되지 않음
root@router:~# while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.100:48894
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.100:48902
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.100:48908
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.101:48922
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.101:48936
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.101:48940
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.100:48952
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.101:48968
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.100:58500
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.10.101:58510
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:58520
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.101:58536
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.10.100:58550
...

 

External Traffic Policy를 Cluster로 설정되면, 모든 노드들이 BGP Advertise를 하기 때문에, 요청이 모든 노드로 분산된 이후에 다시 파드가 위치하는 노드로 전달됩니다. 이 과정에서 SNAT이 발생하고, remoteAddr도 경유한 노드의 IP가 기록됩니다.

 

이러한 경우 External Traffic Policy를 Local로 설정하면, 파드가 실행된 노드에만 실제로 BGP advertise가 가능하도록 변경됩니다. 그리고 해당 노드에 진입한 트래픽은 해당 노드에 위치한 파드로 전달됩니다.

이로써 불필요한 노드로 트래픽이 경유하지 않게되고, 결과적으로 Client IP가 보존되는 효과가 있습니다.

# k8s-ctr
kubectl patch service webpod -p '{"spec":{"externalTrafficPolicy":"Local"}}'

# router(frr) : 서비스에 대상 파드가 배치된 노드만 BGP 경로에 출력된다.
ip -c route
vtysh -c 'show ip bgp 172.16.1.1/32'

root@router:~# 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
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 125 proto bgp metric 20
        nexthop via 192.168.20.100 dev eth2 weight 1
        nexthop via 192.168.10.100 dev eth1 weight 1
172.20.0.0/24 nhid 108 via 192.168.10.100 dev eth1 proto bgp metric 20
172.20.1.0/24 nhid 106 via 192.168.10.101 dev eth1 proto bgp metric 20
172.20.2.0/24 nhid 107 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

root@router:~# vtysh -c 'show ip bgp 172.16.1.1/32'
BGP routing table entry for 172.16.1.1/32, version 21
Paths: (2 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: Thu Aug 14 00:03:42 2025
  65001
    192.168.20.100 from 192.168.20.100 (192.168.20.100)
      Origin IGP, valid, external, multipath
      Last update: Wed Aug 13 23:44:50 2025


# 현재 실습 환경 경우 반복 접속 시 한쪽 노드로 선택되고, 소스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

# 한쪽 파드로 몰린다.
root@router:~# for i in {1..100};  do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
    100 Hostname: webpod-697b545f57-bvm82

# 리눅스 커널은 기본적으로 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

# 재확인: 분산이 고루 된다.
root@router:~# for i in {1..100};  do curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
     54 Hostname: webpod-697b545f57-bvm82
     46 Hostname: webpod-697b545f57-rtmv9

# 불필요한 노드를 경유하지 않음에 따라 SNAT이 발생하지 않고, remoteAddr도 router의 IP로 보존되는 것을 알 수 있다.
root@router:~# while true; do curl -s $LBIP | egrep 'Hostname|RemoteAddr' ; sleep 0.1; done
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35666
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35678
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35692
Hostname: webpod-697b545f57-bvm82
RemoteAddr: 192.168.20.200:35708
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35720
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35732
Hostname: webpod-697b545f57-rtmv9
RemoteAddr: 192.168.20.200:35744

 

이상 BGP를 통해서 Loadbalancer의 External IP를 Advertise하는 방법을 살펴봤습니다.

 

마치며

이번 게시물에서는 Cilium에서 BGP Control Plane을 활용하여 PodCIDR이나 External IP를 외부 라우터에 Advertise해 보았습니다.

다음 게시물에서는 Cilium으로 구성된 클러스터 간 서비스를 연동하는 ClusterMesh에 대해서 살펴보겠습니다.

 

'Cilium' 카테고리의 다른 글

[9] Cilium - ServiceMesh  (0) 2025.08.23
[8] Cilium - Cluster Mesh  (0) 2025.08.14
[6] Cilium - LoadBalancer IPAM, L2 Announcement  (0) 2025.08.08
[5] Cilium - Encapsulation 모드  (0) 2025.08.08
[4] Cilium에서 NodeLocalDNS 사용  (0) 2025.08.02