스터디/Cilium

[Cilium] Native Routing Mode

안녕유지 2025. 8. 10. 02:18
Cloudnet Cilium 4주차 스터디를 진행하며 정리한 글입니다.

 

이번 포스팅에서는 Cilium Native Routing Mode 환경에서 Pod 간 통신 장애를 진단하고, 이를 해결하는 방법을 알아보겠습니다.

 

실습 환경 소개

실습을 진행할 환경은 다음과 같습니다.

  • 기본 배포 가상 머신 : k8s-ctr, k8s-w1, k8s-w0, router
  • router : 192.168.10.0/24 ↔ 192.168.20.0/24 대역 라우팅 역할, k8s 에 join 되지 않은 서버, loop1/loop2 dump 인터페이스 배치
  • k8s-w0 : k8s-ctr/w1 노드와 다른 네트워크 대역에 배치

Cloudnet 가시다님 제공 이미지

 

 

샘플 어플리케이션 배포 

Native Routing 모드에서는 Cilium이 Pod CIDR 경로를 직접 라우팅하므로, 서비스의 엔드포인트가 올바르게 잡혀 있는지 확인하겠습니다. 

또한, cilium-dbg를 사용하여 cilium-dbg는 Cilium의 내부 상태와 eBPF 맵 내용을 확인하겠습니다.

 

+) cilium-dbg

cilium-dbg는Cilium 데이터 플레인(eBPF 레벨) 을 보는 도구로, 디버깅 명령을 실행할 수 있는 CLI 도구입니다. 

  • cilium-dbg service list : Cilium이 관리하는 Service와 해당 백엔드 Pod IP 매핑 확인
  • cilium-dbg bpf lb list : ClusterIP·NodePort·LoadBalancer 서비스의 eBPF 로드밸런서 엔트리 확인
  • cilium-dbg bpf nat list : SNAT/DNAT 변환 세션 엔트리 확인
  • cilium-dbg endpoint list : Cilium이 관리하는 Pod 엔드포인트 상태·IP·정책 적용 여부 확인
  • cilium-dbg map list : Cilium이 사용하는 eBPF 맵 목록, 엔트리 수, 캐시 여부 확인
  • cilium-dbg map get <맵이름> : 특정 eBPF 맵에 저장된 Key/Value 상세 조회
  • cilium-dbg map get cilium_ipcache_v2 : IP와 Security Identity 매핑 상태 확인
  • cilium-dbg policy get : CiliumNetworkPolicy(CNP) 및 ClusterwideCNP 적용 상태 확인

+) eBPF 맵

eBPF 맵은, 리눅스 커널 안에 있는 Key-Value 저장소로, Cilium이 네트워크 처리를 위해 필요한 모든 상태 정보를 커널 레벨에 유지하는 곳입니다. Cilium이 현재 어떤 서비스, IP, 백엔드, NAT 매핑을 인식하고 있는지를 확인할 수 있습니다.

  • cilium_lb4_services_v2 : 서비스 로드밸런싱 정보
  • cilium_lb4_backends_v3 : 백엔드 Pod IP 목록
  • cilium_lb4_reverse_nat : NAT 변환 정보
  • cilium_ipcache_v2 : IP→Identity 매핑 
# 샘플 어플리케이션 배포
root@k8s-ctr:~# cat << EOF | kubectl apply -f -
...
deployment.apps/webpod created
service/webpod created
pod/curl-pod created


# 배포 확인
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           79s   webpod       traefik/whoami   app=webpod

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

NAME               ENDPOINTS                                         AGE
endpoints/webpod   172.20.0.154:80,172.20.1.164:80,172.20.2.152:80   79s

root@k8s-ctr:~# kubectl get endpointslices -l app=webpod
NAME           ADDRESSTYPE   PORTS   ENDPOINTS                                AGE
webpod-lh5lx   IPv4          80      172.20.2.152,172.20.1.164,172.20.0.154   79s

root@k8s-ctr:~# kubectl get ciliumendpoints
NAME                      SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
curl-pod                  27143               ready            172.20.0.215
webpod-697b545f57-9nnfd   16202               ready            172.20.1.164
webpod-697b545f57-9ns5w   16202               ready            172.20.2.152
webpod-697b545f57-mn7r5   16202               ready            172.20.0.154



