Cilium CNI란?
eBPF(Berkeley Packet Filter)를 기반으로 Pod Network 환경 + 보안 을 제공하는 CNI Plugin이다.
eBPF를 기반으로 고성능의 네트워크 처리를 제공하고, 다양한 기능을 제공한다.
Cilium 주요 특징
- eBPF 기반 기술 : Cilium은 리눅스 커널의 eBPF 기술을 활용한다. eBPF는 커널 내에서 안전하게 프로그램을 실행할 수 있게 해주는 기술인데, 이를 통해 Cilium에서 커널 단의 빠른 처리를 통한 높은 성능을 제공한다.
- 마이크로서비스 지원 : Cilium은 L3/L4 레벨뿐만 아니라 L7 레벨의 네트워크 정책을 지원한다.
- 네트워크 정책 : Kubernetes Network Policy를 지원하며, 추가적으로 CiliumNetworkPolicy CRD를 통해 자체 네트워크 정책을 구현할 수 있다.
- 로드 밸런싱 : Cilium은 eBPF를 사용하여 kube-proxy를 대체 할 수 있는 고성능의 로드 밸런싱을 제공한다.
- 투명한 암호화 : WireGuard를 사용하여 애플리케이션 수정 없이 노드 간 트래픽을 자동으로 암호화할 수 있다.
Cilium 아키텍처
- Cilium Agent : 각 노드에서 실행되는 핵심 구성 요소로 네트워킹, 로드 밸런싱, 네트워크 정책, 모니터링 요구 사항을 관리한다. Orchestration systems(Kubernetes)의 이벤트를 수신하여 컨테이너나 워크로드의 시작과 중지를 감지한다.
- eBPF Programs : Linux 커널에서 실행되며, 모든 네트워크 접근을 제어하는 구성요소이다. Cilium Agent가 해당 Program을 관리한다.
- Cilium Client (CLI) : Cilium Agent의 REST API와 상호 작용하는 명령줄 도구이다. 로컬 에이전트의 상태를 검사하고 eBPF 맵에 직접 접근할 수 있다.
- Cilium Operator : 새로운 IP 주소 할당이나 kvstore 하트비트 키 업데이트 등 클러스터 전체에 대한 작업을 관리한다.
- CNI Plugin : Kubernetes가 Pod를 스케줄링하거나 종료할 때 호출되어, Cilium API와 상호 작용하여 Pod에 대한 네트워킹, 로드 밸런싱, 네트워크 정책을 설정한다.
(추가)Hubble : Cilium의 네트워크 트래픽을 모니터링하고 가시성을 제공한다. Hubble Server, Relay, CLI, UI 등의 구성 요소로 이루어져 있다.
Cilium CNI 실습을 위한 환경 구성
이번에도 역시 kind를 통한 환경에서 테스트를 수행한다.
https://medium.com/@nahelou.j/play-with-cilium-native-routing-in-kind-cluster-5a9e586a81ca 링크를 보고 구성 및 테스트를 수행하였다.
kind를 통한 cluster 생성
cat <<EOF > kind-ersia-8w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
protocol: TCP
- containerPort: 30001
hostPort: 30001
protocol: TCP
- role: worker
- role: worker
networking:
disableDefaultCNI: true
kubeProxyMode: none
EOF
kind create cluster --config=kind-ersia-8w.yaml --name ersia
# 1.31에서 테스트
Creating cluster "ersia" ...
✓ Ensuring node image (kindest/node:v1.31.0) 🖼
✓ Preparing nodes 📦 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-ersia"
...
- disableDefaultCNI true 설정을 통해 기본 CNI(Container Network Interface)를 비활성화한다.(Cilium을 CNI로 사용하기 위해)
- kubeProxyMode none 설정을 통해 kube-proxy를 비활성화한다.(Cilium으로 kube-proxy의 기능을 대체하기 위해)
kind로 cluster를 생성 후 pod를 확인해보면, pending 상태로 유지되는 pod가 일부 있는 것을 알 수 있다.
IP를 확인해보면 알 수 있는데 cluster에서 IP를 관리해줄 CNI가 없는 상태로 cluster를 생성했기 때문이다.
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl get pod -A -owide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-6f6b679f8f-mbmwr 0/1 Pending 0 4m46s <none> <none> <none> <none>
kube-system coredns-6f6b679f8f-wfv6d 0/1 Pending 0 4m46s <none> <none> <none> <none>
kube-system etcd-ersia-control-plane 1/1 Running 0 4m52s 172.18.0.3 ersia-control-plane <none> <none>
kube-system kube-apiserver-ersia-control-plane 1/1 Running 0 4m54s 172.18.0.3 ersia-control-plane <none> <none>
kube-system kube-controller-manager-ersia-control-plane 1/1 Running 0 4m52s 172.18.0.3 ersia-control-plane <none> <none>
kube-system kube-scheduler-ersia-control-plane 1/1 Running 0 4m54s 172.18.0.3 ersia-control-plane <none> <none>
local-path-storage local-path-provisioner-57c5987fd4-wjtnf 0/1 Pending 0 4m46s <none> <none> <none> <none>
Cilium 설치
설치 전 제약사항 확인
이 부분을 잘 몰라서 맨 처음에 삽질을 좀 했는데 kind 환경에 Cilium을 설치하기 위해서는 몇가지 제약 사항이 있다.
- cgroup v2를 활성화
- Linux의 커널 버전이 5.14 이상
- 컨테이너 런타임이 cgroup v2를 지원해야 한다. (containerd v1.4 이상, cri-o v1.20 이상)
- Kind 노드는 별도의 cgroup 네임스페이스에서 실행해야 하며 이러한 네임스페이스는 Cilium이 올바른 cgroup 계층에서 BPF 프로그램을 연결할 수 있도록 기본 호스트의 cgroup 네임스페이스와 달라야 한다.
참고 : https://docs.cilium.io/en/stable/installation/kind/
wsl2와 가상머신 최신 버전을 설치할 경우 cgroup2를 사용하는지 모르겠지만, 내 환경에서는 cgroup1을 사용하고 있었다.
# cgruop2를 사용할 수 있는지 확인
(⎈|kind-ersia:N/A) root@Ersia:~/week8# uname -a
Linux Ersia 5.15.153.1-microsoft-standard-WSL2 #1 SMP Fri Mar 29 23:14:13 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
(⎈|kind-ersia:N/A) root@Ersia:~/week8# mount -l | grep cgroup2
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
# cgroup1을 사용하고 있음을 확인
(⎈|kind-ersia:N/A) root@Ersia:~/week8# stat -fc %T /sys/fs/cgroup/
tmpfs
(⎈|kind-ersia:N/A) root@Ersia:~/week8# docker info | grep -i "cgroup version"
Cgroup Version: 1
# cgroup2를 사용하게 변경
# Host PC의 CMD 창을 관리자 권한으로 실행
C:\Windows\system32>echo [wsl2] > %UserProfile%\.wslconfig && echo kernelCommandLine = cgroup_no_v1=all >> %UserProfile%\.wslconfig
C:\Windows\system32>type %UserProfile%\.wslconfig
[wsl2]
kernelCommandLine = cgroup_no_v1=all
# wsl2 ubuntu 재시작
C:\Users\user>wsl -t Ubuntu
작업을 완료했습니다.
C:\Users\user>wsl -l -v
NAME STATE VERSION
* Ubuntu Stopped 2
C:\Users\user>wsl -d Ubuntu
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:/mnt/c/Users/user#
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~#
(⎈|kind-ersia:N/A) root@DESKTOP-O4EPQ9T:~# docker info | grep -i "cgroup version"
Cgroup Version: 2
- 참고1 : https://kind.sigs.k8s.io/docs/user/rootless/
- 참고2 : https://stackoverflow.com/questions/73021599/how-to-enable-cgroup-v2-in-wsl2
Cilium CLI 설치
curl -OL https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz
tar xzfv cilium-linux-amd64.tar.gz
mv cilium /usr/local/bin/
# 아직 cilium 설치 전이라 에러가 발생한다
(⎈|kind-ersia:N/A) root@Ersia:~/week8# cilium status
/¯¯\
/¯¯\__/¯¯\ Cilium: 1 errors
\__/¯¯\__/ Operator: disabled
/¯¯\__/¯¯\ Envoy DaemonSet: disabled (using embedded mode)
\__/¯¯\__/ Hubble Relay: disabled
\__/ ClusterMesh: disabled
Containers: cilium
cilium-operator
Cluster Pods: 0/3 managed by Cilium
Helm chart version:
Errors: cilium cilium daemonsets.apps "cilium" not found
status check failed: [daemonsets.apps "cilium" not found]
helm을 통한 Cilium 설치
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kind get kubeconfig --name ersia | grep cluster
clusters:
- cluster:
cluster: kind-ersia
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl get nodes --selector='node-role.kubernetes.io/control-plane'
NAME STATUS ROLES AGE VERSION
ersia-control-plane NotReady control-plane 47m v1.31.0
다만 주의해야할 점이 cluster에 입력하는 정보는 자신의 환경에 맞춰서 수정해야 한다.
(정보가 안맞을 경우 cilium pod들이 계속 CrashLoopBackOff 상태에 빠지면서 정상 동작을 하지 않는다.)
cat <<EOF > cilium-values.yaml
k8sServiceHost: ersia-control-plane
k8sServicePort: 6443
kubeProxyReplacement: true
hostServices:
enabled: false
externalIPs:
enabled: true
nodePort:
enabled: true
hostPort:
enabled: true
image:
pullPolicy: IfNotPresent
ipam:
mode: kubernetes
routingMode: native
ipv4NativeRoutingCIDR: 10.244.0.0/16
enableIPv4Masquerade: true
autoDirectNodeRoutes: true
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
ingress:
enabled: false
service:
type: NodePort
nodePort: 30000
EOF
helm repo add cilium https://helm.cilium.io/
helm install -n kube-system cilium cilium/cilium -f cilium-values.yaml
(⎈|kind-ersia:N/A) root@Ersia:~/week8# helm install -n kube-system cilium cilium/cilium -f cilium-medium.yaml
NAME: cilium
LAST DEPLOYED: Sun Oct 27 02:35:19 2024
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.
Your release version is 1.16.3.
For any further help, visit https://docs.cilium.io/en/v1.16/gettinghelp
- kubeProxyReplacement: true는 Cilium이 kube-proxy의 모든 기능을 완전히 대체하도록 설정한다.
- 참고로한 블로그의 strict 설정은 Cilium v1.15부터 지원하지 않는다.
- https://docs.cilium.io/en/v1.14/operations/upgrade/#deprecated-options
- hubble은 Cilium의 모니터링 도구로 enable을 설정해 같이 구성한다.
- routingMode: native는 Cilium이 호스트의 라우팅 테이블을 직접 관리하도록 설정한다.
- autoDirectNodeRoutes: true는 노드 간 직접 라우팅을 활성화한다.
해당 설정으로 배포 후 2~3분 정도 기다리면, 아래와 같이 IP를 할당받지 못해 Pending 상태에 있던 Pod들이 IP를 할당 받고 정상적으로 기동한 것을 볼 수 있다.
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}'
ersia-control-plane 10.244.0.0/24
ersia-worker 10.244.1.0/24
ersia-worker2 10.244.2.0/24
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system cilium-djjdq 1/1 Running 0 68s 172.18.0.3 ersia-control-plane <none> <none>
kube-system cilium-envoy-ft2nf 1/1 Running 0 68s 172.18.0.3 ersia-control-plane <none> <none>
kube-system cilium-envoy-lmm5x 1/1 Running 0 68s 172.18.0.4 ersia-worker2 <none> <none>
kube-system cilium-envoy-vth5t 1/1 Running 0 68s 172.18.0.2 ersia-worker <none> <none>
kube-system cilium-lcdm6 1/1 Running 0 68s 172.18.0.2 ersia-worker <none> <none>
kube-system cilium-operator-7b4c79db7d-2f6jh 1/1 Running 0 68s 172.18.0.4 ersia-worker2 <none> <none>
kube-system cilium-operator-7b4c79db7d-pffvp 1/1 Running 0 68s 172.18.0.2 ersia-worker <none> <none>
kube-system cilium-prjds 1/1 Running 0 68s 172.18.0.4 ersia-worker2 <none> <none>
kube-system coredns-6f6b679f8f-mbmwr 1/1 Running 0 144m 10.244.0.213 ersia-control-plane <none> <none>
kube-system coredns-6f6b679f8f-wfv6d 1/1 Running 0 144m 10.244.0.54 ersia-control-plane <none> <none>
kube-system etcd-ersia-control-plane 1/1 Running 0 42m 172.18.0.3 ersia-control-plane <none> <none>
kube-system hubble-relay-c56665db6-wgcjp 1/1 Running 0 68s 10.244.1.167 ersia-worker <none> <none>
kube-system hubble-ui-77555d5dcf-lgvq7 2/2 Running 0 68s 10.244.2.53 ersia-worker2 <none> <none>
kube-system kube-apiserver-ersia-control-plane 1/1 Running 0 42m 172.18.0.3 ersia-control-plane <none> <none>
kube-system kube-controller-manager-ersia-control-plane 1/1 Running 5 (43m ago) 144m 172.18.0.3 ersia-control-plane <none> <none>
kube-system kube-scheduler-ersia-control-plane 1/1 Running 5 (43m ago) 144m 172.18.0.3 ersia-control-plane <none> <none>
local-path-storage local-path-provisioner-57c5987fd4-wjtnf 1/1 Running 0 144m 10.244.0.242 ersia-control-plane <none> <none>
(⎈|kind-ersia:N/A) root@Ersia:~/week8# cilium status
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: OK
\__/¯¯\__/ Hubble Relay: OK
\__/ ClusterMesh: disabled
DaemonSet cilium Desired: 3, Ready: 3/3, Available: 3/3
DaemonSet cilium-envoy Desired: 3, Ready: 3/3, Available: 3/3
Deployment cilium-operator Desired: 2, Ready: 2/2, Available: 2/2
Deployment hubble-relay Desired: 1, Ready: 1/1, Available: 1/1
Deployment hubble-ui Desired: 1, Ready: 1/1, Available: 1/1
Containers: cilium Running: 3
cilium-envoy Running: 3
cilium-operator Running: 2
hubble-relay Running: 1
hubble-ui Running: 1
Cluster Pods: 5/5 managed by Cilium
Helm chart version: 1.16.3
Image versions cilium quay.io/cilium/cilium:v1.16.3@sha256:62d2a09bbef840a46099ac4c69421c90f84f28d018d479749049011329aa7f28: 3
cilium-envoy quay.io/cilium/cilium-envoy:v1.29.9-1728346947-0d05e48bfbb8c4737ec40d5781d970a550ed2bbd@sha256:42614a44e508f70d03a04470df5f61e3cffd22462471a0be0544cf116f2c50ba: 3
cilium-operator quay.io/cilium/operator-generic:v1.16.3@sha256:6e2925ef47a1c76e183c48f95d4ce0d34a1e5e848252f910476c3e11ce1ec94b: 2
hubble-relay quay.io/cilium/hubble-relay:v1.16.3@sha256:feb60efd767e0e7863a94689f4a8db56a0acc7c1d2b307dee66422e3dc25a089: 1
hubble-ui quay.io/cilium/hubble-ui-backend:v0.13.1@sha256:0e0eed917653441fded4e7cdb096b7be6a3bddded5a2dd10812a27b1fc6ed95b: 1
hubble-ui quay.io/cilium/hubble-ui:v0.13.1@sha256:e2e9313eb7caf64b0061d9da0efbdad59c6c461f6ca1752768942bfeda0796c6: 1
# 아래의 명령어는 상당히 오래(체감상 약 30분 이상?)걸리기 때문에 시간 여유가 없다면 패스하자
(⎈|kind-ersia:N/A) root@Ersia:~/week8# cilium connectivity test
ℹ️ Monitor aggregation detected, will skip some flow validation steps
✨ [kind-ersia] Creating namespace cilium-test-1 for connectivity check...
✨ [kind-ersia] Deploying echo-same-node service...
✨ [kind-ersia] Deploying DNS test server configmap...
✨ [kind-ersia] Deploying same-node deployment...
✨ [kind-ersia] Deploying client deployment...
✨ [kind-ersia] Deploying client
...
...
ℹ️ Cilium version: 1.16.3
🏃[cilium-test-1] Running 102 tests ...
[=] [cilium-test-1] Test [no-unexpected-packet-drops] [1/102]
...
[=] [cilium-test-1] Test [no-policies] [2/102]
...
# 만약 실수로 명령어를 입력했다면, 취소하고 테스트 리소스를 삭제한다.
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl delete ns cilium-test-1
namespace "cilium-test-1" deleted
autoDirectNodeRoutes 설정을 true로 하고 배포했기 때문에 각 노드는 다른 모든 노드의 모든 Pod IP를 자동으로 인식하고 라우팅 테이블에 경로를 삽입한다.
(⎈|kind-ersia:N/A) root@Ersia:~/week8# docker exec -it ersia-control-plane ip -c route
default via 172.18.0.1 dev eth0
10.244.0.0/24 via 10.244.0.180 dev cilium_host proto kernel src 10.244.0.180
10.244.0.180 dev cilium_host proto kernel scope link
10.244.1.0/24 via 172.18.0.2 dev eth0 proto kernel
10.244.2.0/24 via 172.18.0.4 dev eth0 proto kernel
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3
(⎈|kind-ersia:N/A) root@Ersia:~/week8# docker exec -it ersia-worker ip -c route
default via 172.18.0.1 dev eth0
10.244.0.0/24 via 172.18.0.3 dev eth0 proto kernel
10.244.1.0/24 via 10.244.1.64 dev cilium_host proto kernel src 10.244.1.64
10.244.1.64 dev cilium_host proto kernel scope link
10.244.2.0/24 via 172.18.0.4 dev eth0 proto kernel
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
(⎈|kind-ersia:N/A) root@Ersia:~/week8# docker exec -it ersia-worker2 ip -c route
default via 172.18.0.1 dev eth0
10.244.0.0/24 via 172.18.0.3 dev eth0 proto kernel
10.244.1.0/24 via 172.18.0.2 dev eth0 proto kernel
10.244.2.0/24 via 10.244.2.36 dev cilium_host proto kernel src 10.244.2.36
10.244.2.36 dev cilium_host proto kernel scope link
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.4
Hubble UI 접속 테스트
배포할 때 30000 포트로 kind에서 매핑을 했고, Cilium 배포시에도 30000포트를 NodePort로 할당했으니 바로 접속하면 된다.
Cilium 테스트
Cilium에서 제공하는 테스트용 어플리케이션 배포 차트를 사용한다.
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/http-sw-app.yaml
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl get pod
NAME READY STATUS RESTARTS AGE
deathstar-bf77cddc9-5sqd2 1/1 Running 0 18s
deathstar-bf77cddc9-6z9d2 1/1 Running 0 18s
tiefighter 1/1 Running 0 18s
xwing 1/1 Running 0 18s
배포하고 Hubble UI에서 확인하면 아무것도 안나와서 당황할 수 있는데, 안나오는게 정상이다.
모니터링할 트래픽이 없기 때문이다.
통신 테스트
아래 명령어로 cluster 내부에서 외부로 DNS Resolution 테스트를 해보자.
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl exec tiefighter -- curl --connect-timeout 10 -s https://swapi.dev/api/starships
{"count":36,"next":"https://swapi.dev/api/starships/?page=2","previous":null,
...
...
,"url":"https://swapi.dev/api/starships/17/"}]}
이런식으로 Hubble UI에서 트래픽의 흐름을 바로바로 표시해준다.
cluster 내부 통신도 테스트해보자.
for i in {1..10}; do
kubectl exec tiefighter -- curl --connect-timeout 10 -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
done
tiefighter Pod에서 deathstart 서비스로 통신이 수행되는 흐름을 확인할 수 있다.
추가 기능 테스트
Cilium은 제공하는 기능이 어마어마하게 많은데, 이 중에 kube-proxy 비활성화 상태에서 몇 가지만 추려서 테스트 해보았다.
- https://docs.cilium.io/en/stable/cmdref/cilium-agent/
- https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free
Maglev 로드 밸런싱
Maglev는 구글에서 개발한 일관 해싱 알고리즘으로, Cilium은 해당 알고리즘을 지원해 더 효율적이고 안정적인 로드 밸런싱을 제공한다.
- Maglev 해싱은 외부(NS) 트래픽에만 적용
- maglev.tableSize 및 maglev.hashSeed 설정으로 Maglev 알고리즘을 세부적으로 컨트롤
- 장애 발생 시 복원성을 향상
https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#maglev-consistent-hashing
아래의 echo-server 앱을 배포하고 Maglev 적용 전/후를 비교한다.
# 테스트용 앱 배포(Pod 5개)
cat <<EOF > echo-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
spec:
replicas: 5
selector:
matchLabels:
app: echo-server
template:
metadata:
labels:
app: echo-server
spec:
containers:
- name: echo-server
image: ealen/echo-server
ports:
- containerPort: 80
EOF
kubectl apply -f echo-server.yaml
# 호출을 위한 NodePort 서비스 생성
cat <<EOF > echo-server-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: echo-server
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: echo-server
EOF
kubectl apply -f echo-server-svc.yaml
적용 전 호출
# 기본 라운드로빈 방식 로드밸런싱
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
108 "HOSTNAME": "echo-server-666c84646c-vhkcr",
105 "HOSTNAME": "echo-server-666c84646c-jtslj",
105 "HOSTNAME": "echo-server-666c84646c-4fnlj",
102 "HOSTNAME": "echo-server-666c84646c-plwq9",
80 "HOSTNAME": "echo-server-666c84646c-8w2zx",
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl scale deployment echo-server --replicas=6
deployment.apps/echo-server scaled
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
102 "HOSTNAME": "echo-server-666c84646c-4fnlj",
91 "HOSTNAME": "echo-server-666c84646c-jtslj",
83 "HOSTNAME": "echo-server-666c84646c-8w2zx",
79 "HOSTNAME": "echo-server-666c84646c-plwq9",
76 "HOSTNAME": "echo-server-666c84646c-d24rq",
69 "HOSTNAME": "echo-server-666c84646c-vhkcr",
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl scale deployment echo-server --replicas=3
deployment.apps/echo-server scaled
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
175 "HOSTNAME": "echo-server-666c84646c-8w2zx",
170 "HOSTNAME": "echo-server-666c84646c-vhkcr",
155 "HOSTNAME": "echo-server-666c84646c-4fnlj",
적용 후 호출
# 적용
helm upgrade cilium cilium/cilium \
--namespace kube-system \
--reuse-values \
--set loadBalancer.algorithm=maglev \
--set maglev.tableSize=16381 \
--set maglev.hashSeed=$(head -c12 /dev/urandom | base64 -w0)
# 롤백
helm upgrade cilium cilium/cilium \
--namespace kube-system \
--reuse-values \
--set loadBalancer.algorithm=random
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl scale deployment echo-server --replicas=5
deployment.apps/echo-server scaled
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl -n kube-system get configmap cilium-config -o yaml | grep -i maglev
bpf-lb-algorithm: maglev
bpf-lb-maglev-hash-seed: YO9vCjJo9fTG0SL2
bpf-lb-maglev-table-size: "16381"
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
114 "HOSTNAME": "echo-server-666c84646c-jtslj",
104 "HOSTNAME": "echo-server-666c84646c-vhkcr",
99 "HOSTNAME": "echo-server-666c84646c-plwq9",
94 "HOSTNAME": "echo-server-666c84646c-4fnlj",
89 "HOSTNAME": "echo-server-666c84646c-8w2zx",
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl scale deployment echo-server --replicas=6
deployment.apps/echo-server scaled
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
98 "HOSTNAME": "echo-server-666c84646c-jtslj",
86 "HOSTNAME": "echo-server-666c84646c-plwq9",
83 "HOSTNAME": "echo-server-666c84646c-8w2zx",
82 "HOSTNAME": "echo-server-666c84646c-vhkcr",
77 "HOSTNAME": "echo-server-666c84646c-4fnlj",
74 "HOSTNAME": "echo-server-666c84646c-745rh",
(⎈|kind-ersia:N/A) root@Ersia:~/week8# kubectl scale deployment echo-server --replicas=3
deployment.apps/echo-server scaled
(⎈|kind-ersia:N/A) root@Ersia:~/week8# for i in {1..500}; do curl -s localhost:30001 | jq | grep HOSTNAME ; done | sort | uniq -c | sort -nr
175 "HOSTNAME": "echo-server-666c84646c-vhkcr",
173 "HOSTNAME": "echo-server-666c84646c-4fnlj",
152 "HOSTNAME": "echo-server-666c84646c-8w2zx",
구성 자체가 너무 작기 때문에 기본 라운드로빈보다 더 효율적이고 안정적인 로드밸런싱이 되는지는 극적으로 체감하지 못했지만, 여러 번 시도해볼 때 Pod가 추가되고 삭제될 때 조금 더 안정적으로 분배가 이루어지는 느낌이다. (데이터로는 산출하지 못한 그냥 느낌...)
Direct Server Return (DSR)
Cilium의 eBPF NodePort는 기본적으로 SNAT(Source Network Address Translation) 모드로 동작한다.
이 방식은 추가적인 MTU 변경이 필요 없지만 응답 패킷이 추가적인 홉(hop)을 거쳐야 한다는 단점이 있다.
이 때 DSR 모드를 사용하면 백엔드가 홉을 거치지 않고 바로 응답을 하게 된다.
- 백엔드는 서비스 IP/포트를 소스로 사용하여 응답
- 추가적인 홉을 거치지 않아 네트워크 지연이 감소
- 중간 노드를 거치지 않아 네트워크 리소스 사용이 감소
- 추가 패킷 헤더로 인해 MTU 조정이 필요할 수 있음
- 다만 직접 Pod가 응답하기 때문에 cluster 내부 등 네트워크 보안 정책을 조정해야 할 수 있음
- 몇몇 클라우드(예: AWS)환경에서는 Source/Destination Check을 하기 때문에 DSR을 쓰기 위해서는 해당 Check 비활성화가 요구됨
테스트 앱은 그대로 echo-server를 활용한다.
# 현재 mode 확인
(⎈|kind-ersia:N/A) root@Ersia:~# kubectl -n kube-system get configmap cilium-config -o yaml | grep -i lb-mode
bpf-lb-mode: snat
# 적용 전 worker 노드 1번에서 tcpdump 후 호출이 발생할 때까지 curl 호출
root@ersia-worker:/# tcpdump -i any -n "port 80 or port 30001"
# 외부에서 호출
curl http://localhost:30001
20:36:14.465392 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [S], seq 2053189904, win 64240, options [mss 1460,sackOK,TS val 2964217116 ecr 0,nop,wscale 7], length 0
20:36:14.465455 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [S], seq 2053189904, win 64240, options [mss 1460,sackOK,TS val 2964217116 ecr 0,nop,wscale 7], length 0
20:36:14.465467 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [S.], seq 3383039225, ack 2053189905, win 65160, options [mss 1460,sackOK,TS val 303720789 ecr 2964217116,nop,wscale 7], length 0
20:36:14.465483 eth0 Out IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [S.], seq 3383039225, ack 2053189905, win 65160, options [mss 1460,sackOK,TS val 303720789 ecr 2964217116,nop,wscale 7], length 0
20:36:14.465510 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2964217117 ecr 303720789], length 0
20:36:14.465520 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2964217117 ecr 303720789], length 0
20:36:14.465743 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 2964217117 ecr 303720789], length 79: HTTP: GET / HTTP/1.1
20:36:14.465799 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 2964217117 ecr 303720789], length 79: HTTP: GET / HTTP/1.1
20:36:14.465832 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [.], ack 80, win 509, options [nop,nop,TS val 303720789 ecr 2964217117], length 0
20:36:14.465841 eth0 Out IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [.], ack 80, win 509, options [nop,nop,TS val 303720789 ecr 2964217117], length 0
20:36:14.467751 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 303720791 ecr 2964217117], length 1005: HTTP: HTTP/1.1 200 OK
20:36:14.467795 eth0 Out IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 303720791 ecr 2964217117], length 1005: HTTP: HTTP/1.1 200 OK
20:36:14.467849 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1006, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
20:36:14.467857 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1006, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
20:36:14.468136 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [F.], seq 80, ack 1006, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
20:36:14.468175 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [F.], seq 80, ack 1006, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
20:36:14.468276 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 303720791 ecr 2964217119], length 0
20:36:14.468320 eth0 Out IP 10.244.1.159.80 > 172.18.0.3.38986: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 303720791 ecr 2964217119], length 0
20:36:14.468390 eth0 In IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1007, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
20:36:14.468432 lxcc819c38283f9 Out IP 172.18.0.3.38986 > 10.244.1.159.80: Flags [.], ack 1007, win 501, options [nop,nop,TS val 2964217119 ecr 303720791], length 0
DSR 적용 후 확인
# DSR 적용
helm upgrade cilium cilium/cilium \
--namespace kube-system \
--reuse-values \
--set loadBalancer.mode=dsr
kubectl rollout restart ds/cilium -n kube-system
(⎈|kind-ersia:N/A) root@Ersia:~# kubectl -n kube-system get configmap cilium-config -o yaml | grep -i lb-mode
bpf-lb-mode: dsr
# 적용 후 worker 노드 1번에서 tcpdump 후 호출이 발생할 때까지 curl 호출
root@ersia-worker:/# tcpdump -i any -n "port 80 or port 30001"
# 외부에서 호출
curl http://localhost:30001
20:29:06.436351 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [S], seq 22104225, win 64240, options [mss 1460,sackOK,TS val 2963789087 ecr 0,nop,wscale 7], length 0
20:29:06.436413 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [S], seq 22104225, win 64240, options [mss 1460,sackOK,TS val 2963789087 ecr 0,nop,wscale 7], length 0
20:29:06.436424 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [S.], seq 1283327774, ack 22104226, win 65160, options [mss 1460,sackOK,TS val 3435184027 ecr 2963789087,nop,wscale 7], length 0
20:29:06.436443 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [S.], seq 1283327774, ack 22104226, win 65160, options [mss 1460,sackOK,TS val 3435184027 ecr 2963789087,nop,wscale 7], length 0
20:29:06.436484 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2963789088 ecr 3435184027], length 0
20:29:06.436492 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2963789088 ecr 3435184027], length 0
20:29:06.436733 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 2963789088 ecr 3435184027], length 79: HTTP: GET / HTTP/1.1
20:29:06.436798 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 2963789088 ecr 3435184027], length 79: HTTP: GET / HTTP/1.1
20:29:06.436822 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [.], ack 80, win 509, options [nop,nop,TS val 3435184027 ecr 2963789088], length 0
20:29:06.436830 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [.], ack 80, win 509, options [nop,nop,TS val 3435184027 ecr 2963789088], length 0
20:29:06.439175 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 3435184029 ecr 2963789088], length 1005: HTTP: HTTP/1.1 200 OK
20:29:06.439245 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 3435184029 ecr 2963789088], length 1005
20:29:06.439331 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1006, win 501, options [nop,nop,TS val 2963789090 ecr 3435184029], length 0
20:29:06.439344 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1006, win 501, options [nop,nop,TS val 2963789090 ecr 3435184029], length 0
20:29:06.439765 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [F.], seq 80, ack 1006, win 501, options [nop,nop,TS val 2963789091 ecr 3435184029], length 0
20:29:06.439816 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [F.], seq 80, ack 1006, win 501, options [nop,nop,TS val 2963789091 ecr 3435184029], length 0
20:29:06.439946 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 3435184030 ecr 2963789091], length 0
20:29:06.440046 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 3435184030 ecr 2963789091], length 0
20:29:06.440089 eth0 In IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1007, win 501, options [nop,nop,TS val 2963789091 ecr 3435184030], length 0
20:29:06.440116 lxcc819c38283f9 Out IP 172.18.0.1.36098 > 10.244.1.159.80: Flags [.], ack 1007, win 501, options [nop,nop,TS val 2963789091 ecr 3435184030], length 0
# DSR에서 SNAT로 변경
helm upgrade cilium cilium/cilium \
--namespace kube-system \
--reuse-values \
--set loadBalancer.mode=snat
kubectl rollout restart ds/cilium -n kube-system
전/후의 패킷을 보면 응답 패킷의 Src IP가 다른 것을 볼 수 있다.
# 초기 SYN-ACK 응답
20:29:06.436424 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [S.], seq 1283327774, ack 22104226, win 65160, options [mss 1460,sackOK,TS val 3435184027 ecr 2963789087,nop,wscale 7], length 0
20:29:06.436443 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [S.], seq 1283327774, ack 22104226, win 65160, options [mss 1460,sackOK,TS val 3435184027 ecr 2963789087,nop,wscale 7], length 0
# 실제 데이터에 대한 응답
20:29:06.439175 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 3435184029 ecr 2963789088], length 1005: HTTP: HTTP/1.1 200 OK
20:29:06.439245 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [P.], seq 1:1006, ack 80, win 509, options [nop,nop,TS val 3435184029 ecr 2963789088], length 1005
# 연결종료 FIN 응답
20:29:06.439946 lxcc819c38283f9 In IP 10.244.1.159.80 > 172.18.0.1.36098: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 3435184030 ecr 2963789091], length 0
20:29:06.440046 eth0 Out IP 172.18.0.3.30001 > 172.18.0.1.36098: Flags [F.], seq 1006, ack 81, win 509, options [nop,nop,TS val 3435184030 ecr 2963789091], length 0
Pod(10.244.1.159:80)에서 생성된 SYN-ACK 응답이 클라이언트로 나갈 때 소스 IP가 서비스 IP(172.18.0.3.30001)로 변경되는 것을 확인할 수 있다.
이로인해 클라이언트는 항상 서비스 IP와 통신하는 것처럼 보이며, 실제 서버 IP를 감출 수 있게 된다.
참고자료
- Cilium 공식문서 : https://docs.cilium.io/en/v1.11/concepts/overview/#cilium
- What is eBPF? : https://www.datadoghq.com/knowledge-center/ebpf/
- The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel : https://lemonade-devlog.tistory.com/17