Kubernetes 2025.03.11 14 min read

[하이브리드 Kubernetes 구축기 4] Cilium 패킷은 어떻게 흐를까: eBPF, VXLAN, Tailscale 이중 캡슐화

Cilium의 Service 처리와 VXLAN 경로를 확인했다. 온프레미스 구간에서 Tailscale이 한 번 더 캡슐화하는 과정과 MTU 1280, 원격 Pod 경로 1230을 적용한 이유를 정리했다.

하이브리드 Kubernetes 구축기 4/6 · ← 이전 글 · 다음 글 →


기록 시기: 2025년
이 글은 2025년에 진행한 RKE2 기반 5노드 하이브리드 Kubernetes의 구축·안정화 과정을 회고하며, 이후 확인한 현재 Cilium 1.18.1 구성과 대조해 작성했다. 명령 출력은 핵심 필드만 남겼다. 예시 Service·Pod 주소, Pod 이름, interface index, flow 수와 시각은 출력 형식만 보여주기 위한 공개용 값이며 실제 구성값이 아니다.

3편에서 Kubernetes 노드 사이의 underlay를 확인했다. OCI 내부 네 노드는 사설 VCN과 LPG로 연결되고, 온프레미스 edge-b 구간만 Tailscale subnet routing을 지난다.

이번에는 Pod에서 출발한 패킷을 따라가 봤다. 현재 Cilium은 아래 설정으로 운영하고 있다.

  • Kubernetes Service 처리는 eBPF 기반 kube-proxy replacement를 사용한다.
  • 노드가 다른 Pod 사이의 네트워크는 Cilium VXLAN tunnel mode다.
  • Cilium 자체 암호화는 켜져 있지 않다.
  • Hubble Relay/UI로 Cilium이 보는 흐름을 관측한다.

현재 Cilium 상태부터 확인하기

먼저 Cilium agent가 모든 노드에 하나씩 실행 중인지 확인한다.

$ kubectl -n kube-system get daemonset cilium -o wide
NAME     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR
cilium   5         5         5       5            5           kubernetes.io/os=linux

특정 Cilium Pod에서 상태를 조회했다. Pod 이름을 직접 복사하지 않도록 현재 노드의 agent를 변수로 선택했다.

$ CILIUM_POD=$(kubectl -n kube-system get pod -l k8s-app=cilium \
    --field-selector spec.nodeName=core-a -o jsonpath='{.items[0].metadata.name}')

$ kubectl -n kube-system exec "$CILIUM_POD" -c cilium-agent -- cilium-dbg status
KVStore:                 Disabled
Kubernetes:              Ok        1.33 (v1.33.5+rke2r1)
Kubernetes APIs:         ["core/v1::Pods", "cilium/v2::CiliumEndpoint"]
KubeProxyReplacement:    True
Host firewall:           Disabled
Encryption:              Disabled
Routing:                 Network: Tunnel [vxlan]
Cilium:                  Ok        1.18.1
Cluster health:          5/5 reachable

상태 출력에서 확인한 항목은 다음과 같다.

  • KubeProxyReplacement: True: Kubernetes Service 처리를 Cilium eBPF datapath가 담당한다.
  • Network: Tunnel [vxlan]: 다른 노드의 Pod CIDR로 갈 때 VXLAN을 사용한다.
  • Encryption: Disabled: Cilium WireGuard/IPsec으로 클러스터 전체를 암호화한 구성은 아니다.
  • 5/5 reachable: Cilium 관점의 노드 연결 상태이지, 애플리케이션 SLO를 보장한다는 뜻은 아니다.

kube-proxy replacement: Service 처리를 eBPF로 옮기다

일반적인 Kubernetes에서는 kube-proxy가 iptables 또는 IPVS 규칙을 관리해 ClusterIP, NodePort 같은 Service 트래픽을 backend Pod로 전달한다. 이 클러스터에서는 Cilium의 eBPF kube-proxy replacement가 그 역할을 맡는다.

$ kubectl -n kube-system get pods -l k8s-app=kube-proxy
No resources found in kube-system namespace.

$ kubectl -n kube-system get configmap cilium-config \
    -o jsonpath='{.data.kube-proxy-replacement}{"\n"}'
true

