a story

[5] Cilium - Encapsulation 모드 본문

Cilium

[5] Cilium - Encapsulation 모드

한명 2025. 8. 8. 23:31

지난 주에 이어서 Cilium CNI Plugin에서 파드들 통신에 대해서 살펴보겠습니다. 특히 워커 노드가 서로 다른 IP대역에 위치하는 상황에서 Encapsulation 모드의 역할을 살펴보고, Cilium LoadBalancer IPAM과 Cilium L2 Announcement 에 대해서 살펴보겠습니다.

 

목차

  1. 실습 환경 구성
  2. Encapsulation 모드
  3. Cilium LoadBalancer IPAM
  4. Cilium L2 Announcement

 

1. 실습 환경 구성

vagrant 를 통해서 실습 환경을 실행합니다.

mkdir cilium-lab && cd cilium-lab

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

vagrant up

vagrant up 이 완료되면 4대의 VM이 생성된 상태로 확인됩니다.

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

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

컨트롤 플레인에 2대의 워커 노드와, 추가로 router라는 VM이 생성되어 있습니다.

 

해당 실습 환경에서는 서로 다른 네트워크에 워커노드를 위치하고, router라는 VM을 통해서 서로 연결되도록 구성되어 있습니다. router가 192.168.10.0/24 대역과 192.168.20.0/24에 연결된 2개의 network Interface를 가지고, 각 네트워크에 대한 라우팅을 처리해주도록 구성되어 있습니다.

 

이후 실습은 vagrant ssh k8s-ctr과 같이 노드에 진입한 이후에 진행하도록 하겠습니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get no -owide
NAME      STATUS   ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s-ctr   Ready    control-plane   59m   v1.33.2   192.168.10.100   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w0    Ready    <none>          41m   v1.33.2   192.168.20.100   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w1    Ready    <none>          97s   v1.33.2   192.168.10.101   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27

 

워커 노드 2대가 연결되어 있으며, k8s-ctr, k8s-w1은 192.168.10.0/24 대역을 사용하고, k8s-w0는 192.168.20.0/24대역을 사용하고 있습니다.

# 각 VM에 간단히 명령어 수행을 위해서 아래 명령 실행
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w0 hostname
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w1 hostname
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@router hostname

# 노드 정보 : 상태, INTERNAL-IP 확인
ifconfig | grep -iEA1 'eth[0-9]:'
ip route | grep static

(⎈|HomeLab:N/A) root@k8s-ctr:~# ifconfig | grep -iEA1 'eth[0-9]:'
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
--
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.100  netmask 255.255.255.0  broadcast 192.168.10.255
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route |grep static
10.10.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.0.0/16 via 192.168.10.200 dev eth1 proto static
192.168.20.0/24 via 192.168.10.200 dev eth1 proto static # router로 향함

# router의 routing 정보를 조회해보면 192.168.10.0/24와 192.168.20.0/24에 대한 라우팅이 적용되어 있음
# 또한 dummy interface로 10.10.1.0/24와 10.10.2.0/24 네트워크에 대한 라우팅도 있음
(⎈|HomeLab:N/A) root@k8s-ctr:~# sshpass -p 'vagrant' ssh vagrant@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

# ipam 모드 확인
cilium config view | grep ^ipam
cilium config view | grep ^routing

