스터디/Cilium

[Cilium] Overlay Network (Encapsulation) mode

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

 

이번 포스팅에서는 Cilium Overlay Network(Encapsulation) Mode 환경 구성을 실습하고, VXLAN 기반 터널링을 통한 노드 간 Pod 통신 동작 방식을 확인해보겠습니다.

 

커널 모듈 준비

Native Routing Mode와 달리 Overlay Mode에서는 모든 노드 간 Pod 통신이 캡슐화되어 전송됩니다.

VXLAN을 사용하기 위해 필요한 커널 옵션을 확인합니다.

# [커널 구성 옵션] Requirements for Tunneling and Routing
root@k8s-ctr:~# grep -E 'CONFIG_VXLAN=y|CONFIG_VXLAN=m|CONFIG_GENEVE=y|CONFIG_GENEVE=m|CONFIG_FIB_RULES=y' /boot/config-$(uname -r)
root@k8s-ctr:~# CONFIG_FIB_RULES=y # 커널에 내장됨
root@k8s-ctr:~# CONFIG_VXLAN=m # 모듈로 컴파일됨 → 커널에 로드해서 사용
root@k8s-ctr:~# CONFIG_GENEVE=m # 모듈로 컴파일됨 → 커널에 로드해서 사용

# 커널 로드
root@k8s-ctr:~# lsmod | grep -E 'vxlan|geneve'
root@k8s-ctr:~# modprobe vxlan # modprobe geneve
root@k8s-ctr:~# lsmod | grep -E 'vxlan|geneve'
vxlan                 147456  0
ip6_udp_tunnel         16384  1 vxlan
udp_tunnel             36864  1 vxlan

root@k8s-ctr:~# for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo modprobe vxlan ; echo; done
>> node : k8s-w1 <<

>> node : k8s-w0 <<

root@k8s-ctr:~# 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
>> node : k8s-w1 <<

>> node : k8s-w0 <<
Host key verification failed.

 

Overlay 모드로 Cilium 업그레이드

Cilium을 VXLAN 기반 Overlay 모드로 전환하기 위해 Helm 업그레이드를 수행하였습니다. 이렇게 설정함으로써 Pod 간 트래픽이 VXLAN 터널을 통해 전송되도록 구성하였습니다.

설정 변경 후 VXLAN 인터페이스가 생성되었음을 확인하였습니다.

root@k8s-ctr:~# helm upgrade cilium cilium/cilium --namespace kube-system --version 1.18.0 --reuse-values \
  --set routingMode=tunnel --set tunnelProtocol=vxlan \
  --set autoDirectNodeRoutes=false --set installNoConntrackIptablesRules=false
Release "cilium" has been upgraded. Happy Helming!
NAME: cilium
LAST DEPLOYED: Sun Aug 10 03:03:57 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

# 설정 확인
root@k8s-ctr:~# cilium features status | grep datapath_network
Yes      cilium_feature_datapath_network                                         mode=overlay-vxlan                                1        1       1

root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium status | grep ^Routing
Routing:                 Network: Tunnel [vxlan]   Host: BPF

root@k8s-ctr:~# cilium config view | grep tunnel
routing-mode                                      tunnel
tunnel-protocol                                   vxlan
tunnel-source-port-range                          0-0

 

또한 ip route | grep cilium_host 명령을 통해, 각 노드의 Pod CIDR이 VXLAN 인터페이스를 통해 라우팅되는 것을 알 수 있습니다.

모든 노드가 자기 Pod CIDR은 직접 연결(scope link), 나머지 Pod CIDR은 cilium_host를 통해 전달합니다. 

 

172.20.0.0/24 → 로컬 Pod 대역 (자기 노드에 직접 연결됨)

172.20.1.0/24, 172.20.2.0/24 → 다른 노드의 Pod CIDR (k8s-ctr 기준)

 

즉, 이를 통해 네트워크 모드는 Pod 간 트래픽이 물리 네트워크를 그대로 쓰지 않고 캡슐화되는 것을 통해 Native Routing이 아닌 VXLAN 기반 Overlay 모드임을 알 수 있습니다. 
통신 경로는 Pod ↔ Pod 트래픽은 먼저 cilium_host로 들어가고, BPF map(cilium_vxlan4)에서 터널 목적지를 찾아 VXLAN로 전송합니다.

커널 라우팅 테이블에서는 단순히 Pod CIDR → cilium_host만 설정하고, 실제 전송 대상 노드 결정은 eBPF에서 수행합니다.

 

 

