이 내용은 CloudNet@에서 진행하는 Cilium 스터디를 참여하면서 공부하는 내용을 기록하며, CloudNet@에서 제공해주는 자료들을 바탕으로 작성되었습니다.
들어가며
kubernetes는 기본적으로 kube-proxy를 통해 네트워크를 처리합니다. 하지만 iptables를 사용하는 kube-proxy의 경우 cluster가 커지고, 서비스가 많아질수록 iptables의 규칙이 수천 개 이상으로 증가하게 되는데 규칙이 많아질수록 packet이 전달될 때마다 순차적으로 검사하게 되므로 latency와 CPU 사용량이 증가하게 됩니다. (이것이 곧 성능 저하 및 관리 복잡도 상승...!)
이를 해결하기 위해 등장한 Cilium을 통해 iptables없이 kurnel 레벨에서 직접 트래픽을 처리하는 eBPF 기반의 네트워크 처리 실습을 진행해보겠습니다.
Cilium 배포 및 통신 실습
위와 같은 환경을 Vagrant와 VirtualBox를 활용하여 구성합니다.
네트워크적으로 기본적인 환경은 위과 같고, eth0은 10.0.2.15로 모든 노드가 동일하지만 eth1은 192.168.10.100, 192.168.10.101, 192.168.10.102로 구성되어있습니다.
실습 환경 구성
VirtualBox 설치 및 확인
# VirtualBox 설치
brew install --cask virtualbox
# VirtualBox 설치 확인
VBoxManage --version
Vagrant 설치 및 확인
# Vagrant 설치
brew install --cask vagrant
# Vagrant 설치 확인
vagrant version
실습 환경 배포 파일 작성
환경 변수 설정
# Kubernetes Version
K8SV = '1.33.2-1.1'
# Containerd Version
CONTAINERDV = '1.7.27-1'
# Worker node 갯수
N = 2
BOX_IMAGE = "bento/ubuntu-24.04"
BOX_VERSION = "202502.21.0"
init_cfg.sh
#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"
echo "[TASK 1] Setting Profile & Change Timezone"
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/vagrant/.bashrc
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
echo "[TASK 2] Disable AppArmor"
systemctl stop ufw && systemctl disable ufw >/dev/null 2>&1
systemctl stop apparmor && systemctl disable apparmor >/dev/null 2>&1
echo "[TASK 3] Disable and turn off SWAP"
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab
echo "[TASK 4] Install Packages"
apt update -qq >/dev/null 2>&1
apt-get install apt-transport-https ca-certificates curl gpg -y -qq >/dev/null 2>&1
# Download the public signing key for the Kubernetes package repositories.
mkdir -p -m 755 /etc/apt/keyrings
K8SMMV=$(echo $1 | sed -En 's/^([0-9]+\.[0-9]+)\..*/\1/p')
curl -fsSL https://pkgs.k8s.io/core:/stable:/v$K8SMMV/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v$K8SMMV/deb/ /" >> /etc/apt/sources.list.d/kubernetes.list
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# packets traversing the bridge are processed by iptables for filtering
echo 1 > /proc/sys/net/ipv4/ip_forward
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/k8s.conf
# enable br_netfilter for iptables
modprobe br_netfilter
modprobe overlay
echo "br_netfilter" >> /etc/modules-load.d/k8s.conf
echo "overlay" >> /etc/modules-load.d/k8s.conf
echo "[TASK 5] Install Kubernetes components (kubeadm, kubelet and kubectl)"
# Update the apt package index, install kubelet, kubeadm and kubectl, and pin their version
apt update >/dev/null 2>&1
# apt list -a kubelet ; apt list -a containerd.io
apt-get install -y kubelet=$1 kubectl=$1 kubeadm=$1 containerd.io=$2 >/dev/null 2>&1
apt-mark hold kubelet kubeadm kubectl >/dev/null 2>&1
# containerd configure to default and cgroup managed by systemd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
# avoid WARN&ERRO(default endpoints) when crictl run
cat <<EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF
# ready to install for k8s
systemctl restart containerd && systemctl enable containerd
systemctl enable --now kubelet
echo "[TASK 6] Install Packages & Helm"
apt-get install -y bridge-utils sshpass net-tools conntrack ngrep tcpdump ipset arping wireguard jq tree bash-completion unzip kubecolor >/dev/null 2>&1
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash >/dev/null 2>&1
echo ">>>> Initial Config End <<<<"
k8s-ctr.sh
#!/usr/bin/env bash
echo ">>>> K8S Controlplane config Start <<<<"
echo "[TASK 1] Initial Kubernetes"
kubeadm init --token 123456.1234567890123456 --token-ttl 0 --pod-network-cidr=10.244.0.0/16 --service-cidr=10.96.0.0/16 --apiserver-advertise-address=192.168.10.100 --cri-socket=unix:///run/containerd/containerd.sock >/dev/null 2>&1
echo "[TASK 2] Setting kube config file"
mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config
echo "[TASK 3] Source the completion"
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'source <(kubeadm completion bash)' >> /etc/profile
echo "[TASK 4] Alias kubectl to k"
echo 'alias k=kubectl' >> /etc/profile
echo 'alias kc=kubecolor' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile
echo "[TASK 5] Install Kubectx & Kubens"
git clone https://github.com/ahmetb/kubectx /opt/kubectx >/dev/null 2>&1
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
echo "[TASK 6] Install Kubeps & Setting PS1"
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1 >/dev/null 2>&1
cat <<"EOT" >> /root/.bash_profile
source /root/kube-ps1/kube-ps1.sh
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab" >/dev/null 2>&1
echo "[TASK 6] Install Kubeps & Setting PS1"
echo "192.168.10.100 k8s-ctr" >> /etc/hosts
for (( i=1; i<=$1; i++ )); do echo "192.168.10.10$i k8s-w$i" >> /etc/hosts; done
echo ">>>> K8S Controlplane Config End <<<<"
k8s-w.sh
kubeadm join --token 123456.1234567890123456 --discovery-token-unsafe-skip-ca-verification 192.168.10.100:6443 >/dev/null 2>&1
Vagrantfile
Vagrant.configure("2") do |config|
#-ControlPlane Node
config.vm.define "k8s-ctr" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/Cilium-Lab"]
vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.name = "k8s-ctr"
vb.cpus = 2
vb.memory = 2048
vb.linked_clone = true
end
subconfig.vm.host_name = "k8s-ctr"
subconfig.vm.network "private_network", ip: "192.168.10.100"
subconfig.vm.network "forwarded_port", guest: 22, host: 60000, auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ K8SV, CONTAINERDV]
subconfig.vm.provision "shell", path: "k8s-ctr.sh", args: [ N ]
end
#-Worker Nodes Subnet1
(1..N).each do |i|
config.vm.define "k8s-w#{i}" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/Cilium-Lab"]
vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.name = "k8s-w#{i}"
vb.cpus = 2
vb.memory = 1536
vb.linked_clone = true
end
subconfig.vm.host_name = "k8s-w#{i}"
subconfig.vm.network "private_network", ip: "192.168.10.10#{i}"
subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ K8SV, CONTAINERDV]
subconfig.vm.provision "shell", path: "k8s-w.sh"
end
end
end
각 구성에서 subconfig.vm.provision에서 args를 위의 init_cfg.sh와 k8s-ctr, k8s-w.sh의 경로를 넣어줍니다.
실습 환경 배포
vagrant up
vargrant up 명령어를 통해 위의 환경 구성 파일들을 실행시켜줍니다.
그럼 위와 같이 실행이 되는 것을 확인하실 수 있습니다. 완료되기까지 대략 5~10분정도 소요됩니다.
VirtualBox로 확인
VirtualBox를 실행시켜 확인해보면 위와 같이 Cilium-Lab 폴더에 k8s-ctr, k8s-w1, k8s-w2가 배포된 것을 확인하실 수 있습니다.
각 node들에 접속
# k8s-ctr ssh 접속
vagrant ssh k8s-ctr
# k8s-w1 ssh 접속
vagrant ssh k8s-w1
# k8s-w2 ssh 접속
vagrant ssh k8s-w2
위와 같이 vargrant ssh 명령어를 통해 각 node에 접근합니다.
[k8s-ctr] 접속 후 기본 정보 확인
vagrant ssh k8s-ctr
위와 같이 vargrant ssh 명령어를 통해 control plane에 접근합니다.
worker node들과 ping test
cat /etc/hosts
먼저 os의 hosts 목록들을 확인합니다.
k8s-ctr 뿐 아니라 k8s-w1, k8s-w2의 IP가 각각 등록되어있습니다.
ping -c 1 k8s-w1
k8s-w1로 ping test를 하면 위와 같이 성공적으로 동작한 것을 확인하실 수 있습니다.
INTERNAL-IP 변경 설정
kubectl get node -o wide
위 명령어로 node의 상세정보를 확인해보면 아래와 같이 k8s-w1, k8s-w2의 INTERNAL-IP가 eth0의 주소인 10.0.2.15로 설정되어 있는 것을 확인하실 수 있습니다.
각 node별로 원활한 통신 환경을 구성하기 위해 eth0이 아닌 eth1의 IP로 변경하도록 하겠습니다.
cat /var/lib/kubelet/kubeadm-flags.env
kubeadm의 환경을 확인해겠습니다.
별 구성이 되어있지 않아 기본 네트워크 인터페이스인 eth0으로 구성되어있는 것을 확인하실 수 있습니다.
NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
sed -i "s/^\(KUBELET_KUBEADM_ARGS=\"\)/\1--node-ip=${NODEIP} /" /var/lib/kubelet/kubeadm-flags.env
systemctl daemon-reexec && systemctl restart kubelet
위 명령어를 통해 기본 네트워크 인터페이스를 eth1로 변경하고 kubelet을 재시작합니다.
그 후 다시 확인해보면 위와 같이 node의 ip가 변경된 것을 확인하실 수 있습니다.
k8s-ctr node에 접근하여 node 정보를 확인해보면 위와 같이 정상적으로 변경된 것을 확인하실 수 있습니다.
Cilium 설치
이제 기본적인 환경 구성이 완료되었니 Cilium을 설치해보도록 하겠습니다.
요구 사항 확인
먼저 Cilium을 설치하기 전에 링크를 통해 Cilium 시스템 요구 사항 확인해야 합니다. 주요 요구 사항은 AMD64 또는 AArch64 CPU 아키텍처를 사용하는 호스트, Linux 커널 5.4 이상 또는 동등 버전입니다.
또한 추후에 Cilium의 고급 기능을 사용하기 위해서는 링크에서 최소 커널 버전을 확인해야 합니다.
위처럼 여러 요구사항을 일일이 검증하기엔 무리가 있으니 (GPT가 만들어준) 스크립트를 하나 돌려줍니다.
curl -sSL https://raw.githubusercontent.com/leehosu/cilium-requirements-check/main/script.sh -o cilium-check.sh
bash cilium-check.sh
sciprt를 받아와 실행시켜줍니다.
위의 사진과 같이 각 영역별로 요구 사항을 체크 후 간략하게 보여주는 스크립트입니다. 제 환경에서는 기본적인 요구사항은 통과 했으니 이제 Cilium을 설치해주도록 하겠습니다.
kube-proxy 제거
kubectl -n kube-system delete ds kube-proxy
kubectl -n kube-system delete cm kube-proxy
공식 문서를 참고로 kube-proxy를 제거 하고 Cilium을 설치하도록 하겠습니다.
Cilium은 eBPF 기반의 kube-proxy-free 모드를 지원하며, Kubernetes Service 기능(ClusterIP, NodePort 등)을 자체적으로 처리할 수 있습니다. 이 경우 kube-proxy는 필요 없게 됩니다. 이를 통해 kube-proxy를 제거하면 구성 요소가 줄고, 충돌 가능성 및 설정 오류 가능성이 감소합니다.
helm을 통해 Cilium 1.17.5 설치
모든 NIC 지정하여 설치
helm repo add cilium https://helm.cilium.io/
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
kubectl get po -A
cilium 관련 pod들이 정상적으로 배포된 것을 확인하실 수 있습니다.
샘플 애플리케이션 설치
샘플 애플리케이션 배포
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webpod
spec:
replicas: 2
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
샘플 애플리케이션 설치 확인
위와 같이 정상적으로 배포된 것을 확인하실 수 있습니다.
kube-proxy 없이 cilium만으로 pod간 통신이 이루어지는 지 확인해보겠습니다.
kubectl exec -it curl-pod -- curl webpod | grep Hostname
정상적으로 통신이 이루어지는 것을 확인할 수 있습니다!
Cilium CLI 설치
환경 변수 지정
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
cilium cli 설치
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 상태 확인
cilium status
설치 후 cilium status 명령어를 통해 cilium의 상태를 확인하면서 정상적으로 설치된 것을 알 수 있습니다.
Cilium 디버그 로깅 활성화
cilium config set debug true
문제가 생겼거나 내부 동작을 정밀하게 확인하기 위해 Cilium Agent 설정에서 debug 옵션을 ture로 설정하여 자세한 로그를 출력하겠습니다.
환경 변수 및 alias 설정
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-w2 -o jsonpath='{.items[0].metadata.name}')
alias c0="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium"
alias c1="kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium"
alias c2="kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium"
alias c0bpf="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- bpftool"
alias c1bpf="kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- bpftool"
alias c2bpf="kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- bpftool"
원활한 실습 진행을 위해 node 및 pod의 정보등을 환경변수와 alias로 설정하겠습니다.
- c0,c1,c2는 각각 Cilium Pod에 접속하여 cilium CLI를 실행하는 명령어
- c0bpf, c1bpf, c2bpf는 해당 Cilium Pod 내에서 bpftool을 실행하여 Cilium이 사용하는 BPF 맵, 프로그램, 링크 등을 조회하거나 조작하는 명령어입니다.
Cilium 정보 확인
현재 Pod는 control plane에 떠있는 curl-pod와 각 woker node에 떠있는 webpod들이 있습니다.
이때, node간 통신을 확인해보기 위해 curl-pod에서 k8s-w1 node에 배포되어있는 webpod와 통신하기 위한 정보를 확인해보겠습니다.
엔드포인트 정보 확인
kubectl get pod -owide
kubectl get svc,ep webpod
가장 먼저 pod의 상세 정보와 webpod의 svc, endpoint를 조회해보겠습니다.
k8s-w1 node에 배포되어있는 webopd의 Endpot는 IP는 현재 172.20.0.143이므로 해당 값을 WEBPOD1IP라는 환경 변수로 설정합니다.
WEBPOD1IP=172.20.0.143
BPF maps 확인
c0 map get cilium_ipcache | grep $WEBPOD1IP
eBPF 맵 중 하나인 cilium_ipcache의 내용을 출력하는 명령어인 cilium map get cilium_ipcach을 통해 Cilium이 통신하는 IP 주소를 확인해보겠습니다.
현재 보면 WEBPOD1IP의 Pod와 통신하기 위해서는 192.168.10.101로 통신하라는 정보를 확인할 수 있습니다.
Curl pod의 LXC 인터페이스 확인
Cilium은 eBPF를 활용해 Pod 단위로 네트워크 인터페이스(lxc*)를 생성하게 되는데, 이 인터페이스는 해당 Pod에 대한 네트워크 트래픽을 처리합니다.
ip -c a
위 명령어를 통해 LXC 변수를 확인하고 해당 값을 환경변수로 설정합니다.
LXC=lxccc8f25b70e8d
webpod에 연결되어있는 eBPF 프로그램 확인
c0bpf net show | grep $LXC
위 명령어를 통해 Webpod1의 veth 인터페이스(lxccc8f25b70e8d)에 연결된 eBPF 프로그램을 확인할 수 있습니다. 이를 통해 Cilium이 해당 인터페이스에서 어떤 eBPF 프로그램을 사용해 패킷을 처리하고 있는지 확인할 수 있습니다.
이런식으로 map을 타고타고 들어가게되면 map을 타고타고 들어가게 되면, Cilium이 각 네트워크 인터페이스에 어떤 eBPF 프로그램을 attach했는지, 그 프로그램이 참조하는 내부 BPF 맵들(ipcache, policy map 등)은 무엇인지, 그리고 최종적으로 어떤 방식으로 패킷을 필터링/포워딩/정책 적용하는지를 확인할 수 있습니다
노드 간 ‘파드 → 파드’ 통신 확인
k8s-w1 node 접속
vagrant ssh k8s-w1
위 명령어를 통해 k8s-w1 node에 접속합니다.
[k8s-w1] 트래픽 확인
ngrep -tW byline -d eth1 '' 'tcp port 80'
ngrep 명령어를 사용해 eth1 인터페이스에서 TCP 80번 포트(HTTP)로 오가는 트래픽을 실시간으로 출력해보겠습니다.
k8s-ctr 접속
vagrant ssh k8s-ctr
이번엔 새로운 터미널을 열어서 k8s-ctr node에 접속합니다.
[k8s-ctr] curl-pod 에서 curl 요청 시도
kubectl exec -it curl-pod -- curl $WEBPOD1IP
curl pod에서 k8s-w1 node에 배포되어있는 Webpod로 curl 요청을 시도해보겠습니다.
k8s-ctr node에 배포되어있는 curl-pod는 172.20.8.82라는 IP를 갖고 있고, 이를 통해 k8s-w1 node에 배포되어 있는 webpod(172.20.0.143)으로 curl 요청을 보냈습니다.
이때 위에서 k8s-w1 노드에서 실행한 ngrep 명령을 통해 k8s-w1의 eth1 인터페이스를 통해 요청이 수신되고 응답되는 것이 확인할 수 있습니다.