# Cilium eBPF 맵에 서비스가 올바르게 등록되었는지 확인
root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg service list
ID   Frontend                 Service Type   Backend
1    0.0.0.0:30002/TCP        NodePort       1 => 172.20.0.194:3000/TCP (active)
4    10.96.123.249:3000/TCP   ClusterIP      1 => 172.20.0.194:3000/TCP (active)
5    10.96.174.103:80/TCP     ClusterIP      1 => 172.20.0.96:4245/TCP (active)
6    10.96.0.10:53/TCP        ClusterIP      1 => 172.20.0.182:53/TCP (active)
                                             2 => 172.20.0.231:53/TCP (active)
7    10.96.0.10:53/UDP        ClusterIP      1 => 172.20.0.182:53/UDP (active)
                                             2 => 172.20.0.231:53/UDP (active)
8    10.96.0.10:9153/TCP      ClusterIP      1 => 172.20.0.182:9153/TCP (active)
                                             2 => 172.20.0.231:9153/TCP (active)
9    10.96.69.147:443/TCP     ClusterIP      1 => 172.20.0.73:10250/TCP (active)
10   0.0.0.0:30001/TCP        NodePort       1 => 172.20.0.197:9090/TCP (active)
13   10.96.48.163:9090/TCP    ClusterIP      1 => 172.20.0.197:9090/TCP (active)
14   0.0.0.0:30003/TCP        NodePort       1 => 172.20.0.78:8081/TCP (active)
17   10.96.68.188:80/TCP      ClusterIP      1 => 172.20.0.78:8081/TCP (active)
18   10.96.0.1:443/TCP        ClusterIP      1 => 192.168.10.100:6443/TCP (active)
19   10.96.39.55:443/TCP      ClusterIP      1 => 192.168.10.100:4244/TCP (active)
### 요기 추가한 webpod 서비스 등록 된 것 확인
20   10.96.215.249:80/TCP     ClusterIP      1 => 172.20.0.154:80/TCP (active)
                                             2 => 172.20.1.164:80/TCP (active)
                                             3 => 172.20.2.152:80/TCP (active)             

root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf lb list | grep 10.96.215.249
10.96.215.249:80/TCP (3)       172.20.2.152:80/TCP (20) (3)
10.96.215.249:80/TCP (2)       172.20.1.164:80/TCP (20) (2)
10.96.215.249:80/TCP (1)       172.20.0.154:80/TCP (20) (1)
10.96.215.249:80/TCP (0)       0.0.0.0:0 (20) (0) [ClusterIP, non-routable]

# map 확인
root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg map list | grep -v '0             0'
Name                           Num entries   Num errors   Cache enabled
cilium_policy_v2_02778         3             0            true
cilium_runtime_config          256           0            true
cilium_policy_v2_00213         3             0            true
cilium_policy_v2_00140         3             0            true
cilium_policy_v2_01042         3             0            true
cilium_lb4_reverse_sk          9             0            true
cilium_policy_v2_00761         3             0            true
cilium_policy_v2_00254         3             0            true
cilium_policy_v2_03240         3             0            true
cilium_lb4_backends_v3         16            0            true
cilium_lb4_reverse_nat         20            0            true
cilium_lxc                     13            0            true
cilium_policy_v2_00091         3             0            true
cilium_policy_v2_00573         2             0            true
cilium_lb4_services_v2         45            0            true
cilium_ipcache_v2              22            0            true
cilium_policy_v2_01835         3             0            true
cilium_policy_v2_01954         3             0            true

root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_services_v2 | grep 10.96.215.249
10.96.215.249:80/TCP (2)       15 0[0] (20) [0x0 0x0]
10.96.215.249:80/TCP (0)       0 3[0] (20) [0x0 0x0]
10.96.215.249:80/TCP (3)       14 0[0] (20) [0x0 0x0]
10.96.215.249:80/TCP (1)       16 0[0] (20) [0x0 0x0]

 

통신 확인

 

Service를 거치지 않고 직접 Pod IP로 통신 가능 여부 확인하겠습니다. 중간에 connection timeout을 1로 설정하였는데, Hostname이 출력되지 못하는 경우가 있는 것을 보아 통신에 문제가 발생한 것을 알 수 있습니다.

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-mn7r5
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-mn7r5
---
---
Hostname: webpod-697b545f57-9nnfd
---

 

패킷이 어떤 경로를 통해 전달되는지 확인하기 위해 k8s-ctr 노드에서 curl-pod에서 동일 클러스터 내 다른 노드(k8s-w0)의 파드 IP(172.20.2.152)로 ping을 실행하였습니다.
그 결과, 100% 패킷 손실이 발생하였고 응답 패킷이 전혀 수신되지 않았습니다.
따라서 노드 간 라우팅 경로나 응답 경로에 문제가 있음을 알 수 있습니다.