+) cilium_host

  • Cilium이 각 노드에 생성하는 가상 인터페이스
  • 노드의 Pod 네트워크 게이트웨이 역할
  • Pod ↔ Node 간 트래픽이 반드시 지나가는 경유지이며, Node ↔ 다른 Node로 가는 Pod 트래픽도 이 인터페이스를 통해 시작
# 라우팅 정보 확인
# k8s node 간 다른 네트워크 대역에 있더라도, 파드의 네트워크 대역 정보가 라우팅에 올라옴
root@k8s-ctr:~# ip -c route | grep cilium_host
172.20.0.0/24 via 172.20.0.84 dev cilium_host proto kernel src 172.20.0.84
172.20.0.84 dev cilium_host proto kernel scope link
172.20.1.0/24 via 172.20.0.84 dev cilium_host proto kernel src 172.20.0.84 mtu 1450 # 요기
172.20.2.0/24 via 172.20.0.84 dev cilium_host proto kernel src 172.20.0.84 mtu 1450 # 요기

root@k8s-w0:~# ip -c route | grep cilium_host
172.20.0.0/24 via 172.20.2.70 dev cilium_host proto kernel src 172.20.2.70 mtu 1450
172.20.1.0/24 via 172.20.2.70 dev cilium_host proto kernel src 172.20.2.70 mtu 1450
172.20.2.0/24 via 172.20.2.70 dev cilium_host proto kernel src 172.20.2.70
172.20.2.70 dev cilium_host proto kernel scope link

root@k8s-w1:~# ip -c route | grep cilium_host
172.20.0.0/24 via 172.20.1.179 dev cilium_host proto kernel src 172.20.1.179 mtu 1450
172.20.1.0/24 via 172.20.1.179 dev cilium_host proto kernel src 172.20.1.179
172.20.1.179 dev cilium_host proto kernel scope link
172.20.2.0/24 via 172.20.1.179 dev cilium_host proto kernel src 172.20.1.179 mtu 1450

 

통신 및 패킷 캡처

각 노드에서 실행 중인 Cilium 파드 이름을 환경 변수로 지정하였습니다. 
cilium status --all-addresses | grep router를 실행한 결과, 각 노드별로 다른 router IP(172.20.0.84, 172.20.1.179, 172.20.2.70)가 할당되어 있음을 확인했습니다.
또한 cilium-dbg bpf ipcache list 명령을 통해 Pod IP와 터널 엔드포인트(VXLAN IP) 매핑을 확인하였고, flags=hastunnel을 통해 Overlay 모드에서 터널을 사용하는 상태임을 알 수 있습니다.

# cilium 파드 이름 지정

root@k8s-ctr:~# export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-ctr -o jsonpath='{.items[0].metadata.name}')
root@k8s-ctr:~# export CILIUMPOD1=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w1  -o jsonpath='{.items[0].metadata.name}')
root@k8s-ctr:~# export CILIUMPOD2=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w0  -o jsonpath='{.items[0].metadata.name}')
root@k8s-ctr:~# echo $CILIUMPOD0 $CILIUMPOD1 $CILIUMPOD2
cilium-2lmcl cilium-4d66j cilium-b6t44

# router 역할 IP 확인
root@k8s-ctr:~# kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
root@k8s-ctr:~# kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
root@k8s-ctr:~# kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
  172.20.0.84 (router)
  172.20.1.179 (router)
  172.20.2.70 (router)
  
root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf ipcache list
IP PREFIX/ADDRESS   IDENTITY
172.20.0.215/32     identity=27143 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.231/32     identity=17559 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.2.152/32     identity=16202 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
172.20.0.182/32     identity=17559 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.1.179/32     identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
192.168.10.101/32   identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
192.168.20.100/32   identity=6 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
172.20.0.78/32      identity=26816 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.84/32      identity=6 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.194/32     identity=25089 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.2.70/32      identity=6 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
0.0.0.0/0           identity=2 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
172.20.0.96/32      identity=28473 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.154/32     identity=16202 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.197/32     identity=46313 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.2.0/24       identity=2 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
192.168.10.100/32   identity=7 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
172.20.0.0/24       identity=2 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.1.164/32     identity=16202 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
10.0.2.15/32        identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
172.20.0.15/32      identity=52439 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
172.20.0.73/32      identity=55586 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel

root@k8s-ctr:~# kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf socknat list
Socket Cookie   Backend -> Frontend