Cilium이 알고 있는 Service와 backend는 BPF map에서도 확인할 수 있다.

$ kubectl -n kube-system exec "$CILIUM_POD" -c cilium-agent -- \
    cilium-dbg service list | head
ID   Frontend                Service Type   Backend
1    10.43.0.1:443/TCP       ClusterIP      1 => 10.0.0.3:6443/TCP
2    10.43.120.80:80/TCP     ClusterIP      1 => 10.42.3.20:8080/TCP
                                                  2 => 10.42.4.20:8080/TCP

Service 처리, Network Policy, Hubble 관측을 Cilium의 데이터 경로에서 함께 관리하려고 이 구성을 선택했다. iptables와 성능을 비교하는 부하 시험은 하지 않았다.

XDP/native acceleration은 별도 설정과 NIC·드라이버 조건이 필요하다. 이번에 확인한 것은 eBPF kube-proxy replacement 적용이다. XDP 사용 여부나 성능 향상 폭은 이 결과로 확인하지 못했다.

VXLAN tunnel mode: Pod CIDR과 노드 주소를 연결한다

Cilium이 원격 Pod를 찾는 과정을 보기 위해 노드와 Pod CIDR 매핑부터 조회했다.

노드 Underlay/Internal-IP Pod CIDR
core-a 10.0.0.3 10.42.0.0/24
core-b 10.0.0.2 10.42.1.0/24
core-c 10.0.1.3 10.42.2.0/24
edge-b 192.168.0.188 10.42.3.0/24
edge-a 10.0.1.2 10.42.4.0/24

Cilium의 터널 맵에는 원격 Pod CIDR과 VXLAN 종단점인 노드 주소가 연결되어 있다.

$ kubectl -n kube-system exec "$CILIUM_POD" -c cilium-agent -- \
    cilium-dbg bpf tunnel list
TUNNEL          VALUE
10.42.0.0:0     10.0.0.3:0
10.42.1.0:0     10.0.0.2:0
10.42.2.0:0     10.0.1.3:0
10.42.3.0:0     192.168.0.188:0
10.42.4.0:0     10.0.1.2:0

출력 형식은 Cilium 버전에 따라 조금 달라질 수 있지만 의미는 같다. 예를 들어 목적지 Pod가 10.42.3.20이면 10.42.3.0/24를 소유한 edge-b(192.168.0.188)를 VXLAN tunnel endpoint로 선택한다.

현재 터널 모드와 기본 VXLAN 포트도 확인했다.

$ kubectl -n kube-system get configmap cilium-config \
    -o go-template='routing-mode={{index .data "routing-mode"}}{{"\n"}}tunnel-protocol={{index .data "tunnel-protocol"}}{{"\n"}}'
routing-mode=tunnel
tunnel-protocol=vxlan

user@core-a:~$ ip -o link show dev cilium_vxlan
6: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1280 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000

user@core-a:~$ ss -lun | grep ':8472'
UNCONN 0 0 0.0.0.0:8472 0.0.0.0:*

VXLAN 자체는 암호화 기능이 아니다. Cilium 상태도 Encryption: Disabled이므로 OCI 내부의 VXLAN 구간까지 Tailscale이 암호화한다고 설명해서는 안 된다. Tailscale WireGuard 암호화는 underlay 경로가 tailscale0를 통과할 때 Tailscale peer 사이에 적용된다. 예를 들어 edge-b → core-a(10.0.0.3) 방향에서는 edge-b ↔ core-b(subnet router) 구간에서 Tailscale 암호화가 끝나고, core-b → core-a의 OCI 사설망 전달 구간은 Cilium 암호화가 적용된 구간이 아니다.

패킷 경로 A: OCI Pod에서 OCI Pod로 갈 때

core-a의 Pod 10.42.0.10edge-a의 Pod 10.42.4.20에 요청한다고 가정해 보자.

원본 Pod 패킷
  10.42.0.10  → 10.42.4.20

          Cilium VXLAN 캡슐화
                    ↓

VXLAN outer packet (UDP 8472)
  10.0.0.3(core-a) → 10.0.1.2(edge-a)

          OCI VCN + LPG 전송
                    ↓

edge-a에서 VXLAN decapsulation
  10.42.4.20 Pod에 전달

