본문 바로가기
Work/개발 노트

[Cilium study] 1주차 - Cilium 구축 환경 이해하기

by ★용호★ 2025. 7. 18.

로컬 환경 설정

Vagrant와 VirtualBox를 기반으로 실습 환경을 설정합니다. 구성된 쿠버네티스 클러스터는 아래와 같습니다. 

 

Vagrantfile을 기반으로 vagrant up 명령을 실행하여 아래와 같이 쿠버네티스 컨트롤 플레인 1대와 워커노드 2대로 구성된 클러스터를 생성합니다. 

각 서버들은 독립적으로 실행된 가상머신들이기 때문에 아래와 같이 동일 IP를 가지고 있더라도 문제는 없습니다. 쿠버네티스 클러스터에서 Pod간 통신에 활용되는 IP는 아니고 vagrant ssh와 같이 호스트와 가상 서버간 통신을 위한 IP입니다. 

 

위 네트워크 구성은 아래와 같습니다. 

 

쿠버네티스 네트워킹

쿠버네티스의 네트워킹은 CNI를 통해 이루어지며 이 CNI는 아래 4가지 문제를 해결해야 합니다. 

1. 파드 내 컨테이너는 루프백을 통한 통신을 할 수 있습니다. 

2. 파드 간 통신을 할 수 있습니다. 

3. 클러스터 내부에서 서비스를 통한 통신을 할 수 있습니다. 

4. 클로스터 외부에서 서비스를 통한 통신을 할 수 있습니다. 

 

쿠버네티스에 사용할 수 있는 CNI는 다양하며, 여기서는 먼저 Flannel CNI에 대해 살표봅니다. 

Flannel CNI

https://ikcoo.tistory.com/101

Flannel은 작은 규모의 클러스터 환경에서 파드들 간 통신 환경을 구성해주는 CNI 입니다. 아래 세가지 네트워크 환경을 지원합니다. 

1. VXLAN : 네트워크 오버레이 기술을 구현해주는 대표적인 방법으로, 물리적인 네트워크 위에 논리적인 가상의 네트워크 환경을 구성

2. UDP 네트워크 오버레이 : VXLAN을 지원하지 않는 오래된 리눅스 커널 버전 운영 시 사용

3. host-gw 모드 : 네트워크 오버레이 기법을 사용하지 않고, 각 노드의 파드 네트워크 대역을 라우팅 테이블에 업데이트하여 직접 라우팅

 

아래 명령어를 입력해서 Flannel을 설치합니다. 

kubectl create ns kube-flannel
kubectl label --overwrite ns kube-flannel pod-security.kubernetes.io/enforce=privileged

helm repo add flannel https://flannel-io.github.io/flannel/
helm repo list
helm search repo flannel
helm show values flannel/flannel

# helm 설치
helm install flannel --namespace kube-flannel flannel/flannel -f flannel-values.yaml
helm list -A

# 확인 : install-cni-plugin, install-cni
kc describe pod -n kube-flannel -l app=flannel

 

설치 후 노드의 라우팅 테이블 정보를 확인하면 아래와 같습니다. 

Flannel이 정상적으로 설치 되어 오버레이 네트워크의 IP를 Pod가 할당 받더라도 통신이 가능하도록 라우팅 테이블이 갱신되었습니다. 위 라우팅 테이블에서 10.244.1.0/24 IP 대역의 경우에는 10.244.1.0 IP로 보내라는 설정이고, 실제 대상 워커 노드의 라우팅 테이블을 살펴보면 해당 IP로 flannel.1의 네트워크 인터페이스가 존재하는 것을 볼 수 있습니다. 

ip -c a 명령 실행 결과

Service를 통해 Pod간 통신이 정상적으로 실행되는지 확인하기 위해 아래와 같이 샘플 애플리케이션을 배포 했습니다. 

 

위 엔드포인트의 IP 주소 중 하나로 curl 명령을 실행해보면 정상적으로 통신이 되는 것을 확인할 수 있습니다. 

 