(⎈|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의 autoDirectNodeRoutes=true 설정에 대해 이해해 보겠습니다.

## --set routingMode=native --set autoDirectNodeRoutes=true 가 설정되어 있다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^auto
auto-direct-node-routes                           true

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode
NAME      CILIUMINTERNALIP   INTERNALIP       AGE
k8s-ctr   172.20.0.172       192.168.10.100   121m
k8s-w0    172.20.1.16        192.168.20.100   104m
k8s-w1    172.20.2.247       192.168.10.101   64m

ip -c route
sshpass -p 'vagrant' ssh vagrant@k8s-w1 ip -c route
sshpass -p 'vagrant' ssh vagrant@k8s-w0 ip -c route

(⎈|HomeLab:N/A) root@k8s-ctr:~#  ip -c route
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.10.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.0.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172
172.20.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.0.172 dev cilium_host proto kernel scope link
172.20.2.0/24 via 192.168.10.101 dev eth1 proto kernel # k8s-w1로 라우팅이 자동 추가됨
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:~# sshpass -p 'vagrant' ssh vagrant@k8s-w1 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.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.0.0/24 via 192.168.10.100 dev eth1 proto kernel # k8s-ctr로 라우팅이 자동 추가됨
172.20.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.2.0/24 via 172.20.2.247 dev cilium_host proto kernel src 172.20.2.247
172.20.2.247 dev cilium_host proto kernel scope link
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101
192.168.20.0/24 via 192.168.10.200 dev eth1 proto static
(⎈|HomeLab:N/A) root@k8s-ctr:~# sshpass -p 'vagrant' ssh vagrant@k8s-w0 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.0.0/16 via 192.168.20.200 dev eth1 proto static
172.20.0.0/16 via 192.168.20.200 dev eth1 proto static
172.20.1.0/24 via 172.20.1.16 dev cilium_host proto kernel src 172.20.1.16
172.20.1.16 dev cilium_host proto kernel scope link
192.168.10.0/24 via 192.168.20.200 dev eth1 proto static
192.168.20.0/24 dev eth1 proto kernel scope link src 192.168.20.100

# 통신 확인
ping -c 1 10.10.1.200     # router loop1 
ping -c 1 192.168.20.100  # k8s-w0 eth1

# router를 통해서 k8s-w0와 통신은 성립됨
(⎈|HomeLab:N/A) root@k8s-ctr:~# ping -c 1 10.10.1.200
PING 10.10.1.200 (10.10.1.200) 56(84) bytes of data.
64 bytes from 10.10.1.200: icmp_seq=1 ttl=64 time=3.36 ms

--- 10.10.1.200 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.358/3.358/3.358/0.000 ms
(⎈|HomeLab:N/A) root@k8s-ctr:~# ping -c 1 192.168.20.100
PING 192.168.20.100 (192.168.20.100) 56(84) bytes of data.
64 bytes from 192.168.20.100: icmp_seq=1 ttl=63 time=3.96 ms

--- 192.168.20.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.963/3.963/3.963/0.000 ms

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

 

실습 환경에서는 k8s-ctr과 k8s-w1은 같은 네트워크에 위치해 있으며, 중간에 라우터를 거쳐야 k8s-w0와 통신이 가능합니다. 이때문에 Direct Node routing이 의미가 없으며 실제로 라우터에서 라우팅을 처리해주지 않는다면, 실제로 통신이 되지 않습니다.

결론적으로는 다른 대역의 노드에 위치한 파드 간에는 통신이 불가하게 됩니다.

 

 

2. Encapsulation 모드

앞서 살펴본 바와 같이 현재는 Native Routing 모드로 구성되어 있으며, 이 상황에서 실습 환경과 같은 구성이 어떤 문제가 되는지 살펴보겠습니다.

 

먼저 샘플 애플리케이션 배포하겠습니다.

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

 

생성된 애플리케이션을 확인해보고, 실제 통신이 제대로 되는지 확인해보겠습니다.

# 배포 확인
kubectl get deploy,svc,ep webpod -owide
kubectl get endpointslices -l app=webpod
kubectl get ciliumendpoints # IP 확인


(⎈|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           20h   webpod       traefik/whoami   app=webpod

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
service/webpod   ClusterIP   10.96.234.235   <none>        80/TCP    20h   app=webpod

NAME               ENDPOINTS                                       AGE
endpoints/webpod   172.20.0.74:80,172.20.1.224:80,172.20.2.63:80   20h
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get endpointslices -l app=webpod
NAME           ADDRESSTYPE   PORTS   ENDPOINTS                              AGE
webpod-9v987   IPv4          80      172.20.0.74,172.20.1.224,172.20.2.63   20h
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints
NAME                      SECURITY IDENTITY   ENDPOINT STATE   IPV4           IPV6
curl-pod                  58696               ready            172.20.0.217
webpod-697b545f57-5dd9p   12564               ready            172.20.0.74
webpod-697b545f57-rdh2t   12564               ready            172.20.1.224
webpod-697b545f57-vhn6k   12564               ready            172.20.2.63


# 통신 확인 : 문제 확인
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'

# 통신이 하나씩 빠진다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
Hostname: webpod-697b545f57-5dd9p
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
Hostname: webpod-697b545f57-vhn6k
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
command terminated with exit code 130
(⎈|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-5dd9p
---
---
Hostname: webpod-697b545f57-vhn6k
---
Hostname: webpod-697b545f57-5dd9p
---
Hostname: webpod-697b545f57-vhn6k
---
Hostname: webpod-697b545f57-vhn6k
---
Hostname: webpod-697b545f57-5dd9p

# k8s-w0 노드에 배포된 webpod 파드 IP 지정 (k8s-w0는 다른 네트워크에 위치함)
export WEBPOD=$(kubectl get pod -l app=webpod --field-selector spec.nodeName=k8s-w0 -o jsonpath='{.items[0].status.podIP}')
echo $WEBPOD

(⎈|HomeLab:N/A) 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.1.224

# 신규 터미널 [router]
tcpdump -i any icmp -nn

# k8s-w0(192.168.20.100)에 있는 WEBPOD로 ping 체크를 하면 실패한다.
# 앞서 살펴본 바와 같이 k8s-w0에 위치한 PodCIDR에 대해서는 라우팅 정보가 없다.
kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD


(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
PING 172.20.1.224 (172.20.1.224) 56(84) bytes of data.

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

command terminated with exit code 1


# 신규 터미널 [router] : 응답이 없다.
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
20:37:18.313178 eth1  In  IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 99, seq 1, length 64 # eth1 에서 들어옴
20:37:18.313209 eth0  Out IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 99, seq 1, length 64 # eth0 로 보낸다 : eth0는 SNAT을 위한 NIC임 -> 통신 성립 안됨


# 신규 터미널 [router]
ip -c route
ip route get 172.20.2.36

# 172.20.1.0/24에 대한 라우팅 정보가 없다(PodCIDR에 대한 라우팅 정보 없음, 노드의 IP대역에 대한 라우팅만 처리함) 
# -> default route로 보냄
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

# 해당 IP에 대한 라우팅 정보 확인
root@router:~# ip route get 172.20.2.36
172.20.2.36 via 10.0.2.2 dev eth0 src 10.0.2.15 uid 0
    cache

 

현재 실습 환경과 같이 노드의 네트워크가 다른 구성에서 Cilium 의 Native Routing 모드에서는 서로 다른 네트워크에 위치한 파드로의 통신이 불가하다는 것을 알 수 있습니다.

 

이 문제를 해결하려면 두 가지 방안이 있습니다.

  • 라우팅 처리: Router에서 PodCIDR에 대한 라우팅을 Static 하게 추가할 수 있습니다. 다만 노드가 많아질 수록 관리의 복잡도가 높아지고, 노드의 PodCIDR이 변경되는 상황에서 문제가 발생할 수 있습니다. 혹은 BGP를 통해서 자동으로 라우팅을 등록해줄 수 있습니다. 이 방안은 다음 포스트에서 알아보겠습니다.
  • 오버레이 네트워크: Cilium의 Encapsulation 모드를 사용할 수 있습니다. 노드 간 통신이 가능한 환경이므로 파드의 원본 패킷을 노드에서 encapsulation하여 상대 노드로 전달하는 방식입니다.

 

해당 포스트에서는 Cilium의 Encapsulation 모드를 통해서 문제 상황을 해결해 보겠습니다.

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

#  커널 로드
lsmod | grep -E 'vxlan|geneve'
modprobe vxlan # modprobe geneve
lsmod | grep -E 'vxlan|geneve'

(⎈|HomeLab:N/A) root@k8s-ctr:~# lsmod | grep -E 'vxlan|geneve'
(⎈|HomeLab:N/A) root@k8s-ctr:~# modprobe vxlan # modprobe geneve
(⎈|HomeLab:N/A) root@k8s-ctr:~# lsmod | grep -E 'vxlan|geneve'
vxlan                 155648  0
ip6_udp_tunnel         16384  1 vxlan
udp_tunnel             32768  1 vxlan

for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo modprobe vxlan ; echo; done
for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo lsmod | grep -E 'vxlan|geneve' ; echo; done


# k8s-w1 노드에 배포된 webpod 파드 IP 지정
export WEBPOD1=$(kubectl get pod -l app=webpod --field-selector spec.nodeName=k8s-w1 -o jsonpath='{.items[0].status.podIP}')
echo $WEBPOD1

# 반복 ping 실행해두기
kubectl exec -it curl-pod -- ping $WEBPOD1


# rollout 되는 순간 일시적으로 통신이 끊어짐
64 bytes from 172.20.2.63: icmp_seq=107 ttl=62 time=3.71 ms
64 bytes from 172.20.2.63: icmp_seq=108 ttl=62 time=2.75 ms
From 172.20.0.172 icmp_seq=143 Time to live exceeded
From 172.20.0.172 icmp_seq=144 Time to live exceeded
From 172.20.0.172 icmp_seq=145 Time to live exceeded
From 172.20.0.172 icmp_seq=146 Time to live exceeded
From 172.20.0.172 icmp_seq=147 Time to live exceeded
From 172.20.0.172 icmp_seq=148 Time to live exceeded
From 172.20.0.172 icmp_seq=149 Time to live exceeded
From 172.20.0.172 icmp_seq=150 Time to live exceeded
From 172.20.0.172 icmp_seq=151 Time to live exceeded
From 172.20.0.172 icmp_seq=152 Time to live exceeded
64 bytes from 172.20.2.63: icmp_seq=153 ttl=63 time=9.84 ms
64 bytes from 172.20.2.63: icmp_seq=154 ttl=63 time=5.50 ms
64 bytes from 172.20.2.63: icmp_seq=155 ttl=63 time=2.37 ms


# 업그레이드
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

kubectl rollout restart -n kube-system ds/cilium

# 반복 ping 실행 결과
kubectl exec -it curl-pod -- ping $WEBPOD1
# rollout 되는 순간 일시적으로 통신이 끊어짐
64 bytes from 172.20.2.63: icmp_seq=107 ttl=62 time=3.71 ms
64 bytes from 172.20.2.63: icmp_seq=108 ttl=62 time=2.75 ms
From 172.20.0.172 icmp_seq=143 Time to live exceeded
From 172.20.0.172 icmp_seq=144 Time to live exceeded
From 172.20.0.172 icmp_seq=145 Time to live exceeded
From 172.20.0.172 icmp_seq=146 Time to live exceeded
From 172.20.0.172 icmp_seq=147 Time to live exceeded
From 172.20.0.172 icmp_seq=148 Time to live exceeded
From 172.20.0.172 icmp_seq=149 Time to live exceeded
From 172.20.0.172 icmp_seq=150 Time to live exceeded
From 172.20.0.172 icmp_seq=151 Time to live exceeded
From 172.20.0.172 icmp_seq=152 Time to live exceeded
64 bytes from 172.20.2.63: icmp_seq=153 ttl=63 time=9.84 ms
64 bytes from 172.20.2.63: icmp_seq=154 ttl=63 time=5.50 ms
64 bytes from 172.20.2.63: icmp_seq=155 ttl=63 time=2.37 ms

 

cilium을 encapsulation 모드로 업데이트 이후 설정값과 노드의 정보가 어떻게 변경되었는지 살펴보겠습니다.

# 설정 확인
cilium features status
cilium features status | grep datapath_network

kubectl exec -it -n kube-system ds/cilium -- cilium status | grep ^Routing
cilium config view | grep tunnel


(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium features status | grep datapath_network
Yes      cilium_feature_datapath_network                                         mode=overlay-vxlan                                1        1       1
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium status | grep ^Routing
Routing:                 Network: Tunnel [vxlan]   Host: BPF
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep tunnel
routing-mode                                      tunnel
tunnel-protocol                                   vxlan
tunnel-source-port-range                          0-0

# cilium_vxlan 확인
ip -c addr show dev cilium_vxlan
for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i ip -c addr show dev cilium_vxlan ; echo; done

(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c addr show dev cilium_vxlan
26: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
    link/ether 16:5d:7a:ab:58:3f brd ff:ff:ff:ff:ff:ff
    inet6 fe80::145d:7aff:feab:583f/64 scope link
       valid_lft forever preferred_lft forever

# 라우팅 정보 확인
ip -c route | grep cilium_host
ip route get 172.20.1.10
ip route get 172.20.2.10

# k8s node 간 다른 네트워크 대역에 있더라도, 파드의 네트워크 대역 정보가 라우팅에 올라온다. 
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c route | grep cilium_host
172.20.0.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172
172.20.0.172 dev cilium_host proto kernel scope link
172.20.1.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172 mtu 1450
172.20.2.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172 mtu 1450
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route get 172.20.1.10
172.20.1.10 dev cilium_host src 172.20.0.172 uid 0
    cache mtu 1450
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route get 172.20.2.10
172.20.2.10 dev cilium_host src 172.20.0.172 uid 0
    cache mtu 1450

 

노드의 라우팅 정보에서 다른 노드의 PodCIDR에 대한 라우팅이 확인됩니다. 상대 PodCIDR에 대한 라우팅은 실제로 cilium_host 쪽으로 향하고 여기서 encapsulation 수행됩니다. 이때 노드 간은 통신이 가능한 환경이기 때문에 encapsulation된 패킷이 상대 노드로 라우팅되어 통신되게 됩니다.

 

라우팅 정보의 172.20.0.172 IP와 cilium_host 인터페이스에 대해서 아래와 같이 정보를 추가로 확인해보겠습니다.

# cilium 파드 이름 지정
export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-ctr -o jsonpath='{.items[0].metadata.name}')
export CILIUMPOD1=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w1  -o jsonpath='{.items[0].metadata.name}')
export CILIUMPOD2=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w0  -o jsonpath='{.items[0].metadata.name}')
echo $CILIUMPOD0 $CILIUMPOD1 $CILIUMPOD2

# router 역할 IP 확인
kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router

(⎈|HomeLab:N/A) 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}')
export CILIUMPOD1=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w1  -o jsonpath='{.items[0].metadata.name}')
export CILIUMPOD2=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k8s-w0  -o jsonpath='{.items[0].metadata.name}')
echo $CILIUMPOD0 $CILIUMPOD1 $CILIUMPOD2
cilium-4ljkb cilium-2lgrw cilium-5s92k