# 통신 확인
root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
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-mn7r5
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-9ns5w
---
Hostname: webpod-697b545f57-mn7r5
---
Hostname: webpod-697b545f57-9nnfd
---
Hostname: webpod-697b545f57-9ns5w
---

 

curl-pod에서 k8s-w0 노드에 배포된 webpod의 Pod IP(172.20.2.152)로 ICMP 요청을 전송하였습니다.
그 결과, 응답이 정상적으로 수신되었으며 이를 통해 Overlay 모드에서 다른 노드의 Pod와도 정상적으로 네트워크 연결이 가능함을 확인할 수 있습니다.

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.
64 bytes from 172.20.2.152: icmp_seq=1 ttl=63 time=3.08 ms

--- 172.20.2.152 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.082/3.082/3.082/0.000 ms
command terminated with exit code 1

 

라우터에서 VXLAN 트래픽을 모니터링하였습니다.
그 결과, ICMP 요청과 응답이 UDP 8472 포트를 통해 캡슐화되어 전달되는 과정을 확인할 수 있었습니다.
이를 통해 노드 간 통신이 물리 네트워크가 아닌 VXLAN 터널을 통해 전달되고 있음을 알 수 있습니다.

root@router:~# tcpdump -i any udp port 8472 -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
03:13:23.583824 eth1  In  IP 192.168.10.100.46624 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 27143
IP 172.20.0.215 > 172.20.2.152: ICMP echo request, id 163, seq 1, length 64
03:13:23.583916 eth2  Out IP 192.168.10.100.46624 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 27143
IP 172.20.0.215 > 172.20.2.152: ICMP echo request, id 163, seq 1, length 64
03:13:23.585821 eth2  In  IP 192.168.20.100.47789 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 16202
IP 172.20.2.152 > 172.20.0.215: ICMP echo reply, id 163, seq 1, length 64
03:13:23.585839 eth1  Out IP 192.168.20.100.47789 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 16202
IP 172.20.2.152 > 172.20.0.215: ICMP echo reply, id 163, seq 1, length 64

 

curl-pod에서 webpod 서비스로 반복 요청을 전송하여 Hostname을 확인하였습니다.
그 결과, webpod-697b545f57-9nnfd, webpod-697b545f57-mn7r5, webpod-697b545f57-9ns5w, 총 3개의 파드로 정상적으로 모두 요청이 분산되어 응답이 돌아왔습니다.

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

 

해당 통신을 라우터에서 VXLAN 트래픽을 pcap 파일로 저장하고, tshark와 termshark를 통해 확인해보겠습니다.
그 결과, HTTP 요청/응답이 VXLAN 터널 내부에서 TCP 세션으로 정상적으로 교환되는 과정을 확인하였으며, SYN, ACK, FIN 등 TCP 플래그 변화를 통해 세션 수립과 종료까지의 흐름을 확인할 수 있었습니다.
이를 통해 Overlay 네트워크 상에서 애플리케이션 레벨의 HTTP 통신이 문제없이 이루어지고 있음을 알 수 있습니다.

root@router:~# tcpdump -i any udp port 8472 -w /tmp/vxlan.pcap
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
^C22 packets captured
26 packets received by filter
0 packets dropped by kernel