서비스명으로 curl 요청을 하게 되면 iptables에 의해 서비스의 엔드포인트 중 대상 Pod 하나로 로드밸런싱이 되기 때문에 아래와 같이 각 Pod들에 트래픽이 분배되는 것을 확인할 수 있습니다. 

 

각 워커노드의 iptables를 확인해보면 아래와 같이 서비스에 대한 정보가 iptables rule에 동일하게 적용된 것을 확인할 수 있습니다. 

 

iptables를 통해 논리적으로 로드밸런싱을 구현해낼 수 있었지만 만약 서비스의 개수가 1000개, 10000개로 증가한다면 그만큼 모든 노드의 iptables의 rule이 추가되기 때문에 과연 효율적인 것인가를 고민해 볼 필요가 있습니다. 

 

Cilium CNI

Cilium은 eBPF를 기반으로 Pod 네트워크 환경과 보안을 제공하는 CNI Plugin 입니다. Cilium eBPF는 추가적인 App이나 설정 변경 없이 리눅스 커널을 자유롭게 프로그래밍하여 동작 가능하기 때문에 아래 그림처럼 복잡한 구조를 단순화 할 수 있다는 장점이 있습니다. 

https://isovalent.com/blog/post/migrating-from-metallb-to-cilium/

 

위에서 살펴본 flannel과 같이 iptables를 사용하여 쿠버네티스 네트워크를 구성하는 경우 아래와 같은 문제점이 있습니다. 

1. 단일 트랜잭션으로 모든 규칙 업데이트 필요 : iptables 규칙을 업데이트 할 때 모든 규칙을 처음부터 다시 만들고 업데이트 해야하는 단일 트랜잭션 방식을 따릅니다.

2. 연결리스트로 구현된 규칙 체인, 모든 연산은 O(n) : 연결 리스트 특성상 어떤 규칙을 찾거나 적용하기 위해 목록의 처음부터 순차적으로 탐색해야 합니다. 이로인해 모든 연산은 규칙 수에 비례하는 시간 복잡도 O(n)을 가집니다. 즉, 규칙 수가 많아질 수록 성능 저하가 심해집니다. 또한 접근 제어 목록(ACL)도 규칙들을 순차적으로 나열하기 때문에 O(n) 탐색 문제를 더욱 부각 시킵니다. 

3. IP 및 포트 매칭 기반, L7 프로토콜에 대한 인지 부족 : iptables는 IP 주소와 포트 번호를 기반으로 규칙을 부여하기 때문에 L3, L4에서는 효과적이지만 HTTP, DNS 등과 같은 L7 프로토콜의 내용을 이해하거나 이에 기반한 정교한 규칙을 적용하는데 한계가 있습니다. 

4. 쿠버네티스에서 높은 자원 소모 : 수많은 서비스와 파드가 동적으로 생성되고 삭제가 되며, 각 서비스와 네트워크 정책은 iptables 규칙으로 변환 됩니다. 이 과정에서 iptables는 시스템 자원을 많이 소모하게 됩니다. 특히, 대규모 클러스터에서 성능 병목 현상을 유발할 수 있습니다. 

https://cilium.io/blog/2020/11/10/ebpf-future-of-networking/

 

eBPF는 리눅스 커널 내부에서 안전하게 샌드박싱된 프로그램을 실행하여 네트워킹, 관측(Observability), 보안, 트레이싱 등 광범위한 영역에 활용 될 수 있습니다. 커널 레벨에서 패킷을 통제(필터링)할 수 있으며 다양한 영역에서 Hook을 통해서 접근할 수 있습니다.

 