이 구간에서 Tailscale은 주 데이터 경로에 들어오지 않는다. 노드 주소 10.0.0.3 → 10.0.1.2가 OCI 사설망에서 직접 라우팅되기 때문이다.

user@core-a:~$ ip route get 10.0.1.2
10.0.1.2 via 10.0.0.1 dev enp0s6 src 10.0.0.3 uid 0

패킷 경로 B: OCI Pod에서 edge-b Pod로 갈 때

이번에는 같은 core-a Pod가 edge-b의 Pod 10.42.3.20에 요청한다고 가정한다.

  1. Cilium은 10.42.3.0/24의 터널 종단점이 192.168.0.188임을 찾는다.
  2. 원본 Pod 패킷을 목적지 192.168.0.188, UDP 8472인 VXLAN 패킷으로 감싼다.
  3. Linux 정책 라우팅은 192.168.0.188tailscale0로 보낸다.
  4. Tailscale은 이 패킷을 다시 WireGuard로 암호화해 edge-b peer로 전달한다.
  5. edge-b에서 Tailscale decapsulation 후 Cilium VXLAN decapsulation이 이뤄지고 목적지 Pod에 도착한다.
[Inner]
Pod IP 10.42.0.10 → Pod IP 10.42.3.20
          │
          │ Cilium VXLAN encapsulation
          ▼
[Middle]
route-selected source 100.123.123.1 → 192.168.0.188, UDP 8472
          │
          │ Linux route: tailscale0
          ▼
[Outer]
Tailscale WireGuard over Internet
core-a TS peer ──> edge-b TS peer
          │
          ▼
edge-b: WireGuard decap → VXLAN decap → destination Pod

실제 underlay 목적지의 경로는 다음처럼 확인된다.

user@core-a:~$ ip route get 192.168.0.188
192.168.0.188 dev tailscale0 table 52 src 100.123.123.1 uid 0

user@edge-b:~$ ip route get 10.0.0.3
10.0.0.3 dev tailscale0 table 52 src 100.123.123.5 uid 0

첫 번째 조회에서 확정할 수 있는 것은 192.168.0.188:8472라는 VXLAN 목적지가 tailscale0로 가고, 일반 route lookup이 100.123.123.1을 source로 선택한다는 점이다. Cilium이 만든 실제 VXLAN outer packet의 source를 단정하려면 해당 시점의 tcpdump가 필요하므로, 캡처 없이 10.0.0.3이라고 추정하지 않았다.

응답 경로는 송신 경로와 대칭이 아닐 수 있다. edge-bcore-a(10.0.0.3)로 돌려보내는 패킷은 3편에서 확인했듯 10.0.0.0/24를 광고하는 core-b를 subnet router로 선택한다.

요청: core-a ── Tailscale ──> edge-b ──> edge-b Pod
응답: edge-b ── Tailscale ──> core-b ── OCI 사설망 ──> core-a

이것이 이 클러스터의 이중 캡슐화다. Cilium VXLAN이 Pod 네트워크를 만들고, 그 VXLAN 패킷의 WAN 구간을 Tailscale WireGuard가 다시 운반한다. Tailscale이 direct UDP를 쓰는지 DERP를 쓰는지는 가장 바깥쪽 전송 방식에 영향을 주지만, 안쪽의 Cilium tunnel map은 바뀌지 않는다.

MTU 1280과 원격 Pod 1230을 사용한 이유

캡슐화에는 헤더 비용이 생긴다. Cilium 공식 문서 기준으로 IPv4 VXLAN은 패킷당 약 50바이트의 MTU overhead가 추가된다. 하이브리드 구간에는 그 바깥에 Tailscale WireGuard까지 있으므로 물리 NIC의 1500만 믿으면 단편화, PMTU blackhole, 큰 응답에서만 발생하는 간헐적 timeout을 만날 수 있다.

현재 클러스터에서는 Tailscale과 Cilium 터널 계층을 1280으로 맞추고, VXLAN의 약 50바이트를 제외한 원격 Pod 경로를 1230으로 사용한다.

user@edge-b:~$ ip -o link show dev tailscale0
5: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen 500

user@edge-b:~$ ip -o link show dev cilium_vxlan
6: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1280 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000