root@k8s-ctr:~# export WEBPOD=$(kubectl get pod -l app=webpod --field-selector spec.nodeName=k8s-w0 -o jsonpath='{.items[0].status.podIP}')
echo $WEBPOD
172.20.2.152

root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
PING 172.20.2.152 (172.20.2.152) 56(84) bytes of data.

--- 172.20.2.152 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

command terminated with exit code 1

 

router 노드에서 접근하여 tcpdump로 확인해보니 172.20.0.215에서 172.20.2.152로 향하는 ICMP 요청이 라우터에 도착하였습니다.
그러나 ip route get 명령을 통해 확인한 결과, 해당 Pod CIDR(172.20.2.0/16)에 대한 라우팅 정보가 없어 기본 게이트웨이(10.0.2.2)로 전달되고 있었습니다.
이를 통해 라우터에 Pod CIDR 경로가 누락되어 있어 응답 패킷이 반환되지 않는다는 점을 알 수 있습니다.

root@router:~# tcpdump -i any icmp -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
02:33:47.217059 eth1  In  IP 172.20.0.215 > 172.20.2.152: ICMP echo request, id 38, seq 1, length 64
02:33:47.217178 eth0  Out IP 172.20.0.215 > 172.20.2.152: ICMP echo request, id 38, seq 1, length 64

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
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:~# ip route get 172.20.2.152
172.20.2.152 via 10.0.2.2 dev eth0 src 10.0.2.15 uid 0
    cache

 

curl-pod에서 webpod 서비스로 1초 간격으로 HTTP 요청을 보낼 때도, Hostname이 출력되지 못하는 경우가 있는 것을 보아 통신에 문제가 발생한 것을 알 수 있습니다.

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-9nnfd
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-9nnfd
---
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-mn7r5
---

 

router에서 tcpdump로 TCP 80 포트를 모니터링하였습니다.
그 결과, 172.20.0.215(curl-pod)에서 172.20.2.152(webpod)로 향하는 SYN 패킷이 eth1에서 들어와 eth0으로 나가는 것을 확인했습니다. eth1 → eth0 흐름은 라우터에서 Pod CIDR(172.20.2.0/16)로 가는 경로가 설정되지 않음을 의미합니다.

 

root@router:~# tcpdump -i any tcp port 80 -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
02:44:04.527193 eth1  In  IP 172.20.0.215.54754 > 172.20.2.152.80: Flags [S], seq 3047054747, win 64240, options [mss 1460,sackOK,TS val 2052114438 ecr 0,nop,wscale 7], length 0
02:44:04.527214 eth0  Out IP 172.20.0.215.54754 > 172.20.2.152.80: Flags [S], seq 3047054747, win 64240, options [mss 1460,sackOK,TS val 2052114438 ecr 0,nop,wscale 7], length 0

 

 

해결 방법

정상 통신을 위해선 1. 라우팅 설정 (수동 or 자동 BGP) 혹은 2. Overlay Network (VXLAN/Geneve)을 통해 해결할 수 있습니다.

방안 1. 라우팅 설정 (수동 or 자동 BGP)

수동 라우팅

  • 라우터에 각 노드가 가진 Pod CIDR에 대한 정적 라우트 추가
ip route add 172.20.2.0/24 via <k8s-w0 노드IP>
ip route add 172.20.1.0/24 via <k8s-w1 노드IP>

 

자동 라우팅 (BGP)

  • Cilium BGP Control Plane 또는 외부 BGP 데몬(예: FRR, Bird) 사용
  • 각 노드의 Pod CIDR을 라우터에 자동 광고해서 경로를 항상 최신 상태로 유지

장점: Overlay 오버헤드 없음, 네이티브 성능

단점: 라우터에 경로를 반드시 알아야 하고, 외부 네트워크 구성 변경이 필요

 

방안 2. Overlay Network (VXLAN/Geneve)

  • Cilium에서 VXLAN이나 Geneve 터널링을 켜서 모든 Pod 간 통신을 Overlay로 처리
  • 각 노드 간 Pod IP는 터널을 통해 전달되므로, 라우터가 Pod CIDR 경로를 몰라도 됨

장점: 네트워크 구성 단순, 라우터 설정 불필요

단점: 추가 캡슐화로 인한 오버헤드, 성능 감소 가능성

 

다음 포스팅에서, 이를 해결하기 위한 방안 두 번째인 overlay network를 사용하는 방법에 대해 알아보겠습니다.