리눅스 커널에서 네트워크 패킷을 처리하는 과정에서 userspace, netfilter, tc(Traffic Control), XDP 영역은 각각 다른 레이어에서 패킷을 차단하거나 처리할 수 있는 중요한 지점들입니다. 먼저 각 영역들에 대해 살펴보면 :

  • userspace는 애플리케이션 레이어에서 동작하며, 소켓 시스템 콜을 통해 패킷을 수신한 후 처리합니다. 패킷이 커널 네트워크 스택을 모두 통과한 후 userspace로 전달되는 단계에서 필터링이 이루어집니다. 
  • netfilter는 리눅스 커널 네트워크 스택 내부의 5개 주요 hook 지점에서 동작합니다. 
    • PRE_ROUTING, LOCAL_IN, FORWARD, LOCAL_OUT, POST_ROUTING
  • TC(Traffic Control)은 Data Link 레이어에서 동작하며 sk_buffer가 할당 된 후 동작합니다. ingress qdisc와 egress qdisc 두 가지 방향에서 트래픽을 제어할 수 있습니다. 
  • XDP(eXpress Data Path)는 네트워크 드라이버 레벨에서 동작하며, sk_buff 할당 이전에 패킷을 처리합니다. 하드웨어 인터럽트 처리 직후 메모리 할당이나 네트워크 스택 처리 이전의 가장 이른 시점에서 실행됩니다. XDP는 eBPF를 기반으로 동작하며, 아래 5가지 동작 중 하나를 반환합니다.
    • XDP_PASS : 네트워크 스택으로 패킷 전달
    • XDP_DROP : 패킷 폐기
    • XDP_ABORTED : 트레이스 포인트와 함께 패킷 폐기
    • XDP_TX : 동일 NIC로 패킷 반송
    • XDP_REDIRECT : 다른 NIC나 AF_XDP 소켓으로 패킷 리다이렉트

 

패킷 차단에 대한 성능 비교를 보면 XDP가 압도적인 성능을 보이며 그 중에서도 XDP Offload 사용 시 가장 좋은 성능을 나타냅니다. 그 이유는 XDP가 CPU를 활용하여 네트워크 패킷 처리를 했다면 Offload 시에는 NIC가 패킷 처리를 수행하기 때문에 훨씬 더 좋은 성능을 낼 수 있습니다. 

 

Cilium은 패킷 처리를 위해 최소 TC 레이어부터 사용하기 때문에 다른 CNI들에 비해 훨씬 나은 성능을 제공합니다. Cilium은 네트워크 모드로 터널모드(VXLAN, GENEVE)와 네이티브 라우팅 모드를 지원합니다. 

 

이를 통해 기존의 iptables를 사용하는 kube-proxy를 완전히 대체할 수 있습니다. 예를들어 아래와 같이 기존의 iptables를 사용하지 않고 eBPF로 Masqueading(SNAT) 처리를 할 수 있습니다. 

 

바이트댄스(틱톡) 사례

틱톡으로 유명한 바이트댄스라는 회사에서 iptables의 성능 문제를 확인하기 위해 테스트를 진행한 결과를 공유했습니다. 테스트 클러스터에는 총 3800여대의 노드가 실행되고, 19,000여개의 Pod를 실행합니다. 이 때 각 노드의 iptables rule은 24,000여개나 됩니다. 이 때의 성능 병목 현상을 아래와 같습니다. 

  • 커넥션 맺는데 1.2 ms의 추가적인 네트워크 지연
  • 클러스터의 iptables rule을 업데이트하는데 5분 이상 소요
  • 53% 이상의 CPU 오버헤드
  • 같은 도시의 같은 ISP 내 클라우드 기반 게임을 수행했을 때 총 end-to-end RTT가 15ms (게임에서는 허용하기 어려운 수치)

 

Cilium의 구성요소

  • Cilium Operator : K8S 클러스터에 대한 한 번씩 처리해야 하는 작업을 관리하며 가장 상위 레벨에서 동작합니다. 
  • Cilium Agent : 데몬셋으로 실행되어 K8S API 설정으로 부터 '네트워크 설정, 네트워크 정책, 서비스 부하분산, 모니터링' 등을 수행하며, eBPF 프로그램을 관리합니다.
  • Cilium Client (CLI) : Cilium 커멘드툴이며, eBPF maps 에 직접 접속하여 상태를 확인할 수 있습니다. (kubectl과 유사)
  • Hubble : 네트워크와 보안 모니터링 플랫폼 역할을 하여, 'Server, Relay, Client, Graphical UI' 로 구성되어 있습니다.
  • Data Store : Cilium Agent 간의 상태를 저장하고 전파하는 데이터 저장소이며, 2가지 종류 중 선택(K8S CRDs, Key-Value Store)할 수 있습니다. 