root@router:~# tshark -r /tmp/vxlan.pcap -d udp.port==8472,vxlan
Running as user "root" and group "root". This could be dangerous.
    1   0.000000 172.20.0.215 → 172.20.2.152 TCP 130 48220 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2053954081 TSecr=0 WS=128
    2   0.000049 172.20.0.215 → 172.20.2.152 TCP 130 [TCP Retransmission] 48220 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2053954081 TSecr=0 WS=128
    3   0.001525 172.20.2.152 → 172.20.0.215 TCP 130 80 → 48220 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3941321021 TSecr=2053954081 WS=128
    4   0.001531 172.20.2.152 → 172.20.0.215 TCP 130 [TCP Retransmission] 80 → 48220 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3941321021 TSecr=2053954081 WS=128
    5   0.002049 172.20.0.215 → 172.20.2.152 TCP 122 48220 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2053954083 TSecr=3941321021
    6   0.002072 172.20.0.215 → 172.20.2.152 TCP 122 [TCP Dup ACK 5#1] 48220 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2053954083 TSecr=3941321021
    7   0.002252 172.20.0.215 → 172.20.2.152 HTTP 192 GET / HTTP/1.1
    8   0.002256 172.20.0.215 → 172.20.2.152 TCP 192 [TCP Retransmission] 48220 → 80 [PSH, ACK] Seq=1 Ack=1 Win=64896 Len=70 TSval=2053954083 TSecr=3941321021
    9   0.002691 172.20.2.152 → 172.20.0.215 TCP 122 80 → 48220 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3941321022 TSecr=2053954083
   10   0.002691 172.20.2.152 → 172.20.0.215 TCP 122 [TCP Dup ACK 9#1] 80 → 48220 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3941321022 TSecr=2053954083
   11   0.002698 172.20.2.152 → 172.20.0.215 TCP 122 [TCP Dup ACK 9#2] 80 → 48220 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3941321022 TSecr=2053954083
   12   0.002755 172.20.2.152 → 172.20.0.215 TCP 122 [TCP Dup ACK 9#3] 80 → 48220 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3941321022 TSecr=2053954083
   13   0.006506 172.20.2.152 → 172.20.0.215 HTTP 444 HTTP/1.1 200 OK  (text/plain)
   14   0.006511 172.20.2.152 → 172.20.0.215 TCP 444 [TCP Retransmission] 80 → 48220 [PSH, ACK] Seq=1 Ack=71 Win=64256 Len=322 TSval=3941321026 TSecr=2053954083
   15   0.006830 172.20.0.215 → 172.20.2.152 TCP 122 48220 → 80 [ACK] Seq=71 Ack=323 Win=64640 Len=0 TSval=2053954088 TSecr=3941321026
   16   0.006835 172.20.0.215 → 172.20.2.152 TCP 122 [TCP Dup ACK 15#1] 48220 → 80 [ACK] Seq=71 Ack=323 Win=64640 Len=0 TSval=2053954088 TSecr=3941321026
   17   0.007004 172.20.0.215 → 172.20.2.152 TCP 122 48220 → 80 [FIN, ACK] Seq=71 Ack=323 Win=64640 Len=0 TSval=2053954088 TSecr=3941321026
   18   0.007007 172.20.0.215 → 172.20.2.152 TCP 122 [TCP Fast Retransmission] 48220 → 80 [FIN, ACK] Seq=71 Ack=323 Win=64640 Len=0 TSval=2053954088 TSecr=3941321026
   19   0.007478 172.20.2.152 → 172.20.0.215 TCP 122 80 → 48220 [FIN, ACK] Seq=323 Ack=72 Win=64256 Len=0 TSval=3941321027 TSecr=2053954088
   20   0.007483 172.20.2.152 → 172.20.0.215 TCP 122 [TCP Retransmission] 80 → 48220 [FIN, ACK] Seq=323 Ack=72 Win=64256 Len=0 TSval=3941321027 TSecr=2053954088
   21   0.007831 172.20.0.215 → 172.20.2.152 TCP 122 48220 → 80 [ACK] Seq=72 Ack=324 Win=64640 Len=0 TSval=2053954089 TSecr=3941321027
   22   0.007834 172.20.0.215 → 172.20.2.152 TCP 122 [TCP Dup ACK 21#1] 48220 → 80 [ACK] Seq=72 Ack=324 Win=64640 Len=0 TSval=2053954089 TSecr=3941321027

 

중간 라우터에서 L2, L3 프레임까지만 확인하고 그 이후 페이로드는 해석하지 않았습니다.
172.20.x.x 대역은 라우터 입장에서 알 수 없는 네트워크이므로, 일반적인 네이티브 라우팅 환경이라면 패킷을 Drop해야 합니다.
그러나 이번 경우에는 VXLAN 기반 Overlay 네트워크를 사용하고 있어, 실제 라우터가 보는 것은 UDP 8472 VXLAN 캡슐화 트래픽이며,
172.20 대역의 패킷은 이 터널 내부 페이로드일 뿐이므로 라우터가 이를 해석하거나 Drop하지 않고 그대로 전달하게 됩니다.

:

 

이번 실습에서는 Cilium Overlay Network(VXLAN) Mode에서의 Pod 간 통신 구조와 동작 방식을 확인하였습니다.
VXLAN 모드로 전환 후 각 노드에는 cilium_host 인터페이스가 생성되었으며, 커널 라우팅 테이블에서 모든 Pod CIDR이 이 인터페이스를 통해 전달되도록 설정되었습니다.
이를 통해 원격 Pod로의 트래픽은 cilium_host를 거쳐 Cilium eBPF 데이터 패스에 진입한 뒤, VXLAN 캡슐화(UDP 8472)를 거쳐 대상 노드로 전송됩니다. 이 방식은 외부 라우터가 Pod CIDR을 전혀 알지 못해도 통신이 가능하게 해줍니다.