$ kubectl -n demo exec mtu-check-edge-b -- ip -o link show eth0
2: eth0@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1230 qdisc noqueue state UP mode DEFAULT group default

1280 - 50 = 1230이라는 관계는 현재 토폴로지에서 관측하고 적용한 값이다. 1280이나 1230이 Cilium의 공통 기본값은 아니다. 환경에 따라 물리 underlay MTU, IPv4/IPv6, 캡슐화 방식, 클라우드 네트워크, VPN 종류가 달라지므로 실제 경로를 기준으로 산정해야 한다.

DF 비트를 유지한 ping으로 현재 경로의 경계도 점검할 수 있다. IPv4 ICMP에서는 payload 1202바이트에 IP+ICMP 헤더 28바이트를 더하면 1230바이트다. 아래 결과는 명령의 판독 방법을 보여주는 예시 출력이다. 대상 Pod에서 직접 측정한 결과와 혼동하지 않도록 구분했다.

# 진단 도구가 있는 기존 Pod에서 실행한 예시
$ ping -c 3 -M do -s 1202 10.42.3.20
PING 10.42.3.20 (10.42.3.20) 1202(1230) bytes of data.
1210 bytes from 10.42.3.20: icmp_seq=1 ttl=62 time=13.0 ms
1210 bytes from 10.42.3.20: icmp_seq=2 ttl=62 time=12.4 ms
1210 bytes from 10.42.3.20: icmp_seq=3 ttl=62 time=12.6 ms

$ ping -c 1 -M do -s 1203 10.42.3.20
ping: local error: message too long, mtu=1230

컨테이너 이미지의 ping 구현에 따라 -M 옵션이 없을 수 있고, ICMP가 Network Policy에서 막혀 있을 수도 있다. 그런 경우 일회성 진단 Pod 또는 tracepath를 사용하되, 테스트가 끝나면 삭제한다. MTU 변경 전후에는 작은 ping만 보지 말고 HTTP 큰 응답, gRPC 스트림, 이미지 push/pull 같은 실제 워크로드도 함께 검증해야 한다.

Hubble로 Cilium이 보는 흐름 확인하기

Cilium datapath 상태만으로는 어느 애플리케이션 흐름이 drop되는지 알기 어렵다. 이 클러스터는 Hubble Relay와 UI를 실행해 노드별 flow를 한곳에서 조회한다.

$ cilium status
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    OK
 \__/¯¯\__/    Hubble Relay:       OK
    \__/       ClusterMesh:        disabled

$ cilium hubble port-forward
Forwarding from 127.0.0.1:4245 -> 4245

다른 터미널에서 OCI 노드와 edge-b 사이 흐름을 좁혀 본다. 아래 flow는 필드 형식을 보여주기 위해 주소와 시각을 정리한 예시다.

$ hubble status --server localhost:4245
Healthcheck (via localhost:4245): Ok
Current/Max Flows: 8,192/16,384
Connected Nodes: 5/5

$ hubble observe --server localhost:4245 \
    --from-node core-a --to-node edge-b --last 5
Jan 15 10:24:31.102  10.42.0.10:48722 -> 10.42.3.20:8080  to-endpoint FORWARDED (TCP Flags: SYN)
Jan 15 10:24:31.114  10.42.3.20:8080 -> 10.42.0.10:48722  to-network FORWARDED (TCP Flags: SYN, ACK)
Jan 15 10:24:31.127  10.42.0.10:48722 -> 10.42.3.20:8080  to-endpoint FORWARDED (TCP Flags: ACK)

Hubble은 Pod IP, identity, L3/L4/L7 정보, policy verdict를 보는 데 유용하다. 반면 이 출력만으로 outer WireGuard가 direct인지 DERP인지, OCI LPG가 어느 물리 경로를 탔는지는 알 수 없다.

Pod flow와 Cilium policy verdict  → Hubble
VXLAN tunnel endpoint/map         → cilium-dbg, ip, ss
Linux underlay route              → ip route get
Tailscale direct/DERP             → tailscale ping, tailscale status

Hubble로 Pod 흐름을 보고, 바깥쪽 전송은 Tailscale과 Linux 경로 조회로 확인했다.