Cilium의 시스템 요구사항 확인

Cilium은 AMD64 또는 AArch64 CPU 아키텍처, Linux 커널 5.4 이상을 사용하는 호스트에서 모두 사용할 수 있습니다. 하지만 Cilium의 여러 기능들이 OS에서 사용 가능한지는 확인이 필요합니다. (참고) 예를들어 아래 표와 같이 Cilium의 기능을 사용하기 위한 최소한의 커널 버전이 충족하는지를 확인 해야합니다.

https://docs.cilium.io/en/stable/operations/system_requirements/

 

또한 기능을 지원하더라도 실제 활성화가 되어 있는지 명령어를 통해 확인 필요합니다. 아래 명령어는 네트워크 모드를 확인하기 위한 예시 명령어 입니다.

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 # 모듈로 컴파일됨 → 커널에 로드해서 사용

 

Cilium 설치

먼저 아래 명령을 실행하여 kube-proxy를 제거합니다. 이 때 각 노드의 iptables에는 기존의 rule이 존재하기 때문에 iptables rule을 제거하는 명령어도 함께 실행합니다. 

# kube-proxy 제거
kubectl -n kube-system delete ds kube-proxy
kubectl -n kube-system delete cm kube-proxy

# 배포된 파드의 IP는 남겨져 있음
kubectl get pod -A -owide

# iptables 확인
iptables-save

# 각 노드에서 root 권한으로 아래 명령 실행: 
iptables-save | grep -v KUBE | grep -v FLANNEL | iptables-restore
iptables-save

 

이제 아래 명령으로 Cilium을 설치합니다.

# Cilium 설치 with Helm
helm repo add cilium https://helm.cilium.io/

# 모든 NIC 지정 + bpf.masq=true + NoIptablesRules
helm install cilium cilium/cilium --version 1.17.5 --namespace kube-system \
--set k8sServiceHost=192.168.10.100 --set k8sServicePort=6443 \
--set kubeProxyReplacement=true \
--set routingMode=native \
--set autoDirectNodeRoutes=true \
--set ipam.mode="cluster-pool" \
--set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} \
--set ipv4NativeRoutingCIDR=172.20.0.0/16 \
--set endpointRoutes.enabled=true \
--set installNoConntrackIptablesRules=true \
--set bpf.masquerade=true \
--set ipv6.enabled=false

# 확인
helm get values cilium -n kube-system
helm list -A
kubectl get crd
watch -d kubectl get pod -A

 

정상적으로 설치되었다면 아래와 같이 상태를 확인할 수 있습니다. 

kubectl get pod -A
kubectl exec -it -n kube-system ds/cilium -c cilium-agent -- cilium-dbg status --verbose

 

kube-proxy를 사용하지 않기 때문에 이제 쿠버네티스 네트워크에서 iptables를 사용하지 않아 아래와 같이 iptables 규칙들이 깔끔하게 정리된 것을 확인할 수 있습니다. 

iptables -t nat -S

 

Pod의 CIDR도 이제는 Cilium CNI로부터 할당 받기 때문에 만약 기존에 생성된 Pod들이 있다면 마이그레이션이 필요합니다. 마이그레이션은 간단히 Pod를 재생성(rollout restart)하는 것으로 해결할 수 있습니다. Cilium의 IPAM은 아래와 같이 확인해볼 수 있습니다. 

kubectl get ciliumnodes -o json ❘ grep podCIDRs -A2

 

최소한의 다운타임으로 기존 CNI에서 마이그레이션을 해야하는 경우 아래 문서를 참고하시기 바랍니다. 

Cilium CLI 설치

Cilium의 운영을 효과적으로 하기 위해 CLI 도구를 설치합니다.

CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz >/dev/null 2>&1
tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz

# cilium 상태 확인
which cilium
cilium status
cilium config view
kubectl get cm -n kube-system cilium-config -o json | jq

#
cilium config set debug true && watch kubectl get pod -A
cilium config view | grep -i debug


# cilium daemon = cilium-dbg
kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg config
kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg status --verbose

 

설치가 정상적으로 완료되면 아래와 같이 Cilium 데몬에 대한 설정 정보를 확인해봅니다. 

kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg config

 

Cilium Network

Cilium CNI가 생성하는 핵심 네트워크 인터페이스들인 cilium_hostcilium_netcilium_health는 각각 고유한 역할과 특성을 가지고 있으며, Cilium의 전체 네트워킹 아키텍처에서 중요한 기능을 담당합니다

http://arthurchiao.art/blog/ctrip-network-arch-evolution/

  • cilium_host : 호스트 네트워크 인터페이스이며, 클러스터 내 Pod와 외부 네트워크 간 연결을 처리합니다. veth 페어의 한쪽 끝으로 cilium_net과 연결되어 있고, eBPF 프로그램이 부착되어 패킷 처리와 정책 적용을 담당합니다. Pod의 네트워크 네임스페이스에서 기본 라우팅 테이블의 게이트웨이로 지정됩니다. 
  • cilium_net : 가상 네트워크 인터페이스이며, 주로 IPv6 주소를 할당 받습니다. cilium_host와 veth 페어를 구성하여 네트워크 연결을 제공하고, TC hook을 통해 eBPF 프로그램을 부착하여 패킷 필터링과 처리를 수행합니다. Pod 간 통신을 관리하기 위해 cilium_net 인터페이스를 활용합니다. 
  • cilium_health : Cilium 클러스터의 전반적인 네트워크 연결 상태를 모니터링하는 핵심 구성요소입니다. lxc_health@<peer> 형태의 veth 페어를 생성하여 별도의 네트워크 네임스페이스에 배치되며, 실제 Pod와 유사한 환경을 시뮬레이션합니다.

이 핵심 네트워크 인터페이스들은 서로 독립적으로 동작하는 것이 아니라 유기적으로 연결되어 Cilium 전체 네트워킹 기능을 구현합니다. 또한 모든 인터페이스에 eBPF 프로그램이 부착되어 다음과 같은 기능들을 제공합니다. 

  • 네트워크 정책 적용
  • 로드밸런싱
  • 서비스 디스커버리
  • 패킷 필터링 및 변조

Cilium CNI를 사용할 때 서비스를 통해 Pod로 트래픽을 전달하는 과정에서 네트워크 기반 로드밸런싱과 소켓 기반 로드밸런싱을 지원합니다. 

여기서 독특한 점은 쿠버네티스의 서비스 동작은 기본적으로 서비스의 엔드포인트로 트래픽을 보내면 DNAT가 발생하면서 대상 Pod의 IP로 트래픽이 전달되는데, Cilium은 eBPF를 사용하기 때문에 Pod에서 노드의 네트워크 인터페이스를 거쳐 패킷이 밖으로 나가기 전에 이미 대상 Pod의 IP로 목적지가 변경됩니다. 이런 이유로 노드에서 tcpdump로 패킷을 캡처해봐도 서비스의 엔드포인트가 아닌 대상 Pod의 IP가 캡처됩니다. 

https://velog.io/@haruband/K8SCilium-Socket-Based-LoadBalancing-기법

 

위 그림에서 Pod1 안에서 동작하는 앱이 connect() 시스템콜을 이용하여 소켓을 연결할 때 목적지 주소가 서비스 주소(10.10.8.55)이더라도 소켓의 목적지 주소를 바로 백엔드 주소(10.0.0.31)로 설정하게 됩니다. 이후 앱에서 해당 소켓을 통해 보내는 모든 패킷의 목적지 주소는 이미 백엔드 주소(10.0.0.31)로 설정되어 있기 때문에 중간에 DNAT 변환 및 역변환 과정이 필요 없어지기 때문에 네트워크 처리 과정이 간소화되는 장점이 있습니다. 

댓글