# router 역할을 하는 IP를 확인해보면 172.20.0.172 IP가 확인된다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
  172.20.0.172 (router)

 

이제 앞서 실패한 서로 다른 네트워크에 위치한 노드로 실패한 파드 통신을 다시 확인해보겠습니다.

# 통신 확인
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'

# 파드 3개의 이름이 모두 확인된다.
(⎈|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-5dd9p
---
Hostname: webpod-697b545f57-vhn6k
---
Hostname: webpod-697b545f57-5dd9p
---
Hostname: webpod-697b545f57-vhn6k
---
Hostname: webpod-697b545f57-rdh2t

# k8s-w0 노드에 배포된 webpod 파드 IP 지정
export WEBPOD=$(kubectl get pod -l app=webpod --field-selector spec.nodeName=k8s-w0 -o jsonpath='{.items[0].status.podIP}')
echo $WEBPOD

# 신규 터미널 [router]
tcpdump -i any udp port 8472 -nn

# Ping 테스트
kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD

# 성공
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
PING 172.20.1.224 (172.20.1.224) 56(84) bytes of data.
64 bytes from 172.20.1.224: icmp_seq=1 ttl=63 time=7.61 ms

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

# 신규 터미널 [router] : 라우팅이 어떻게 되는가?
tcpdump -i any icmp -nn
tcpdump -i any udp port 8472 -nn # vXLAN에서 사용하는 포트

# 결과1
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
21:52:10.609698 eth1  In  IP 192.168.10.100.41429 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 58696
IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 194, seq 1, length 64
21:52:10.609730 eth2  Out IP 192.168.10.100.41429 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 58696
IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 194, seq 1, length 64
21:52:10.612113 eth2  In  IP 192.168.20.100.49596 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 12564
IP 172.20.1.224 > 172.20.0.217: ICMP echo reply, id 194, seq 1, length 64
21:52:10.612135 eth1  Out IP 192.168.20.100.49596 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 12564
IP 172.20.1.224 > 172.20.0.217: ICMP echo reply, id 194, seq 1, length 64

# 결과2
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
21:52:10.609698 eth1  In  IP 192.168.10.100.41429 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 58696 # k8s-ctr -> k8s-w0 로 통신
IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 194, seq 1, length 64 # Inner header 정보 (pod to pod 통신)
21:52:10.609730 eth2  Out IP 192.168.10.100.41429 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 58696
IP 172.20.0.217 > 172.20.1.224: ICMP echo request, id 194, seq 1, length 64
21:52:10.612113 eth2  In  IP 192.168.20.100.49596 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 12564
IP 172.20.1.224 > 172.20.0.217: ICMP echo reply, id 194, seq 1, length 64
21:52:10.612135 eth1  Out IP 192.168.20.100.49596 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 12564
IP 172.20.1.224 > 172.20.0.217: ICMP echo reply, id 194, seq 1, length 64

 

앞서 Cilium의 Routing 모드를 설명하면서 Encapsulation 모드가 가지는 단점을 말씀드렸습니다.

 

Encapsulation 모드에서는 encapsulation을 위한 header를 추가하므로, 페이로드에 사용할 수 있는 유효 MTU가 네이티브 라우팅(VXLAN의 경우 네트워크 패킷당 50바이트)보다 낮아지고, 결과적으로 네트워크 연결에 대한 최대 처리량이 낮아집니다.

아래 노드에서 확인해보면 eth1 인터페이스에 대해서 MTU가 1500인 것을 확인할 수 있지만, 라우팅 정보에서는 MTU가 1450으로 확인됩니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:6b:69:c9 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic eth0
       valid_lft 79973sec preferred_lft 79973sec
    inet6 fd17:625c:f037:2:a00:27ff:fe6b:69c9/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 86322sec preferred_lft 14322sec
    inet6 fe80::a00:27ff:fe6b:69c9/64 scope link
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:53:64:57 brd ff:ff:ff:ff:ff:ff
    altname enp0s8
    inet 192.168.10.100/24 brd 192.168.10.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe53:6457/64 scope link
       valid_lft forever preferred_lft forever
...

(⎈|HomeLab:N/A) root@k8s-ctr:~# ip route | grep 172.20
172.20.0.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172
172.20.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.0.172 dev cilium_host proto kernel scope link
172.20.1.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172 mtu 1450
172.20.2.0/24 via 172.20.0.172 dev cilium_host proto kernel src 172.20.0.172 mtu 1450

 

파드간 통신에서 MTU를 테스트 하기 위해서 아래와 같이 확인해보겠습니다.

curl-pod에서 DF (Don't Fragment)플래그를 설정하고(네트워크 구간에서 MTU가 작아도 조각화하지 않는 설정) 1500 사이즈의 Ping을 수행하면 ping: sendmsg: Message too large 와 같은 에러가 발생하는 것을 알 수 있습니다.

# -M do : Don't Fragment (DF) 플래그를 설정하여 조각화 방지
# -s 1472 : 페이로드(payload) 크기, 즉 ICMP 데이터 크기
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -M do -s 1500 $WEBPOD
PING 172.20.1.224 (172.20.1.224) 1500(1528) bytes of data.
ping: sendmsg: Message too large
ping: sendmsg: Message too large
^C
--- 172.20.1.224 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1019ms

command terminated with exit code 1

 

이때 아래와 같이 1422에서는 통신이 가능한 것을 알 수 있습니다. 이는 payload인 1422에 IP 헤더 20 바이트와 ICMP 헤더 8바이트가 추가 되고, 이후 vXLAN의 outer header인 50 바이트가 필요하기 때문입니다.

(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -M do -s 1423 $WEBPOD
PING 172.20.1.224 (172.20.1.224) 1423(1451) bytes of data.
ping: sendmsg: Message too large
ping: sendmsg: Message too large
ping: sendmsg: Message too large
ping: sendmsg: Message too large
^C
--- 172.20.1.224 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3097ms

command terminated with exit code 1
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping -M do -s 1422 $WEBPOD
PING 172.20.1.224 (172.20.1.224) 1422(1450) bytes of data.
1430 bytes from 172.20.1.224: icmp_seq=1 ttl=63 time=7.45 ms
1430 bytes from 172.20.1.224: icmp_seq=2 ttl=63 time=4.37 ms
^C
--- 172.20.1.224 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 4.374/5.912/7.450/1.538 ms

 

 

마치며

Cilium 실습 환경을 통해서 서로 네트워크가 다른 노드에서 파드 통신이 가능하도록 Encapulation 모드를 살펴봤습니다. 주제가 다소 다르기 때문에 Cilium LoadBalancer IPAM와 Cilium L2 Announcement에 대해서는 다음 포스트로 나눠서 작성하도록 하겠습니다.