장애가 났을 때 확인하는 순서

edge-b의 Pod만 통신이 되지 않는다면 아래 순서로 범위를 좁힌다.

# 1. Kubernetes 노드와 Cilium agent 상태
kubectl get nodes -o wide
kubectl -n kube-system get pods -l k8s-app=cilium -o wide

# 2. Cilium 상태와 edge-b Pod CIDR tunnel endpoint
kubectl -n kube-system exec "$CILIUM_POD" -c cilium-agent -- cilium-dbg status
kubectl -n kube-system exec "$CILIUM_POD" -c cilium-agent -- \
  cilium-dbg bpf tunnel list | grep '10.42.3.0'

# 3. edge-b underlay 주소에 대한 Linux 경로
ssh user@core-a 'ip route get 192.168.0.188'

# 4. Tailscale peer 전송 방식
ssh user@core-a 'tailscale ping edge-b'

# 5. Cilium이 관측한 drop/forward verdict
hubble observe --server localhost:4245 --to-node edge-b --last 50
hubble observe --server localhost:4245 --verdict DROPPED --since 5m

# 6. MTU 경계와 실제 애플리케이션 요청 확인
ping -M do -s 1202 10.42.3.20
curl -v --max-time 5 http://10.42.3.20:8080/health

예를 들어 tunnel map이 맞지만 ip route get 192.168.0.188tailscale0를 가리키지 않으면 Cilium보다 Tailscale route 승인·수신 설정을 먼저 확인해야 한다. 반대로 underlay와 Tailscale ping은 정상인데 Hubble에 policy drop이 보이면 CiliumNetworkPolicy 쪽으로 범위를 좁힐 수 있다.

이 구성을 설명할 때 피해야 할 표현

  1. “Cilium을 써서 네트워크 성능이 크게 향상됐다.”
    비교 부하 테스트는 수행하지 않았다. 확인한 범위는 eBPF kube-proxy replacement 적용까지다.
  2. “Cilium VXLAN으로 트래픽을 암호화했다.”
    VXLAN은 오버레이 캡슐화이며 암호화가 아니다. 현재 Cilium encryption은 비활성이다.
  3. “모든 Pod 트래픽이 Tailscale을 지난다.”
    OCI 노드 사이 VXLAN은 OCI VCN/LPG를 사용한다. edge-b ↔ OCI underlay 구간만 Tailscale을 지난다.
  4. “Cilium의 기본 MTU는 1280이다.”
    1280/1230은 이 클러스터의 Tailscale 포함 경로에서 관측·적용한 값이다.
  5. “Hubble에서 보이니 인터넷 구간도 direct다.”
    Hubble은 Cilium flow를 보여준다. Tailscale outer path는 tailscale ping으로 별도 확인한다.

이번 편의 결론

Pod 패킷부터 물리 경로까지 연결하면 다음과 같다.

Kubernetes Service
  └─ Cilium eBPF kube-proxy replacement

노드 간 Pod 네트워크
  └─ Cilium VXLAN (UDP 8472)
       ├─ OCI ↔ OCI: OCI VCN/LPG underlay
       └─ OCI ↔ edge-b: Tailscale subnet route 위의 WireGuard

관측
  ├─ Hubble: Pod flow와 policy verdict
  ├─ cilium-dbg: eBPF/tunnel 상태
  ├─ ip route: underlay 선택
  └─ tailscale ping: direct/DERP 전송 방식

Cilium은 Pod CIDR을 보고 목적지 노드와 VXLAN 터널을 선택한다. Linux와 Tailscale은 그 노드 주소까지의 경로를 정한다. 장애가 나면 이 순서대로 확인할 수 있다. 현재 하이브리드 경로에는 MTU 1280/1230을 적용했다.

참고 문서

다음 편에서는 이 네트워크 위에서 실제 워크로드를 어떻게 배치하고 관측했는지 다룬다. 멀티아키텍처 ARM64/x86_64 이미지, node affinity와 scheduling, 그리고 Hubble·Prometheus·Grafana를 이용한 운영 검증을 CLI와 함께 정리할 예정이다.


하이브리드 Kubernetes 구축기 4/6 · ← 이전 글 · 다음 글 →


SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 결과 이동