[Cilium] Networking - 노드의 파드들간 통신 상세 part 2
들어가며
이번 포스트에서는 Overlay Network인 VXLAN을 통한 파드간 통신과 Kubernetes 서비스의 외부 노출, LB-IPAM 등을 통한 통신을 살펴보겠습니다.
실습 환경 구성
- 이번 실습에서는 다음 그림과 같이 k8s-w0를 별도의 네트워크에 배치하고 router를 통해 k8s-w0와 k8s-ctr/w1 노드간 통신을 확인합니다.
- 기본 배포 가상 머신 : k8s-ctr, k8s-w1, k8s-w0, router
- router : router : 192.168.10.0/24 ↔ 192.168.20.0/24 대역 라우팅 역할, k8s 에 join 되지 않은 서버, loop1/loop2 dump 인터페이스 배치
- k8s-w0 : k8s-ctr/w1 노드와 다른 네트워크 대역에 배치됩니다.
- 실습 동작에 필요한 static routing이 설저된 상태로 배포 됩니다.
- Cilium CNI v1.18이 설치된 상태로 배포됩니다.
실습환경 배포 파일
-
Vagrantfile : 가상머신 정의, 부팅 시 초기 프로비저닝 설정을 포함하는 Vagrantfile입니다.
# Variables K8SV = '1.33.2-1.1' # Kubernetes Version : apt list -a kubelet , ex) 1.32.5-1.1 CONTAINERDV = '1.7.27-1' # Containerd Version : apt list -a containerd.io , ex) 1.6.33-1 CILIUMV = '1.18.0' # Cilium CNI Version : https://github.com/cilium/cilium/tags N = 1 # max number of worker nodes # Base Image https://portal.cloud.hashicorp.com/vagrant/discover/bento/ubuntu-24.04 BOX_IMAGE = "bento/ubuntu-24.04" BOX_VERSION = "202502.21.0" 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 = 2560 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: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/init_cfg.sh", args: [ K8SV, CONTAINERDV ] subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/k8s-ctr.sh", args: [ N, CILIUMV, K8SV ] subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/route-add1.sh" 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: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/init_cfg.sh", args: [ K8SV, CONTAINERDV] subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/k8s-w.sh" subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/route-add1.sh" end end #-Router Node config.vm.define "router" 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.name = "router" vb.cpus = 1 vb.memory = 768 vb.linked_clone = true end subconfig.vm.host_name = "router" subconfig.vm.network "private_network", ip: "192.168.10.200" subconfig.vm.network "forwarded_port", guest: 22, host: 60009, auto_correct: true, id: "ssh" subconfig.vm.network "private_network", ip: "192.168.20.200", auto_config: false subconfig.vm.synced_folder "./", "/vagrant", disabled: true subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/router.sh" end #-Worker Nodes Subnet2 config.vm.define "k8s-w0" 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-w0" vb.cpus = 2 vb.memory = 1536 vb.linked_clone = true end subconfig.vm.host_name = "k8s-w0" subconfig.vm.network "private_network", ip: "192.168.20.100" subconfig.vm.network "forwarded_port", guest: 22, host: 60010, auto_correct: true, id: "ssh" subconfig.vm.synced_folder "./", "/vagrant", disabled: true subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/init_cfg.sh", args: [ K8SV, CONTAINERDV] subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/k8s-w.sh" subconfig.vm.provision "shell", path: "https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/4w/route-add2.sh" end end
-
init_cfg.sh : args 참고하여 초기 설정을 수행하는 스크립트입니다.
#!/usr/bin/env bash echo ">>>> Initial Config Start <<<<" echo "[TASK 1] Setting Profile & Bashrc" echo 'alias vi=vim' >> /etc/profile echo "sudo su -" >> /home/vagrant/.bashrc ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime # Change Timezone 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" export DEBIAN_FRONTEND=noninteractive apt-get install -y bridge-utils sshpass net-tools conntrack ngrep tcpdump ipset arping wireguard jq yq tree bash-completion unzip kubecolor termshark >/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 : kubeadm init를 통하여 kubernetes controlplane 노드를 설정하고 Cilium CNI 설치, 편리성 설정(k, kc)하는 스크립트입니다. local-path-storageclass와 metrics-server도 설치합니다.
#!/usr/bin/env bash echo ">>>> K8S Controlplane config Start <<<<" echo "[TASK 1] Initial Kubernetes" curl --silent -o /root/kubeadm-init-ctr-config.yaml https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/kubeadm-init-ctr-config.yaml K8SMMV=$(echo $3 | sed -En 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/p') sed -i "s/K8S_VERSION_PLACEHOLDER/v${K8SMMV}/g" /root/kubeadm-init-ctr-config.yaml kubeadm init --config="/root/kubeadm-init-ctr-config.yaml" >/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 7] Install Cilium CNI" NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') helm repo add cilium https://helm.cilium.io/ >/dev/null 2>&1 helm repo update >/dev/null 2>&1 helm install cilium cilium/cilium --version $2 --namespace kube-system \ --set k8sServiceHost=192.168.10.100 --set k8sServicePort=6443 \ --set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16 \ --set routingMode=native --set autoDirectNodeRoutes=true \ --set kubeProxyReplacement=true --set bpf.masquerade=true --set installNoConntrackIptablesRules=true \ --set endpointHealthChecking.enabled=false --set healthChecking=false \ --set hubble.enabled=true --set hubble.relay.enabled=true --set hubble.ui.enabled=true \ --set hubble.ui.service.type=NodePort --set hubble.ui.service.nodePort=30003 \ --set prometheus.enabled=true --set operator.prometheus.enabled=true --set hubble.metrics.enableOpenMetrics=true \ --set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \ --set operator.replicas=1 --set debug.enabled=true >/dev/null 2>&1 echo "[TASK 8] Install Cilium / Hubble 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 HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt) HUBBLE_ARCH=amd64 if [ "$(uname -m)" = "aarch64" ]; then HUBBLE_ARCH=arm64; fi curl -L --fail --remote-name-all https://github.com/cilium/hubble/releases/download/$HUBBLE_VERSION/hubble-linux-${HUBBLE_ARCH}.tar.gz >/dev/null 2>&1 tar xzvfC hubble-linux-${HUBBLE_ARCH}.tar.gz /usr/local/bin rm hubble-linux-${HUBBLE_ARCH}.tar.gz echo "[TASK 9] Remove node taint" kubectl taint nodes k8s-ctr node-role.kubernetes.io/control-plane- echo "[TASK 10] local DNS with hosts file" echo "192.168.10.100 k8s-ctr" >> /etc/hosts echo "192.168.10.200 router" >> /etc/hosts echo "192.168.20.100 k8s-w0" >> /etc/hosts for (( i=1; i<=$1; i++ )); do echo "192.168.10.10$i k8s-w$i" >> /etc/hosts; done echo "[TASK 11] Dynamically provisioning persistent local storage with Kubernetes" kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml >/dev/null 2>&1 kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' >/dev/null 2>&1 echo "[TASK 12] Install Prometheus & Grafana" kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.18.0/examples/kubernetes/addons/prometheus/monitoring-example.yaml >/dev/null 2>&1 kubectl patch svc -n cilium-monitoring prometheus -p '{"spec": {"type": "NodePort", "ports": [{"port": 9090, "targetPort": 9090, "nodePort": 30001}]}}' >/dev/null 2>&1 kubectl patch svc -n cilium-monitoring grafana -p '{"spec": {"type": "NodePort", "ports": [{"port": 3000, "targetPort": 3000, "nodePort": 30002}]}}' >/dev/null 2>&1 # echo "[TASK 12] Install Prometheus Stack" # helm repo add prometheus-community https://prometheus-community.github.io/helm-charts >/dev/null 2>&1 # cat <<EOT > monitor-values.yaml # prometheus: # prometheusSpec: # scrapeInterval: "15s" # evaluationInterval: "15s" # service: # type: NodePort # nodePort: 30001 # grafana: # defaultDashboardsTimezone: Asia/Seoul # adminPassword: prom-operator # service: # type: NodePort # nodePort: 30002 # alertmanager: # enabled: false # defaultRules: # create: false # prometheus-windows-exporter: # prometheus: # monitor: # enabled: false # EOT # helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 75.15.1 \ # -f monitor-values.yaml --create-namespace --namespace monitoring >/dev/null 2>&1 echo "[TASK 13] Install Metrics-server" helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/ >/dev/null 2>&1 helm upgrade --install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system >/dev/null 2>&1 echo "[TASK 14] Install k9s" CLI_ARCH=amd64 if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.deb -O /tmp/k9s_linux_${CLI_ARCH}.deb >/dev/null 2>&1 apt install /tmp/k9s_linux_${CLI_ARCH}.deb >/dev/null 2>&1 echo ">>>> K8S Controlplane Config End <<<<"
-
kubeadm-init-ctr-config.yaml
apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration bootstrapTokens: - token: "123456.1234567890123456" ttl: "0s" usages: - signing - authentication localAPIEndpoint: advertiseAddress: "192.168.10.100" nodeRegistration: kubeletExtraArgs: - name: node-ip value: "192.168.10.100" criSocket: "unix:///run/containerd/containerd.sock" --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: "K8S_VERSION_PLACEHOLDER" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/16"
-
-
k8s-w.sh : kubernetes worker 노드 설정, kubeadm join 등을 수행하는 스크립트입니다.
#!/usr/bin/env bash echo ">>>> K8S Node config Start <<<<" echo "[TASK 1] K8S Controlplane Join" curl --silent -o /root/kubeadm-join-worker-config.yaml https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/2w/kubeadm-join-worker-config.yaml NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') sed -i "s/NODE_IP_PLACEHOLDER/${NODEIP}/g" /root/kubeadm-join-worker-config.yaml kubeadm join --config="/root/kubeadm-join-worker-config.yaml" > /dev/null 2>&1 echo ">>>> K8S Node config End <<<<"
-
kubeadm-join-worker-config.yaml
apiVersion: kubeadm.k8s.io/v1beta4 kind: JoinConfiguration discovery: bootstrapToken: token: "123456.1234567890123456" apiServerEndpoint: "192.168.10.100:6443" unsafeSkipCAVerification: true nodeRegistration: criSocket: "unix:///run/containerd/containerd.sock" kubeletExtraArgs: - name: node-ip value: "NODE_IP_PLACEHOLDER"
-
-
route-add1.sh : k8s node 들이 내부망과 통신을 위한 route 설정 스크립트입니다.
#!/usr/bin/env bash echo ">>>> Route Add Config Start <<<<" chmod 600 /etc/netplan/01-netcfg.yaml chmod 600 /etc/netplan/50-vagrant.yaml cat <<EOT>> /etc/netplan/50-vagrant.yaml routes: - to: 192.168.20.0/24 via: 192.168.10.200 - to: 172.20.0.0/16 via: 192.168.10.200 - to: 10.10.0.0/16 via: 192.168.10.200 EOT netplan apply echo ">>>> Route Add Config End <<<<"
-
route-add2.sh : k8s node 들이 내부망과 통신을 위한 route 설정 스크립트입니다.
#!/usr/bin/env bash echo ">>>> Route Add Config Start <<<<" chmod 600 /etc/netplan/01-netcfg.yaml chmod 600 /etc/netplan/50-vagrant.yaml cat <<EOT>> /etc/netplan/50-vagrant.yaml routes: - to: 192.168.10.0/24 via: 192.168.20.200 - to: 172.20.0.0/16 via: 192.168.20.200 - to: 10.10.0.0/16 via: 192.168.20.200 EOT netplan apply echo ">>>> Route Add Config End <<<<"
-
router.sh : router 역할과 추가적으로 웹서버 역할을 하는 서버의 초기 설정을 담당합니다.
#!/usr/bin/env bash echo ">>>> Initial Config Start <<<<" echo "[TASK 0] Setting eth2" chmod 600 /etc/netplan/01-netcfg.yaml chmod 600 /etc/netplan/50-vagrant.yaml cat << EOT >> /etc/netplan/50-vagrant.yaml eth2: addresses: - 192.168.20.200/24 EOT netplan apply echo "[TASK 1] Setting Profile & Bashrc" 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] Add Kernel setting - IP Forwarding" sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/g' /etc/sysctl.conf sysctl -p >/dev/null 2>&1 echo "[TASK 4] Setting Dummy Interface" modprobe dummy ip link add loop1 type dummy ip link set loop1 up ip addr add 10.10.1.200/24 dev loop1 ip link add loop2 type dummy ip link set loop2 up ip addr add 10.10.2.200/24 dev loop2 echo "[TASK 5] Install Packages" export DEBIAN_FRONTEND=noninteractive apt update -qq >/dev/null 2>&1 apt-get install net-tools jq yq tree ngrep tcpdump arping termshark -y -qq >/dev/null 2>&1 echo "[TASK 6] Install Apache" apt install apache2 -y >/dev/null 2>&1 echo -e "<h1>Web Server : $(hostname)</h1>" > /var/www/html/index.html echo ">>>> Initial Config End <<<<"
실습환경 배포 및 분석 툴 설치
-
실습환경 배포
$ vagrant up # => ... # k8s-w0: >>>> Initial Config End <<<< # ==> k8s-w0: Running provisioner: shell... # k8s-w0: Running: /var/folders/7k/qy6rsdds57z3tmyn9_7hhd8r0000gn/T/vagrant-shell20250807-27868-fg24bd.sh # k8s-w0: >>>> K8S Node config Start <<<< # k8s-w0: [TASK 1] K8S Controlplane Join # k8s-w0: >>>> K8S Node config End <<<< # ==> k8s-w0: Running provisioner: shell... # k8s-w0: Running: /var/folders/7k/qy6rsdds57z3tmyn9_7hhd8r0000gn/T/vagrant-shell20250807-27868-fzndov.sh # k8s-w0: >>>> Route Add Config Start <<<< # k8s-w0: >>>> Route Add Config End <<<<
-
기본정보 확인
# k8s-ctr 노드에 접속
$ vagrant ssh k8s-ctr
---------------------------------
# k9s 설치
$ CLI_ARCH=amd64
$ if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
$ wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.deb -O /tmp/k9s_linux_${CLI_ARCH}.deb
$ apt install /tmp/k9s_linux_${CLI_ARCH}.deb
$ k9s # node/pod 정보 확인 - metrics-server 설치되어 있어서, cpu/mem 확인 가능
#
$ cat /etc/hosts
# => 127.0.2.1 k8s-ctr k8s-ctr
# 192.168.10.100 k8s-ctr
# 192.168.20.100 k8s-w0
# 192.168.10.101 k8s-w1
# 192.168.10.200 router
$ sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w0 hostname
# => k8s-w0
# <span style="color: green;">👉 k8s-w0는 k8s-ctr과 다른 네트워크에 있지만 정적 route 설정이 되어 있어서 접속이 가능합니다.</span>
$ sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w1 hostname
# => k8s-w1
$ sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@router hostname
# => router
# 클러스터 정보 확인
$ kubectl cluster-info
# => Kubernetes control plane is running at https://192.168.10.100:6443
# ...
$ kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
# => "--service-cluster-ip-range=10.96.0.0/16",
# "--cluster-cidr=10.244.0.0/16",
$ kubectl describe cm -n kube-system kubeadm-config
# => ...
# podSubnet: 10.244.0.0/16
# serviceSubnet: 10.96.0.0/16
$ kubectl describe cm -n kube-system kubelet-config
# 노드 정보 : 상태, INTERNAL-IP 확인
$ 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
$ kubectl get node -owide
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# k8s-ctr Ready control-plane 9m41s 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> 4m37s 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> 7m13s v1.33.2 192.168.10.101 <none> Ubuntu 24.04.2 LTS 6.8.0-53-generic containerd://1.7.27
# 파드 정보 : 상태, 파드 IP 확인
$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# => k8s-ctr 10.244.0.0/24
# k8s-w0 10.244.3.0/24
# k8s-w1 10.244.1.0/24
$ kubectl get ciliumnode -o json | grep podCIDRs -A2
# => "podCIDRs": [
# "172.20.0.0/24"
# ],
# --
# "podCIDRs": [
# "172.20.2.0/24"
# ],
# --
# "podCIDRs": [
# "172.20.1.0/24"
# ],
$ kubectl get pod -A -owide
# => NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# cilium-monitoring grafana-5c69859d9-n82rg 1/1 Running 0 10m 172.20.0.209 k8s-ctr <none> <none>
# cilium-monitoring prometheus-6fc896bc5d-m82wl 1/1 Running 0 10m 172.20.0.230 k8s-ctr <none> <none>
# kube-system cilium-c8265 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system cilium-envoy-2x4fb 1/1 Running 0 7m54s 192.168.10.101 k8s-w1 <none> <none>
# kube-system cilium-envoy-fktjl 1/1 Running 0 5m18s 192.168.20.100 k8s-w0 <none> <none>
# kube-system cilium-envoy-gw7cw 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system cilium-h5k5g 1/1 Running 0 5m18s 192.168.20.100 k8s-w0 <none> <none>
# kube-system cilium-operator-76788cffb7-8nhzr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system cilium-tzlkp 1/1 Running 0 7m54s 192.168.10.101 k8s-w1 <none> <none>
# kube-system coredns-674b8bbfcf-bkc4b 1/1 Running 0 10m 172.20.0.42 k8s-ctr <none> <none>
# kube-system coredns-674b8bbfcf-jdxtw 1/1 Running 0 10m 172.20.0.185 k8s-ctr <none> <none>
# kube-system etcd-k8s-ctr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system hubble-relay-5b48c999f9-vh8jq 1/1 Running 0 10m 172.20.0.156 k8s-ctr <none> <none>
# kube-system hubble-ui-655f947f96-vv266 2/2 Running 0 10m 172.20.0.147 k8s-ctr <none> <none>
# kube-system kube-apiserver-k8s-ctr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system kube-controller-manager-k8s-ctr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system kube-proxy-5qw8d 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system kube-proxy-7z4ds 1/1 Running 0 7m54s 192.168.10.101 k8s-w1 <none> <none>
# kube-system kube-proxy-twtfn 1/1 Running 0 5m18s 192.168.20.100 k8s-w0 <none> <none>
# kube-system kube-scheduler-k8s-ctr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
# kube-system metrics-server-5dd7b49d79-25cdj 1/1 Running 0 9m59s 172.20.0.253 k8s-ctr <none> <none>
# local-path-storage local-path-provisioner-74f9666bc9-rxvrq 1/1 Running 0 10m 172.20.0.103 k8s-ctr <none> <none>
$ kubectl get ciliumendpoints -A
# => NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# cilium-monitoring grafana-5c69859d9-n82rg 9513 ready 172.20.0.209
# cilium-monitoring prometheus-6fc896bc5d-m82wl 47077 ready 172.20.0.230
# kube-system coredns-674b8bbfcf-bkc4b 2750 ready 172.20.0.42
# kube-system coredns-674b8bbfcf-jdxtw 2750 ready 172.20.0.185
# kube-system hubble-relay-5b48c999f9-vh8jq 17251 ready 172.20.0.156
# kube-system hubble-ui-655f947f96-vv266 10805 ready 172.20.0.147
# kube-system metrics-server-5dd7b49d79-25cdj 14866 ready 172.20.0.253
# local-path-storage local-path-provisioner-74f9666bc9-rxvrq 11456 ready 172.20.0.103
# ipam 모드 확인
$ cilium config view | grep ^ipam
# => ipam cluster-pool
# iptables 확인
$ iptables-save
$ iptables -t nat -S
$ iptables -t filter -S
$ iptables -t mangle -S
$ iptables -t raw -S
- [k8s-ctr] cilium 설치정보 확인
# cilium 상태 확인
$ kubectl get cm -n kube-system cilium-config -o json | jq
$ cilium status
$ cilium config view
# => ...
# auto-direct-node-routes true
# ...
# routing-mode native
# ...
#
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg status --verbose
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg metrics list
# monitor
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor -v
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor -v -v
## Filter for only the events related to endpoint
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor --related-to=<id>
## Show notifications only for dropped packet events
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor --type drop
## Don’t dissect packet payload, display payload in hex information
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor -v -v --hex
## Layer7
$ kubectl exec -n kube-system -c cilium-agent -it ds/cilium -- cilium-dbg monitor -v --type l7
- 네트워크 정보 확인 :
autoDirectNodeRoutes=true
동작 이해
# router 네트워크 인터페이스 정보 확인
$ sshpass -p 'vagrant' ssh vagrant@router ip -br -c -4 addr
# => lo UNKNOWN 127.0.0.1/8
# eth0 UP 10.0.2.15/24 metric 100
# eth1 UP 192.168.10.200/24
# eth2 UP 192.168.20.200/24
# loop1 UNKNOWN 10.10.1.200/24
# loop2 UNKNOWN 10.10.2.200/24
# k8s node 네트워크 인터페이스 정보 확인
$ ip -c -4 addr show dev eth1
# => 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# altname enp0s9
# inet 192.168.10.100/24 brd 192.168.10.255 scope global eth1
# valid_lft forever preferred_lft forever
$ sshpass -p 'vagrant' ssh vagrant@k8s-w1 ip -c -4 addr show dev eth1
# => 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# altname enp0s9
# inet 192.168.10.101/24 brd 192.168.10.255 scope global eth1
# valid_lft forever preferred_lft forever
$ sshpass -p 'vagrant' ssh vagrant@k8s-w0 ip -c -4 addr show dev eth1
# => 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# altname enp0s9
# inet 192.168.20.100/24 brd 192.168.20.255 scope global eth1
# valid_lft forever preferred_lft forever
# 라우팅 정보 확인
$ 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
$ ip -c 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
# <span style="color: green;">👉 192.168.10.0/24와 192.168.20.0/24 네트워크의 routing 등을 위해서 router에 static route 설정이 되어 있습니다.</span>
## --set routingMode=native --set autoDirectNodeRoutes=true 동작을 정확히 이해해보자!
$ ip -c route
# => 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
# <span style="color: green;">10.10.0.0/16 via 192.168.10.200 dev eth1 proto static</span>
# 172.20.0.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201
# <span style="color: green;">172.20.0.0/16 via 192.168.10.200 dev eth1 proto static</span>
# 172.20.0.201 dev cilium_host proto kernel scope link
# 172.20.1.0/24 via 192.168.10.101 dev eth1 proto kernel
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100
# <span style="color: green;">192.168.20.0/24 via 192.168.10.200 dev eth1 proto static</span>
$ 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
# <span style="color: green;">10.10.0.0/16 via 192.168.10.200 dev eth1 proto static</span>
# 172.20.0.0/24 via 192.168.10.100 dev eth1 proto kernel
# <span style="color: green;">172.20.0.0/16 via 192.168.10.200 dev eth1 proto static</span>
# 172.20.1.0/24 via 172.20.1.197 dev cilium_host proto kernel src 172.20.1.197
# 172.20.1.197 dev cilium_host proto kernel scope link
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101
# <span style="color: green;">192.168.20.0/24 via 192.168.10.200 dev eth1 proto static</span>
$ 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
# <span style="color: green;">10.10.0.0/16 via 192.168.20.200 dev eth1 proto static</span>
# <span style="color: green;">172.20.0.0/16 via 192.168.20.200 dev eth1 proto static</span>
# 172.20.2.0/24 via 172.20.2.25 dev cilium_host proto kernel src 172.20.2.25
# 172.20.2.25 dev cilium_host proto kernel scope link
# <span style="color: green;">192.168.10.0/24 via 192.168.20.200 dev eth1 proto static</span>
# 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 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=0.780 ms
#
# --- 10.10.1.200 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 0.780/0.780/0.780/0.000 ms
$ ping -c 1 192.168.20.100 # k8s-w0 eth1
# => 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=1.82 ms
#
# --- 192.168.20.100 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 1.821/1.821/1.821/0.000 ms
# 목적지까지 경우하는 라우팅 정보 제공
## Path MTU (pmtu): 출발지에서 목적지까지 모든 네트워크 경로 상에서 통과할 수 있는 최대 패킷 크기(Byte), IP fragmentation 없이 전송 가능한 가장 큰 패킷 크기를 의미.
## pmtu 1500: 전체 경로의 최소 MTU는 1500 , hops 2: 총 2단계 라우터/노드를 거쳐서 도달 , back 2: 응답도 동일한 hop 수로 돌아옴
$ tracepath -n 192.168.20.100
# => 1?: [LOCALHOST] pmtu 1500
# 1: 192.168.10.200 2.759ms
# 1: 192.168.10.200 0.994ms
# 2: 192.168.20.100 1.461ms reached
# Resume: pmtu 1500 hops 2 back 2
# <span style="color: green;">👉 k8s-ctr(192.168.10.100)에서 k8s-w0(192.168.20.100)은 다른 네트워크이기 때문에</span>
# <span style="color: green;"> 직접적으로 통신할 수 없고, router(192.168.10.200)를 거쳐서 routing되어 통신이 됩니다.</span>
Native Routing Mode
샘플 애플리케이션 배포 및 확인
- 샘플 애플리케이션 배포 :
cilium-dbg
# 샘플 애플리케이션 배포
$ 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
# => deployment.apps/webpod created
# service/webpod created
# 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
# => pod/curl-pod created
- 확인
# 배포 확인
$ kubectl get deploy,svc,ep webpod -owide
# => NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
# deployment.apps/webpod 3/3 3 3 53s webpod traefik/whoami app=webpod
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# service/webpod ClusterIP 10.96.129.95 <none> 80/TCP 53s app=webpod
#
# NAME ENDPOINTS AGE
# endpoints/webpod 172.20.0.131:80,172.20.1.217:80,172.20.2.73:80 53s
$ kubectl get endpointslices -l app=webpod
# => NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# webpod-njp7j IPv4 80 172.20.0.131,172.20.2.73,172.20.1.217 65s
$ kubectl get ciliumendpoints # IP 확인
# => NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# curl-pod 21500 ready 172.20.0.89
# webpod-697b545f57-b46tz 17081 ready 172.20.0.131 # <span style="color: green;">k8s-wctr</span>
# webpod-697b545f57-66grn 17081 ready 172.20.1.217 # <span style="color: green;">k8s-w1</span>
# webpod-697b545f57-24ck5 17081 ready 172.20.2.73 # <span style="color: green;">k8s-w0</span>
#
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg ip list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg endpoint list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg service list
# => ...
# 20 10.96.129.95:80/TCP ClusterIP 1 => 172.20.0.131:80/TCP (active)
# 2 => 172.20.1.217:80/TCP (active)
# 3 => 172.20.2.73:80/TCP (active)
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf lb list
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf lb list | grep 10.96.129.95
# => 10.96.129.95:80/TCP (3) 172.20.2.73:80/TCP (20) (3)
# 10.96.129.95:80/TCP (0) 0.0.0.0:0 (20) (0) [ClusterIP, non-routable]
# 10.96.129.95:80/TCP (1) 172.20.0.131:80/TCP (20) (1)
# 10.96.129.95:80/TCP (2) 172.20.1.217:80/TCP (20) (2)
# <span style="color: green;">👉 cilium의 load balancer가 각 파드의 IP을 확인하고, 해당 IP로 요청을 전달합니다.</span>
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf nat list
# map
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map list | grep -v '0 0'
# => Name Num entries Num errors Cache enabled
# cilium_policy_v2_03166 3 0 true
# cilium_policy_v2_01270 3 0 true
# cilium_policy_v2_01641 3 0 true
# cilium_policy_v2_01688 3 0 true
# cilium_lb4_reverse_nat 20 0 true
# cilium_lxc 13 0 true
# cilium_policy_v2_02965 3 0 true
# cilium_runtime_config 256 0 true
# cilium_ipcache_v2 22 0 true
# cilium_policy_v2_00459 2 0 true
# cilium_policy_v2_00222 3 0 true
# cilium_policy_v2_03535 3 0 true
# cilium_policy_v2_02230 3 0 true
# cilium_lb4_services_v2 45 0 true
# cilium_lb4_backends_v3 16 0 true
# cilium_lb4_reverse_sk 9 0 true
# cilium_policy_v2_00745 3 0 true
# cilium_policy_v2_01678 3 0 true
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_services_v2
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_services_v2 | grep 10.96.129.95
# => Key Value State Error
# 10.96.129.95:80/TCP (3) 15 0[0] (20) [0x0 0x0]
# 10.96.129.95:80/TCP (2) 16 0[0] (20) [0x0 0x0]
# 10.96.129.95:80/TCP (0) 0 3[0] (20) [0x0 0x0]
# 10.96.129.95:80/TCP (1) 14 0[0] (20) [0x0 0x0]
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_backends_v3
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_lb4_reverse_nat
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg map get cilium_ipcache_v2
통신 확인 및 hubble로 모니터링
# 통신 확인 : 문제 확인
$ kubectl exec -it curl-pod -- curl webpod | grep Hostname
# => Hostname: webpod-697b545f57-66grn
$ 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-b46tz
# ---
# Hostname: webpod-697b545f57-66grn
# --- # <span style="color: green;">👉 k8s-w0 노드에 배포된 webpod 파드에 대해서는 통신이 되지 않습니다.</span>
# ---
# Hostname: webpod-697b545f57-b46tz
# ...
# <span style="color: green;">👉 k8s-ctr 노드와 k8s-w1 노드에 배포된 webpod 파드에 대해서는 통신이 되지만,</span>
# <span style="color: green;"> k8s-w0 노드에 배포된 webpod 파드에 대해서는 통신이 되지 않습니다.</span>
# <span style="color: green;"> 왜냐하면, 노드간의 통신은 router에서 정적 라우팅을 통해 통신이 가능하지만,</span>
# <span style="color: green;"> 파드 간의 통신에 대해서는 routing 처리가 되어있지 않기 때문입니다.</span>
# <span style="color: green;"> 이번 포스트에서는 Encapsulation Mode를 통해 이 문제를 해결해보고</span>
# <span style="color: green;"> 다음 포스트에서 Native Routing Mode를 통해 이 문제를 해결해보겠습니다.</span>
# 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
# => 172.20.2.73
# 신규 터미널 [router]
$ tcpdump -i any icmp -nn
#
$ kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
# 신규 터미널 [router] : 라우팅이 어떻게 되는가?
$ tcpdump -i any icmp -nn
# => 14:29:00.156370 eth1 In IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 35, seq 1, length 64
# 14:29:00.156388 eth0 Out IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 35, seq 1, length 64
# <span style="color: green;">👉 k8s-ctr 노드의 파드(172.20.0.89)에서 k8s-w0 노드의 파드(172.20.2.73)로 ping을 보내면,</span>
# <span style="color: green;"> 서로간의 라우팅이 되어있지 않기 때문에 ping이 실패합니다.</span>
# 신규 터미널 [router]
$ ip -c route
$ ip route get 172.20.2.36 # <span style="color: green;">👉 172.20.2.36으로 통신할때 사용되는 라우팅 정보 확인하는 명령어</span>
# => 172.20.2.36 via 10.0.2.2 dev eth0 src 10.0.2.15 uid 0
# [k8s-ctr]에서 반복 호출
$ kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# 신규 터미널 [router]
$ tcpdump -i any tcp port 80 -nn
# <span style="color: green;">👉 k8s-w0노드의 파드로 통신을 시도하여 통신이 안 될 때 마다 router의 tcpdump에 통신이 안 되는 로그가 찍힙니다.</span>
# hubble 확인
# hubble ui 웹 접속 주소 확인 : default 네임스페이스 확인
$ NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
$ echo -e "http://$NODEIP:30003"
# => http://192.168.10.100:30003
# hubble relay 포트 포워딩 실행
$ cilium hubble port-forward&
# => ℹ️ Hubble Relay is available at 127.0.0.1:4245
$ hubble status
# => Healthcheck (via localhost:4245): Ok
# flow log 모니터링
$ hubble observe -f --protocol tcp --pod curl-pod
# => ...
# Aug 9 05:38:54.615: default/curl-pod:46490 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-network FORWARDED (TCP Flags: ACK, FIN)
# Aug 9 05:38:54.618: default/curl-pod:46490 (ID:21500) <- default/webpod-697b545f57-66grn:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
# Aug 9 05:38:54.618: default/curl-pod:46490 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-network FORWARDED (TCP Flags: ACK)
# Aug 9 05:38:55.627: default/curl-pod (ID:21500) <> 10.96.129.95:80 (world) pre-xlate-fwd TRACED (TCP)
# Aug 9 05:38:55.627: default/curl-pod (ID:21500) <> <span style="color: green;">default/webpod-697b545f57-24ck5:80</span> (ID:17081) post-xlate-fwd TRANSLATED (TCP)
# Aug 9 05:38:55.628: default/curl-pod:37198 (ID:21500) -> <span style="color: green;">default/webpod-697b545f57-24ck5:80</span> (ID:17081) to-network FORWARDED <span style="color: green;">(TCP Flags: SYN)</span>
# <span style="color: green;">👉 k8s-w0 노드에 배포된 webpod 파드(default/webpod-697b545f57-24ck5)로 통신을 시도할 때,</span>
# <span style="color: green;"> 라우팅이 되지 않아 SYN 패킷에 대한 ACK 패킷이 오지 않기 때문에, 통신이 되지 않습니다.</span>
# Aug 9 05:38:57.528: default/curl-pod:46498 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: SYN)
# Aug 9 05:38:57.528: default/curl-pod:46498 (ID:21500) <- default/webpod-697b545f57-66grn:80 (ID:17081) to-network FORWARDED (TCP Flags: SYN, ACK)
# Aug 9 05:38:57.528: default/curl-pod:46498 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: ACK)
# Aug 9 05:38:57.529: default/curl-pod:46498 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: ACK, PSH)
Overlay Network (Encapsulated) Mode
- 앞에서 살펴본 것과 같이 k8s-ctr 노드와 k8s-w0 노드가 다른 네트워크에 있는 경우, 라우팅 장비에서 라우팅 룰을 추가해서 노드간에는 통신이 가능하지만, 파드 간의 통신은 라우팅 룰이 없기 때문에 통신이 되지 않습니다.
- 이러한 경우에도 파드 간의 통신을 가능하게 하기 위해서 Cilium에서는 Overlay Network(Encapsulated) Mode를 제공합니다.
- VXLAN과 GENEVE 등을 지원하는데, 이번 포스트에서는 VXLAN을 통해 통신이 되도록 해보겠습니다.
VXLAN 설정
# [커널 구성 옵션] 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'
# => vxlan 147456 0
# ip6_udp_tunnel 16384 1 vxlan
# udp_tunnel 36864 1 vxlan
# <span style="color: green;">👉 modprobe 명령어로 커널 모듈이 로딩 되어 vxlan과 필요 모듈들이 활성화 되었습니다.</span>
$ for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo modprobe vxlan ; echo; done
# => >> node : k8s-w1 <<
# >> node : k8s-w0 <<
$ 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
# => >> node : k8s-w1 <<
# vxlan 147456 0
# ip6_udp_tunnel 16384 1 vxlan
# udp_tunnel 36864 1 vxlan
# >> node : k8s-w0 <<
# vxlan 147456 0
# ip6_udp_tunnel 16384 1 vxlan
# udp_tunnel 36864 1 vxlan
# 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
# => 172.20.1.217
# 반복 ping 실행해두기
$ kubectl exec -it curl-pod -- ping $WEBPOD1
# 업그레이드
$ 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
# => Release "cilium" has been upgraded. Happy Helming!
# ...
# 설정을 적용하기 위해서 Cilium DaemonSet을 재시작합니다.
$ kubectl rollout restart -n kube-system ds/cilium
# 설정 확인
$ cilium features status
$ cilium features status | grep datapath_network
# => Cilium Agents
# Uniform Name Labels k8s-ctr k8s-w0 k8s-w1
# Yes cilium_feature_datapath_network mode=<span style="color: green;">overlay-vxlan</span> 1 1 1
$ kubectl exec -it -n kube-system ds/cilium -- cilium status | grep ^Routing
# => Routing: Network: <span style="color: green;">Tunnel [vxlan]</span> Host: BPF
$ cilium config view | grep tunnel
# => routing-mode <span style="color: green;">tunnel</span>
# tunnel-protocol <span style="color: green;">vxlan</span>
# tunnel-source-port-range 0-0
# cilium_vxlan 확인
$ 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 42:59:64:e0:f6:38 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::4059:64ff:fee0:f638/64 scope link
# valid_lft forever preferred_lft forever
$ 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
# => >> node : k8s-w1 <<
# 8: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether 02:c4:af:d7:83:fc brd ff:ff:ff:ff:ff:ff
# inet6 fe80::c4:afff:fed7:83fc/64 scope link
# valid_lft forever preferred_lft forever
#
# >> node : k8s-w0 <<
# 8: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether ee:f6:72:2b:b9:b9 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::ecf6:72ff:fe2b:b9b9/64 scope link
# valid_lft forever preferred_lft forever
# 라우팅 정보 확인 : k8s node 간 다른 네트워크 대역에 있더라도, 파드의 네트워크 대역 정보가 라우팅에 올라왔다!
$ ip -c route | grep cilium_host
# => 172.20.0.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201
# 172.20.0.201 dev cilium_host proto kernel scope link
# 172.20.1.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201 mtu 1450
# 172.20.2.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201 mtu 1450
$ ip route get 172.20.1.10
# => 172.20.1.10 dev cilium_host src 172.20.0.201 uid 0
# cache mtu 1450
$ ip route get 172.20.2.10
# => 172.20.2.10 dev cilium_host src 172.20.0.201 uid 0
# cache mtu 1450
# <span style="color: green;">👉 MTU는 Maximum Transmission Unit의 약자로, 네트워크에서 전송할 수 있는 최대 패킷 크기를 의미합니다.</span>
# <span style="color: green;"> 본래의 MTU는 1500이지만, VXLAN이 이미 50 bytes를 사용하기 때문에</span>
# <span style="color: green;"> 최대 1450 bytes까지 전송할 수 있는것을 확인할 수 있습니다.</span>
$ for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i ip -c route | grep cilium_host ; echo; done
# => >> node : k8s-w1 <<
# 172.20.0.0/24 via 172.20.1.197 dev cilium_host proto kernel src 172.20.1.197 mtu 1450
# 172.20.1.0/24 via 172.20.1.197 dev cilium_host proto kernel src 172.20.1.197
# 172.20.1.197 dev cilium_host proto kernel scope link
# 172.20.2.0/24 via 172.20.1.197 dev cilium_host proto kernel src 172.20.1.197 mtu 1450
#
# >> node : k8s-w0 <<
# 172.20.0.0/24 via 172.20.2.25 dev cilium_host proto kernel src 172.20.2.25 mtu 1450
# 172.20.1.0/24 via 172.20.2.25 dev cilium_host proto kernel src 172.20.2.25 mtu 1450
# 172.20.2.0/24 via 172.20.2.25 dev cilium_host proto kernel src 172.20.2.25
# 172.20.2.25 dev cilium_host proto kernel scope link
# 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
# => cilium-4jf9p cilium-dqgbw cilium-sqx45
# router 역할 IP 확인
$ kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
# => 172.20.0.201 (router)
$ kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
# => 172.20.1.197 (router)
$ kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium status --all-addresses | grep router
# => 172.20.2.25 (router)
#
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf ipcache list
# => IP PREFIX/ADDRESS IDENTITY
# ...
# 172.20.0.201/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
# 172.20.2.25/32 identity=6 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
# 172.20.1.197/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
$ kubectl exec -n kube-system $CILIUMPOD0 -- cilium-dbg bpf ipcache list
# => IP PREFIX/ADDRESS IDENTITY
# ...
# 172.20.0.201/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
# 172.20.2.25/32 identity=6 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
# 172.20.1.197/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.101 flags=hastunnel
$ kubectl exec -n kube-system $CILIUMPOD1 -- cilium-dbg bpf ipcache list
# => IP PREFIX/ADDRESS IDENTITY
# ...
# 172.20.1.197/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
# 172.20.0.201/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
# 172.20.2.25/32 identity=6 encryptkey=0 tunnelendpoint=192.168.20.100 flags=hastunnel
$ kubectl exec -n kube-system $CILIUMPOD2 -- cilium-dbg bpf ipcache list
# => IP PREFIX/ADDRESS IDENTITY
# ...
# 172.20.0.201/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.100 flags=hastunnel
# 172.20.2.25/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 flags=<none>
# 172.20.1.197/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.101 flags=hastunnel
#
$ kubectl exec -n kube-system ds/cilium -- cilium-dbg bpf socknat list
$ kubectl exec -n kube-system $CILIUMPOD0 -- cilium-dbg bpf socknat list
$ kubectl exec -n kube-system $CILIUMPOD1 -- cilium-dbg bpf socknat list
$ kubectl exec -n kube-system $CILIUMPOD2 -- cilium-dbg bpf socknat list
파드간 통신 확인
# 통신 확인
$ kubectl exec -it curl-pod -- curl webpod | grep Hostname
# => Hostname: webpod-697b545f57-b46tz
$ 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-b46tz
# ---
# Hostname: webpod-697b545f57-24ck5 # <span style="color: green;">👉 k8s-w0 노드에 배포된 webpod 파드에 대해서도 통신이 됩니다.</span>
# ---
# Hostname: webpod-697b545f57-66grn
# ...
# <span style="color: green;">👉 VXLAN 설정 후 이전에는 통신이 되지 않던 k8s-w0 노드에 배포된 webpod 파드에 대해서도 통신이 됩니다.</span>
# 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
# => 172.20.2.73
# 신규 터미널 [router]
$ tcpdump -i any udp port 8472 -nn
# [k8s-ctr] 에서 ping 실행
$ kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
# => PING 172.20.2.73 (172.20.2.73) 56(84) bytes of data.
# 64 bytes from 172.20.2.73: icmp_seq=1 ttl=63 time=1.77 ms
#
# --- 172.20.2.73 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 1.769/1.769/1.769/0.000 ms
# command terminated with exit code 1
# [router]에서 tcpdump 확인 (VXLAN 포트 8472/udp로 통신이 되는지 확인)
$ tcpdump -i any udp port 8472 -nn
# => 16:17:25.514596 eth1 In IP 192.168.10.100.56046 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 21500
# IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 11684, seq 1, length 64
# 16:17:25.514636 eth2 Out IP 192.168.10.100.56046 > 192.168.20.100.8472: OTV, flags [I] (0x08), overlay 0, instance 21500
# IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 11684, seq 1, length 64
# 16:17:25.515307 eth2 In IP 192.168.20.100.59302 > 192.168.10.100.8472: OTV, flags [I] (0x08), overlay 0, instance 17081
# IP 172.20.2.73 > 172.20.0.89: ICMP echo reply, id 11684, seq 1, length 64
# 16:17:25.515315 eth1 Out IP <span style="color: green;">192.168.20.100.59302 > 192.168.10.100.8472</span>: OTV, flags [I] (0x08), overlay 0, instance 17081
# IP <span style="color: green;">172.20.2.73 > 172.20.0.89</span>: ICMP echo reply, id 11684, seq 1, length 64
# <span style="color: green;">👉 VXLAN을 통해 파드 IP간 통신이 노드간 IP로 캡슐화되어 통신이 되는 것을 확인할 수 있습니다.</span>
# 신규 터미널 [router] : 라우팅이 어떻게 되는가?
$ tcpdump -i any icmp -nn
# => (없음)
# <span style="color: green;">👉 VXLAN을 통해 캡슐화된 패킷으로 통신이 되기 때문에 직접적으로 ICMP(ping) 패킷은 보이지 않습니다.</span>
# 반복 접속
$ kubectl exec -it curl-pod -- curl webpod | grep Hostname
# => Hostname: webpod-697b545f57-b46tz
$ kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# 신규 터미널 [router]
$ tcpdump -i any udp port 8472 -w /tmp/vxlan.pcap
$ tshark -r /tmp/vxlan.pcap -d udp.port==8472,vxlan
# => 1 0.000000 172.20.0.89 → 172.20.2.73 TCP 130 53224 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2585589520 TSecr=0 WS=128
# 2 0.000015 172.20.0.89 → 172.20.2.73 TCP 130 [TCP Retransmission] 53224 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2585589520 TSecr=0 WS=128
# 3 0.000633 172.20.2.73 → 172.20.0.89 TCP 130 80 → 53224 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3020660677 TSecr=2585589520 WS=128
# 4 0.000637 172.20.2.73 → 172.20.0.89 TCP 130 [TCP Retransmission] 80 → 53224 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3020660677 TSecr=2585589520 WS=128
# 5 0.001244 172.20.0.89 → 172.20.2.73 TCP 122 53224 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2585589521 TSecr=3020660677
# 6 0.001245 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 5#1] 53224 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2585589521 TSecr=3020660677
# 7 0.002239 172.20.0.89 → 172.20.2.73 HTTP 192 GET / HTTP/1.1
# 8 0.002247 172.20.0.89 → 172.20.2.73 TCP 192 [TCP Retransmission] 53224 → 80 [PSH, ACK] Seq=1 Ack=1 Win=64896 Len=70 TSval=2585589522 TSecr=3020660677
# 9 0.002625 172.20.2.73 → 172.20.0.89 TCP 122 80 → 53224 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3020660679 TSecr=2585589522
# 10 0.002628 172.20.2.73 → 172.20.0.89 TCP 122 [TCP Dup ACK 9#1] 80 → 53224 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3020660679 TSecr=2585589522
# 11 0.004215 172.20.2.73 → 172.20.0.89 HTTP 441 HTTP/1.1 200 OK (text/plain)
# 12 0.004221 172.20.2.73 → 172.20.0.89 TCP 441 [TCP Retransmission] 80 → 53224 [PSH, ACK] Seq=1 Ack=71 Win=64256 Len=319 TSval=3020660680 TSecr=2585589522
# 13 0.004783 172.20.0.89 → 172.20.2.73 TCP 122 53224 → 80 [ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2585589525 TSecr=3020660680
# 14 0.004785 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 13#1] 53224 → 80 [ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2585589525 TSecr=3020660680
# 15 0.005245 172.20.0.89 → 172.20.2.73 TCP 122 53224 → 80 [FIN, ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2585589525 TSecr=3020660680
# 16 0.005248 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Retransmission] 53224 → 80 [FIN, ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2585589525 TSecr=3020660680
# 17 0.005631 172.20.2.73 → 172.20.0.89 TCP 122 80 → 53224 [FIN, ACK] Seq=320 Ack=72 Win=64256 Len=0 TSval=3020660682 TSecr=2585589525
# 18 0.005633 172.20.2.73 → 172.20.0.89 TCP 122 [TCP Retransmission] 80 → 53224 [FIN, ACK] Seq=320 Ack=72 Win=64256 Len=0 TSval=3020660682 TSecr=2585589525
# 19 0.006526 172.20.0.89 → 172.20.2.73 TCP 122 53224 → 80 [ACK] Seq=72 Ack=321 Win=64640 Len=0 TSval=2585589526 TSecr=3020660682
# 20 0.006529 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 19#1] 53224 → 80 [ACK] Seq=72 Ack=321 Win=64640 Len=0 TSval=2585589526 TSecr=3020660682
# ...
# <span style="color: green;">👉 tshark로도 VXLAN 캡슐화된 패킷을 해석할 수 있지만 조금 더 원활한 해석을 위해서 termshark를 사용해보겠습니다.</span>
$ termshark -r /tmp/vxlan.pcap
# => termshark 2.4.0 | vxlan.pcap Analysis Misc
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃Filter: <Apply> <Recent> ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# No. - Time - Source - Dest - Proto - Length - Info - ▲
# 2 0.000015 192.168.10.100 192.168.20.100 UDP 130 56560 → 8472 Len=82
# 3 0.000633 192.168.20.100 192.168.10.100 UDP 130 38192 → 8472 Len=82 █
# 4 0.000637 192.168.20.100 192.168.10.100 UDP 130 38192 → 8472 Len=82
# 5 0.001244 192.168.10.100 192.168.20.100 UDP 122 56560 → 8472 Len=74
# 6 0.001245 192.168.10.100 192.168.20.100 UDP 122 56560 → 8472 Len=74
# 7 0.002239 192.168.10.100 192.168.20.100 UDP 192 56560 → 8472 Len=144 ▼
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# [+] Frame 2: 130 bytes on wire (1040 bits), 130 bytes captured (1040 bits) [=]
# [+] Linux cooked capture v2
# [+] Internet Protocol Version 4, Src: 192.168.10.100, Dst: 192.168.20.100
# [+] User Datagram Protocol, Src Port: 56560, Dst Port: 8472
# ...
# <span style="color: green;">👉 기본 termshark 명령어로는 VXLAN 캡슐화된 패킷을 해석할 수 없어서 8472/udp로 캡슐화된 패킷이 보이지 않습니다.</span>
$ termshark -r /tmp/vxlan.pcap -d udp.port==8472,vxlan
# => termshark 2.4.0 | vxlan.pcap Analysis Misc
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃Filter: <Apply> <Recent> ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# No. Time - Source - Dest - Prot Lengt Info - ▲
# 1 0.0000 172.20.0 172.20.2 TCP 130 53224 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSva
# 2 0.0000 172.20.0 172.20.2 TCP 130 [TCP Retransmission] 53224 → 80 [SYN] Seq=0 Win=64860 Len=0 MS █
# 3 0.0006 172.20.2 172.20.0 TCP 130 80 → 53224 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SAC
# 4 0.0006 172.20.2 172.20.0 TCP 130 [TCP Retransmission] 80 → 53224 [SYN, ACK] Seq=0 Ack=1 Win=643
# 5 0.0012 172.20.0 172.20.2 TCP 122 53224 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2585589521
# 6 0.0012 172.20.0 172.20.2 TCP 122 [TCP Dup ACK 5#1] 53224 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0
# 7 0.0022 172.20.0 172.20.2 HTTP 192 GET / HTTP/1.1
# 8 0.0022 172.20.0 172.20.2 TCP 192 [TCP Retransmission] 53224 → 80 [PSH, ACK] Seq=1 Ack=1 Win=648
# 9 0.0026 172.20.2 172.20.0 TCP 122 80 → 53224 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3020660679
# 10 0.0026 172.20.2 172.20.0 TCP 122 [TCP Dup ACK 9#1] 80 → 53224 [ACK] Seq=1 Ack=71 Win=64256 Len=
# 11 0.0042 172.20.2 172.20.0 HTTP 441 HTTP/1.1 200 OK (text/plain) ▼
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# [+] Frame 7: 192 bytes on wire (1536 bits), 192 bytes captured (1536 bits)
# [+] Linux cooked capture v2 [=]
# [+] Internet Protocol Version 4, <span style="color: green; border: 2px solid rgba(255, 0, 0, 0.5); padding: 1px 4px;">Src: 192.168.10.100, Dst: 192.168.20.100</span>
# [+] User Datagram Protocol, Src Port: 56560, Dst Port: 8472
# [+] <span style="color: green; border: 2px solid rgba(255, 0, 0, 0.5); padding: 1px 4px;">Virtual eXtensible Local Area Network</span>
# [+] Ethernet II, Src: 46:02:28:c8:e7:b6 (46:02:28:c8:e7:b6), Dst: ca:7a:5c:b2:2c:4a (ca:7a:5c:b2:2c:4a)
# [+] Internet Protocol Version 4, Src: <span style="color: green; border: 2px solid rgba(255, 0, 0, 0.5); padding: 1px 4px;">172.20.0.89, Dst: 172.20.2.73</span>
# [+] Transmission Control Protocol, Src Port: 53224, Dst Port: 80, Seq: 1, Ack: 1, Len: 70
# [-] Hypertext Transfer Protocol
# [+] GET / HTTP/1.1
# Host: webpod
# User-Agent: curl/8.14.1
# <span style="color: green;">👉 -d udp.port==8472,vxlan 옵션을 통해 VXLAN 캡슐화된 패킷을 해석할 수 있습니다.</span>
# 신규 터미널 [k8s-ctr] hubble flow log 모니터링 : overlay 통신 모드 확인!
$ hubble observe -f --protocol tcp --pod curl-pod
# => ...
# Aug 9 07:34:42.262: default/curl-pod:37272 (ID:21500) -> default/webpod-697b545f57-24ck5:80 (ID:17081) <span style="color: green;">to-overlay FORWARDED</span> (TCP Flags: SYN)
# Aug 9 07:34:42.263: default/curl-pod:37272 (ID:21500) <- default/webpod-697b545f57-24ck5:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: SYN, ACK)
# Aug 9 07:34:42.263: default/curl-pod:37272 (ID:21500) -> default/webpod-697b545f57-24ck5:80 (ID:17081) <span style="color: green;">to-overlay FORWARDED</span> (TCP Flags: ACK)
# ...
파드에서 나갈때 패킷 흐름
파드로 인입시 패킷 흐름
- 이상과 같이 VXLAN을 통해서 파드간 통신이 가능해졌습니다. 하지만 VXLAN 등의 캡슐화 기술을 사용하게 되면, 다음의 단점이 있습니다.
- 50 bytes의 캡슐화 오버헤드가 발생합니다. (MTU 감소 및 그로인한 패킷 fragmentation 발생 가능성)
- 캡슐화 및 디캡슐화 과정에서 CPU 리소스가 소모됩니다.
- 같은 네트워크 대역에 있더라도 VXLAN을 통해 캡슐화 됩니다.
- VXLAN은 별도의 네트워크 설정 없이 노드간 통신이 가능하다는 장점이 있지만, 위와 같은 문제들로 인프라적인 지원이 가능하다면 Native Routing을 사용하는 것이 좋습니다.
- MTU(Maximum Transmission Unit)는 네트워크에서 전송할 수 있는 최대 패킷 크기를 의미합니다. VXLAN을 사용하면 MTU가 감소하게 되며, 이는 패킷이 분할(fragmentation)되어 전송될 수 있음을 의미합니다. 따라서, VXLAN을 사용할 때는 MTU 설정을 주의 깊게 관리해야 합니다.
- MTU에 대한 간단한 실험을 하고 다음 단계로 넘어가겠습니다.
# -M do : Don't Fragment (DF) 플래그를 설정하여 조각화 방지
# -s 1472 : 페이로드(payload) 크기, 즉 ICMP 데이터 크기
$ kubectl exec -it curl-pod -- ping -M do -s 1472 $WEBPOD
# => PING 172.20.2.73 (172.20.2.73) 1472(1500) bytes of data.
# ping: sendmsg: Message too large
# <span style="color: green;">👉 ping 명령어를 통해 VXLAN 에 의한 MTU (1450 bytes)보다 큰 1500 bytes를 강제로 전송하면 </span>
# <span style="color: green;"> "ping: sendmsg: Message too large" 에러가 발생합니다.</span>
# <span style="color: green;"> 즉, MTU보다 큰 패킷은 전송할 수 없음을 확인할 수 있으며, MTU 단위로 패킷이 분할(fragmentation)되어 전송됩니다.</span>
도전과제2.
VXLAN 대신 GENEVE 모드 사용
- Cilium은 VXLAN 외에도 GENEVE 모드를 지원합니다. GENEVE는 VXLAN보다 더 유연하고 확장 가능한 캡슐화 프로토콜입니다.
GENEVE 소개
- GENEVE(Generic Network Virtualization Encapsulation)는 VXLAN/NVGRE 등과 유사한 네트워크 가상화 캡슐화 프로토콜로 좀 더 발전된 기능을 제공합니다.
- 주요 특징은 다음과 같습니다.
Geneve 헤더 구조 - 출처
- 확장성 높은 옵션 필드(TLV:Type-Length-Value) : 고정 헤더 구조에서 벗어나 유동적으로 다양한 옵션을 추가할 수 있어 다양한 환경과 요구사항에 맞는 발전이 가능합니다. (단, 늘어난 헤더 크기가 오버헤드로 작용합니다.)
- UDP 기반 처리 : VXLAN과 마찬가지로 UDP 6081 포트를 사용하며, 하드웨어·소프트웨어에서 호환성이 뛰어납니다.
- 표준화된 프로토콜 : Cisco 주도로 만들어진 VXLAN과 달리, 처음부터 IETF에서 범람하는 캡슐화 프로토콜을 통합할 목적으로 만든 프로토콜로, 더 널리 채택될 가능성이 높습니다.
- 확장 가능한 서비스 : 정책, 전송 보안, 서비스 체이닝 등 다양한 서비스에 대한 확장성을 제공하며, SDN(Software Defined Networking) 환경에서 유용합니다.
- 하드웨어 오프로드 : VXLAN도 NIC 오프로드를 지원하지만, Geneve는 설계시 부터 하드웨어 오프로드를 고려하여 설계되어, 성능이 향상될 수 있습니다.
GENEVE 실습
- 실습을 통해 GENEVE 모드를 사용하여 파드 간 통신을 설정해보겠습니다.
GENEVE 모드 설정
# GENEVE 커널 모듈 로드
$ lsmod | grep -E 'vxlan|geneve'
# => vxlan 147456 0
# ip6_udp_tunnel 16384 1 vxlan
# udp_tunnel 36864 1 vxlan
$ modprobe geneve
$ lsmod | grep -E 'vxlan|geneve'
# => <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve 45056 0</span>
# vxlan 147456 0
# ...
$ for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i sudo modprobe geneve ; echo; done
# => >> node : k8s-w1 <<
# >> node : k8s-w0 <<
$ 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
# => >> node : k8s-w1 <<
# <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve 45056 0</span>
# vxlan 147456 0
# ip6_udp_tunnel 16384 2 <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve</span>,vxlan
# udp_tunnel 36864 2 <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve</span>,vxlan
# >> node : k8s-w0 <<
# <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve 45056 0</span>
# vxlan 147456 0
# ip6_udp_tunnel 16384 2 <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve</span>,vxlan
# udp_tunnel 36864 2 <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">geneve</span>,vxlan
# 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
# => 172.20.1.198
# 반복 ping 실행해두기
$ kubectl exec -it curl-pod -- ping $WEBPOD1
# 업그레이드
$ helm upgrade cilium cilium/cilium --namespace kube-system --version 1.18.0 --reuse-values \
--set routingMode=tunnel --set tunnelProtocol=geneve \
--set autoDirectNodeRoutes=false --set installNoConntrackIptablesRules=false
# => Release "cilium" has been upgraded. Happy Helming!
# ...
# 설정을 적용하기 위해서 Cilium DaemonSet을 재시작합니다.
$ kubectl rollout restart -n kube-system ds/cilium
# => daemonset.apps/cilium restarted
# 설정 확인
$ cilium features status
$ cilium features status | grep datapath_network
# => Cilium Agents
# Uniform Name Labels k8s-ctr k8s-w0 k8s-w1
# Yes cilium_feature_datapath_network mode=<span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">overlay-geneve</span> 1 1 1
$ kubectl exec -it -n kube-system ds/cilium -- cilium status | grep ^Routing
# => Routing: Network: <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">Tunnel [geneve]</span> Host: BPF
$ cilium config view | grep tunnel
# => routing-mode tunnel
# tunnel-protocol geneve
# tunnel-source-port-range 0-0
# cilium_geneve 확인
$ ip -c addr show dev cilium_geneve
# => 29: cilium_geneve: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether 9e:6f:d6:29:71:60 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::9c6f:d6ff:fe29:7160/64 scope link
# valid_lft forever preferred_lft forever
$ for i in w1 w0 ; do echo ">> node : k8s-$i <<"; sshpass -p 'vagrant' ssh vagrant@k8s-$i ip -c addr show dev cilium_geneve ; echo; done
# => >> node : k8s-w1 <<
# 11: cilium_geneve: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether c2:bd:d4:eb:63:fd brd ff:ff:ff:ff:ff:ff
# inet6 fe80::c0bd:d4ff:feeb:63fd/64 scope link
# valid_lft forever preferred_lft forever
#
# >> node : k8s-w0 <<
# 11: cilium_geneve: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
# link/ether 0e:fd:73:c8:6f:37 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::cfd:73ff:fec8:6f37/64 scope link
# valid_lft forever preferred_lft forever
# 라우팅 정보 확인 : k8s node 간 다른 네트워크 대역에 있더라도, 파드의 네트워크 대역 정보가 라우팅에 올라왔다!
$ ip -c route | grep cilium_host
# => 172.20.0.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201
# 172.20.0.201 dev cilium_host proto kernel scope link
# 172.20.1.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201 mtu 1450
# 172.20.2.0/24 via 172.20.0.201 dev cilium_host proto kernel src 172.20.0.201 mtu 1450
$ ip route get 172.20.1.10
# => 172.20.1.10 dev cilium_host src 172.20.0.201 uid 0
# cache mtu 1450
$ ip route get 172.20.2.10
# => 172.20.2.10 dev cilium_host src 172.20.0.201 uid 0
# cache mtu 1450
# <span style="color: green;">👉 geneve도 VXLAN과 마찬가지로 50 bytes의 캡슐화 오버헤드가 발생하는것 같습니다.</span>
- 커널 모듈을 로드하고, helm을 통해 GENEVE 모드 설정을 바꾸고, daemonset을 재시작하는것 만으로 간단하게 VXLAN 모드에서 GENEVE 모드로 변경할 수 있었습니다.
- VXLAN 모드로 변경할때와 마찬가지로 daemonset이 재시작 되는 동안 파드간 통신이 중단되는것을 확인할 수 있었습니다. 운영 환경에서는 주의가 필요합니다.
파드간 통신 확인
# 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
# => 172.20.2.73
# 신규 터미널 [router]
$ tcpdump -i any udp port 8472 -nn
# [k8s-ctr] 에서 ping 실행
$ kubectl exec -it curl-pod -- ping -c 2 -w 1 -W 1 $WEBPOD
# => PING 172.20.2.73 (172.20.2.73) 56(84) bytes of data.
# 64 bytes from 172.20.2.73: icmp_seq=1 ttl=63 time=4.10 ms
# 64 bytes from 172.20.2.73: icmp_seq=2 ttl=63 time=1.47 ms
#
# --- 172.20.2.73 ping statistics ---
# 2 packets transmitted, 2 received, 0% packet loss, time 1000ms
# rtt min/avg/max/mdev = 1.473/2.786/4.100/1.313 ms
# [router]에서 tcpdump 확인 (GENEVE 포트 6081/udp로 통신이 되는지 확인)
$ tcpdump -i any udp port 6081 -nn
# => 00:17:18.537335 eth1 In IP 192.168.10.100.54038 > 192.168.20.100.6081: Geneve, Flags [none], vni 0x53fc: IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 59783, seq 1, length 64
# 00:17:18.537385 eth2 Out IP 192.168.10.100.54038 > 192.168.20.100.6081: Geneve, Flags [none], vni 0x53fc: IP 172.20.0.89 > 172.20.2.73: ICMP echo request, id 59783, seq 1, length 64
# 00:17:18.538608 eth2 In IP 192.168.20.100.61597 > 192.168.10.100.6081: Geneve, Flags [none], vni 0x42b9: IP 172.20.2.73 > 172.20.0.89: ICMP echo reply, id 59783, seq 1, length 64
# 00:17:18.538616 eth1 Out IP <span style="color: green;">192.168.20.100.61597 > 192.168.10.100.6081</span>: <span style="color: green;">Geneve</span>, Flags [none], vni 0x42b9: IP <span style="color: green;">172.20.2.73 > 172.20.0.89</span>: ICMP echo reply, id 59783, seq 1, length 64
# <span style="color: green;">👉 GENEVE을 통해 파드 IP간 통신이 노드간 IP로 캡슐화되어 통신이 되는 것을 확인할 수 있습니다.</span>
# 신규 터미널 [router] : 라우팅이 어떻게 되는가?
$ tcpdump -i any icmp -nn
# => (없음)
# <span style="color: green;">👉 GENEVE을 통해 캡슐화된 패킷으로 통신이 되기 때문에 직접적으로 ICMP(ping) 패킷은 보이지 않습니다.</span>
# 반복 접속
$ kubectl exec -it curl-pod -- curl webpod | grep Hostname
# => Hostname: webpod-697b545f57-b46tz
$ kubectl exec -it curl-pod -- sh -c 'while true; do curl -s --connect-timeout 1 webpod | grep Hostname; echo "---" ; sleep 1; done'
# 신규 터미널 [router]
$ tcpdump -i any udp port 6081 -w /tmp/geneve.pcap
$ tshark -r /tmp/geneve.pcap -d udp.port==6081,geneve
# => 1 0.000000 172.20.0.89 → 172.20.2.73 TCP 130 44270 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2614161702 TSecr=0 WS=128
# 2 0.000026 172.20.0.89 → 172.20.2.73 TCP 130 [TCP Retransmission] 44270 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2614161702 TSecr=0 WS=128
# 3 0.000809 172.20.2.73 → 172.20.0.89 TCP 130 80 → 44270 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3049232850 TSecr=2614161702 WS=128
# 4 0.000817 172.20.2.73 → 172.20.0.89 TCP 130 [TCP Retransmission] 80 → 44270 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3049232850 TSecr=2614161702 WS=128
# 5 0.001933 172.20.0.89 → 172.20.2.73 TCP 122 44270 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2614161704 TSecr=3049232850
# 6 0.001940 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 5#1] 44270 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2614161704 TSecr=3049232850
# 7 0.002279 172.20.0.89 → 172.20.2.73 HTTP 192 GET / HTTP/1.1
# 8 0.002446 172.20.0.89 → 172.20.2.73 TCP 192 [TCP Retransmission] 44270 → 80 [PSH, ACK] Seq=1 Ack=1 Win=64896 Len=70 TSval=2614161705 TSecr=3049232850
# 9 0.004700 172.20.2.73 → 172.20.0.89 TCP 122 80 → 44270 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3049232852 TSecr=2614161705
# 10 0.004712 172.20.2.73 → 172.20.0.89 TCP 122 [TCP Dup ACK 9#1] 80 → 44270 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3049232852 TSecr=2614161705
# 11 0.005871 172.20.2.73 → 172.20.0.89 HTTP 441 HTTP/1.1 200 OK (text/plain)
# 12 0.005874 172.20.2.73 → 172.20.0.89 TCP 441 [TCP Retransmission] 80 → 44270 [PSH, ACK] Seq=1 Ack=71 Win=64256 Len=319 TSval=3049232855 TSecr=2614161705
# 13 0.006514 172.20.0.89 → 172.20.2.73 TCP 122 44270 → 80 [ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2614161709 TSecr=3049232855
# 14 0.006527 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 13#1] 44270 → 80 [ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2614161709 TSecr=3049232855
# 15 0.006916 172.20.0.89 → 172.20.2.73 TCP 122 44270 → 80 [FIN, ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2614161709 TSecr=3049232855
# 16 0.006922 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Retransmission] 44270 → 80 [FIN, ACK] Seq=71 Ack=320 Win=64640 Len=0 TSval=2614161709 TSecr=3049232855
# 17 0.007468 172.20.2.73 → 172.20.0.89 TCP 122 80 → 44270 [FIN, ACK] Seq=320 Ack=72 Win=64256 Len=0 TSval=3049232856 TSecr=2614161709
# 18 0.007474 172.20.2.73 → 172.20.0.89 TCP 122 [TCP Retransmission] 80 → 44270 [FIN, ACK] Seq=320 Ack=72 Win=64256 Len=0 TSval=3049232856 TSecr=2614161709
# 19 0.008120 172.20.0.89 → 172.20.2.73 TCP 122 44270 → 80 [ACK] Seq=72 Ack=321 Win=64640 Len=0 TSval=2614161710 TSecr=3049232856
# 20 0.008125 172.20.0.89 → 172.20.2.73 TCP 122 [TCP Dup ACK 19#1] 44270 → 80 [ACK] Seq=72 Ack=321 Win=64640 Len=0 TSval=2614161710 TSecr=3049232856
# ...
# <span style="color: green;">👉 tshark로도 GENEVE 캡슐화된 패킷을 해석할 수 있지만 조금 더 원활한 해석을 위해서 termshark를 사용해보겠습니다.</span>
$ termshark -r /tmp/geneve.pcap -d udp.port==6081,geneve
# => termshark 2.4.0 | geneve.pcap Analysis Misc
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃Filter: <Apply> <Recent> ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# No. - Time - Source - Dest - Proto - Length - Info - ▲
# 1 0.000000 172.20.0.89 172.20.2.73 TCP 130 44270 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2614161702 TSecr=0 WS=128
# 2 0.000026 172.20.0.89 172.20.2.73 TCP 130 [TCP Retransmission] 44270 → 80 [SYN] Seq=0 Win=64860 Len=0 MSS=1410 SACK_PERM TSval=2614161702 TSecr=0 WS=128
# 3 0.000809 172.20.2.73 172.20.0.89 TCP 130 80 → 44270 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3049232850 TSecr=2614161702 WS=128 █
# 4 0.000817 172.20.2.73 172.20.0.89 TCP 130 [TCP Retransmission] 80 → 44270 [SYN, ACK] Seq=0 Ack=1 Win=64308 Len=0 MSS=1410 SACK_PERM TSval=3049232850 TSecr=2614
# 5 0.001933 172.20.0.89 172.20.2.73 TCP 122 44270 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2614161704 TSecr=3049232850
# 6 0.001940 172.20.0.89 172.20.2.73 TCP 122 [TCP Dup ACK 5#1] 44270 → 80 [ACK] Seq=1 Ack=1 Win=64896 Len=0 TSval=2614161704 TSecr=3049232850
# 7 0.002279 172.20.0.89 172.20.2.73 HTTP 192 GET / HTTP/1.1
# 8 0.002446 172.20.0.89 172.20.2.73 TCP 192 [TCP Retransmission] 44270 → 80 [PSH, ACK] Seq=1 Ack=1 Win=64896 Len=70 TSval=2614161705 TSecr=3049232850
# 9 0.004700 172.20.2.73 172.20.0.89 TCP 122 80 → 44270 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3049232852 TSecr=2614161705
# 10 0.004712 172.20.2.73 172.20.0.89 TCP 122 [TCP Dup ACK 9#1] 80 → 44270 [ACK] Seq=1 Ack=71 Win=64256 Len=0 TSval=3049232852 TSecr=2614161705
# 11 0.005871 172.20.2.73 172.20.0.89 HTTP 441 HTTP/1.1 200 OK (text/plain) ▼
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# [+] Linux cooked capture v2
# [+] Internet Protocol Version 4, <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">Src: 192.168.10.100, Dst: 192.168.20.100</span>
# [+] User Datagram Protocol, Src Port: 41753, Dst Port: 6081
# [+] <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">Generic Network Virtualization Encapsulation</span>, VNI: 0x0053fc [=]
# [+] Ethernet II, Src: 46:02:28:c8:e7:b6 (46:02:28:c8:e7:b6), Dst: ca:7a:5c:b2:2c:4a (ca:7a:5c:b2:2c:4a)
# [+] Internet Protocol Version 4, Src: <span style="color: green; padding: 2px 3px; border: 2px solid red; margin: 0 -5px;">172.20.0.89, Dst: 172.20.2.73</span>
# [+] Transmission Control Protocol, Src Port: 44270, Dst Port: 80, Seq: 1, Ack: 1, Len: 70
# [-] Hypertext Transfer Protocol
# [+] GET / HTTP/1.1
# Host: webpod
# User-Agent: curl/8.14.1
# Accept: */*
# <span style="color: green;">👉 -d udp.port==6081,geneve 옵션을 통해 VXLAN 캡슐화된 패킷을 해석할 수 있습니다.</span>
# 신규 터미널 [k8s-ctr] hubble flow log 모니터링 : overlay 통신 모드 확인!
$ hubble observe -f --protocol tcp --pod curl-pod
# => ...
# Aug 9 15:29:04.546: default/curl-pod:53356 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-overlay FORWARDED (TCP Flags: SYN)
# Aug 9 15:29:04.548: default/curl-pod:53356 (ID:21500) <- default/webpod-697b545f57-66grn:80 (ID:17081) to-endpoint FORWARDED (TCP Flags: SYN, ACK)
# Aug 9 15:29:04.548: default/curl-pod:53356 (ID:21500) -> default/webpod-697b545f57-66grn:80 (ID:17081) to-overlay FORWARDED (TCP Flags: ACK)
# ...
- GENEVE 모드로 변경한 후에도 파드 간 통신이 정상적으로 이루어지는 것을 확인할 수 있었습니다.
K8S Service
클러스터 내부를 외부에 노출하는 방법의 발전단계
- 파드 생성 : K8S 클러스터 내부에서만 접속
- 서비스(Cluster Type) 연결 : K8S 클러스터 내부에서만 접속
- 동일한 애플리케이션의 다수의 파드의 접속을 용이하게 하기 위한 서비스에 접속
-
고정 접속(호출) 방법을 제공 : 흔히 말하는 ‘고정 Virtual IP’ 와 ‘Domain주소’ 생성
- 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속
- 서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type) 도 있습니다.
Service 종류
ClusterIP 타입
- 동일한 애플리케이션을 실행하는 여러 Pod에 접속을 용이하기 위해 사용합니다.
- ClusterIP는 Cluster 내부에서만 접근이 가능하며 외부에서는 접근이 불가능합니다.
- iptables 의 NAT 기능을 이용하여 Pod에 접근하며, 동일한 iptables 분산룰을 각 노드에 적용합니다.
NodePort 타입
- NodePort는 ClusterIP와 같이 Cluster 내부에서 접근이 가능하며, 외부에서도 접근이 가능합니다.
- NodePort도 ClusterIP와 같이 iptables의 NAT 기능을 이용하여 Pod에 접근하며, 각 노드에 NodePort를 할당합니다.
- 외부에서는 NodePort를 통해 각 노드에 접근 할 수 있습니다.
LoadBalancer 타입
- LoadBalancer도 외부에서 접근이 가능하며, 클라우드 서비스에서 제공하는 LoadBalancer를 사용합니다. (AWS의 경우 ELB(Elastic Load Balancer)가 사용됩니다.)
- 온프레미스 환경에서도 MetalLB와 같은 LoadBalancer를 사용할 수 있습니다.
서비스의 구조
서비스를 선언시 port
와 targetPort
, 그리고 label selector
를 사용합니다. 각각의 역할은 다음과 같습니다.
-
port
: 서비스가 listen 할 포트를 지정합니다. -
targetPort
: 대상 파드의 port를 지정합니다. -
label selector
: 대상 파드를 특정합니다.
kube-proxy 모드
- kube-proxy는 쿠버네티스 클러스터에서 서비스의 트래픽을 파드로 라우팅하는 역할을 합니다.
- 선택사항이지만, 반드시 kube-proxy를 대체할 수 있는 대안이 배포되어야 합니다. (예) cilium 등)
- kube-proxy는 서비스 통신 동작에 대한 설정을 관리합니다. 데몬셋으로 배포되어 모든 노드에 파드가 생성됩니다.
- kube-proxy 모드의 종류는 userspace proxy 모드, iptables proxy 모드, ipvs proxy 모드, nftables proxy 모드 등이 있습니다.
userspace proxy 모드 (현재는 미사용)
출처 : https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6
- 기초적인 모드이며 사용자 영역의 kube-proxy를 통해 NIC1으로 들어온 패킷을 NIC2로 전달하여 목적 파드로 전달합니다.
- 이렇게 하는 과정에서 커널영역(netfilter)과 사용자영역(kube-proxy)를 오가는 과정에서 스위칭에 의한 오버헤드가 발생하는 단점이 있습니다.
iptables proxy 모드
출처 : https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6
- 쿠버네티스 설치시 기본 모드이며, userspace proxy 모드와는 달리 kube-proxy는 트래픽 전달에 직접 관여하지는 않고, netfilter(iptables)를 사용하여 트래픽을 전달합니다.
- iptables proxy 모드는 트래픽 전달 과정에서 kube-proxy를 경유하지 않고, 커널 영역과 사용자 영역 전환이 필요하지 않아서, 유저스페이스 proxy 모드에 비해 오버헤드가 줄어듭니다.
- 단점으로는 iptables 규칙이 많아 질 경우 모든 규칙 평가 하는데 지연이 발생할 수 있습니다.
- 또한 장애시 모든 규칙을 확인하기 어려워 장애 처리에 불리합니다.
ipvs proxy 모드
- IPVS Mode는 Linue Kernel에서 제공하는 L4 Load Balacner인 IPVS가 Service Proxy 역할을 수행하는 Mode입니다.
- Packet Load Balancing 수행시 IPVS가 iptables보다 높은 성능을 보이기 때문에 IPVS Mode는 iptables Mode보다 높은 성능을 보여준다
- IPVS 프록시 모드는 iptables 모드와 유사한 netfilter hook 기능을 기반으로 하지만, 해시 테이블을 기본 데이터 구조로 사용하고 커널 스페이스에서 동작하여 효율 적으로 동작합니다.
- 다른 프록시 모드와 비교했을 때, IPVS 모드는 높은 네트워크 트래픽 처리량도 지원합니다.
nftables proxy 모드
- nftables 는 iptables를 대체하기 위해 개발된 패킷 필터링 프레임워크로, iptables 보다 더 유연하고 강력한 규칙 설정을 제공합니다.
- 하지만 아직 실험적으로 개발중인 단계로 실무에서는 ipvs proxy 모드를 권장합니다.
eBPF 모드 + XDP
- 앞에서 알아보았던 모든 모드들이 netfilter 기반인데 반해, eBPF 모드 + XDP 는 netfilter 전 단계에서 트래픽 라우팅을 처리하여 훨씬 효율 적입니다. calico나 cilium을 사용하여서 eBPF 모드를 사용할 수 있습니다.
Service LB-IPAM
출처 : https://isovalent.com/blog/post/migrating-from-metallb-to-cilium/
- LoadBalancer IP Address Management (LB IPAM) 소개 - Docs, Youtube
- LB-IPAM은 Cilium에서 제공하는 LoadBalancer IP Address Management 기능으로 LoadBalancer 서비스의 External-IP 를 관리합니다. 이 기능은 AWS 등의 클라우드 제공업체에서 제공하는 기능이지만 Private Cloud 환경에서는 K8S 자체에서는 지원하지 않기 때문에 MetalLB와 같은 솔루션을 사용해야 합니다.
- Cilium은 LB-IPAM을 통해서 MetalLB와 같은 솔루션 없이도 LoadBalancer 서비스를 지원합니다.
- LB-IPAM은
Cilium BGP Control Plane
및L2 Announcements
/L2 Aware LB
등의 기능과 함께 사용됩니다. LB-IPAM이 서비스 객체 및 기타 기능에 IP를 할당하고,L2 Announcements
를 통해서 IP를 노출하고,L2 Aware LB
를 통해서 IP를 라우팅합니다. - LB IPAM은 항상 활성화되어 있지만 휴면 상태입니다. 첫 번째 IP 풀이 클러스터에 추가되면 컨트롤러가 활성화됩니다.
- 기능
- [k8s 클러스터 내부] webpod 서비스를 LoadBalancer Type 설정 with Cilium LB IPAM
$ kubectl get CiliumLoadBalancerIPPool -A
# => No resources found
# <span style="color: green;">👉 아직 등록된 IP Pool이 없으므로, CiliumLoadBalancerIPPool 리소스가 없습니다.</span>
# cilium ip pool 생성
# 충돌나지 않는지 대역 확인 할 것!
$ cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2" # v1.17 : cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: "cilium-lb-ippool"
spec:
blocks:
- start: "192.168.10.211"
stop: "192.168.10.215"
EOF
# => ciliumloadbalancerippool.cilium.io/cilium-lb-ippool created
$ kubectl get CiliumLoadBalancerIPPool -A
# => NAME DISABLED CONFLICTING IPS AVAILABLE AGE
# cilium-lb-ippool false False 5 13s
# CiliumLoadBalancerIPPool 축약어 : ippools,ippool,lbippool,lbippools
$ kubectl api-resources | grep -i CiliumLoadBalancerIPPool
# => ciliumloadbalancerippools ippools,ippool,lbippool,lbippools cilium.io/v2 false CiliumLoadBalancerIPPool
$ kubectl get ippools
# => NAME DISABLED CONFLICTING IPS AVAILABLE AGE
# cilium-lb-ippool false False 5 101s
# webpod 서비스를 LoadBalancer Type 변경 설정
$ kubectl patch svc webpod -p '{"spec":{"type":"LoadBalancer"}}'
# => service/webpod patched
# 확인
$ kubectl get svc webpod
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# webpod LoadBalancer 10.96.129.95 <span style="color: green;">192.168.10.211</span> 80:32203/TCP 3h17m
# <span style="color: green;">👉 webpod 서비스가 LoadBalancer Type으로 변경되었으며, EXTERNAL-IP로 Cilium LB IP Pool에서 할당된 IP가 설정되었습니다.</span>
# LBIP로 curl 요청 확인 : k8s 노드들에서 LB EXIP로 통신 가능!
$ kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# => 192.168.10.211
$ LBIP=$(kubectl get svc webpod -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ curl -s $LBIP
# => Hostname: webpod-697b545f57-b46tz
# IP: 127.0.0.1
# IP: ::1
# IP: 172.20.0.131
# IP: fe80::48ca:7aff:fee8:da55
# RemoteAddr: 172.20.0.201:38068
# GET / HTTP/1.1
# Host: 192.168.10.211
# User-Agent: curl/8.5.0
# Accept: */*
$ kubectl exec -it curl-pod -- curl -s $LBIP
# => Hostname: webpod-697b545f57-b46tz
# IP: 127.0.0.1
# IP: ::1
# IP: 172.20.0.131
# IP: fe80::48ca:7aff:fee8:da55
# RemoteAddr: 172.20.0.89:33706
# GET / HTTP/1.1
# Host: 192.168.10.211
# User-Agent: curl/8.14.1
# Accept: */*
$ kubectl exec -it curl-pod -- curl -s $LBIP | grep Hostname # 대상 파드 이름 출력
# => Hostname: webpod-697b545f57-b46tz
$ kubectl exec -it curl-pod -- curl -s $LBIP | grep RemoteAddr # 대상 파드 입장에서 소스 IP 출력(Layer3)
# => RemoteAddr: 172.20.0.89:46452
# <span style="color: green;">👉 curl-pod의 파드 IP가 소스 IP로 출력됩니다.</span>
$ curl -s $LBIP | grep RemoteAddr # k8s-ctr 노드에서 curl 요청시 소스 IP 출력
# => RemoteAddr: 172.20.0.201:35196
# <span style="color: green;">👉 k8s-ctr 노드의 cilium_host의 IP가 출력됩니다.</span>
# 반복 접속 :
$ while true; do kubectl exec -it curl-pod -- curl -s $LBIP | grep Hostname; sleep 0.1; done
$ for i in {1..100}; do kubectl exec -it curl-pod -- curl -s $LBIP | grep Hostname; done | sort | uniq -c | sort -nr
# => 38 Hostname: webpod-697b545f57-66grn
# 35 Hostname: webpod-697b545f57-b46tz
# 27 Hostname: webpod-697b545f57-24ck5
# <span style="color: green;">👉 각 노드의 webpod 파드로 Loadbalancing이 잘 이루어지고 있습니다.</span>
# IP 할당 확인
$ kubectl get ippools
# => NAME DISABLED CONFLICTING IPS AVAILABLE AGE
# cilium-lb-ippool false False 4 8m
$ kubectl get ippools -o jsonpath='{.items[*].status.conditions[?(@.type!="cilium.io/PoolConflict")]}' | jq
# => {
# "lastTransitionTime": "2025-08-09T08:27:06Z",
# "message": "5",
# "observedGeneration": 1,
# "reason": "noreason",
# "status": "Unknown",
# "type": "cilium.io/IPsTotal"
# }
# {
# "lastTransitionTime": "2025-08-09T08:27:06Z",
# "message": "4",
# "observedGeneration": 1,
# "reason": "noreason",
# "status": "Unknown",
# "type": "cilium.io/IPsAvailable"
# }
# {
# "lastTransitionTime": "2025-08-09T08:27:06Z",
# "message": "1",
# "observedGeneration": 1,
# "reason": "noreason",
# "status": "Unknown",
# "type": "cilium.io/IPsUsed"
# }
$ kubectl get svc webpod -o json | jq
$ kubectl get svc webpod -o jsonpath='{.status}' | jq
# => {
# "conditions": [
# {
# "lastTransitionTime": "2025-08-09T08:29:01Z",
# "message": "",
# "reason": "satisfied",
# "status": "True",
# "type": "cilium.io/IPAMRequestSatisfied"
# }
# ],
# "loadBalancer": {
# "ingress": [
# {
# "ip": "192.168.10.211",
# "ipMode": "VIP"
# }
# ]
# }
# }
- [k8s 클러스터 외부] webpod 서비스를 LoadBalancer External IP로 호출 확인
# router : K8S 외부에서 통신 불가!
$ LBIP=192.168.10.211
$ curl --connect-timeout 1 $LBIP
# => curl: (28) Failed to connect to 192.168.10.211 port 80 after 1002 ms: Timeout was reached
$ arping -i eth1 $LBIP -c 1
# => ARPING 192.168.10.211
# Timeout
$ arping -i eth1 $LBIP -c 100000
# => ARPING 192.168.10.211
# Timeout
# Timeout
# Timeout
# ...
# <span style="color: green;">👉 클러스터 외부에서는 webpod 서비스의 LoadBalancer External IP로 통신이 불가능합니다.</span>
# <span style="color: green;"> LB External IP가 외부로 광고(Announcement) 되지 않기 때문입니다.</span>
Cilium L2 Announcement
(참고) MetalLB Layer2 모드
- Docs
- MetalLB는 Layer2 모드에서 BGP를 사용하지 않고, ARP(IPv4) 또는 NDP(IPv6)를 사용하여 IP 주소를 광고합니다.
- 하나의 노드 (Leader Node)에서만 IP 주소를 광고하며, Leader Node가 장애가 발생하면 다른 노드가 Leader Node로 승격되어 IP 주소를 광고합니다.
- MetalLB의 장점은 별도의 하드웨어나 라우팅 장치가 없어도 어떠한 이더넷 네트워크에서도 동작한다는 점입니다.
-
아래는 MetalLB Layer2 모드에서 GARP 패킷을 통해 VIP(Virtual IP : 서비스의 IP)를 광고하는 예시입니다.
- MetalLB Layer 2 모드에서는 기본적으로 모든 트래픽이 Leader Node로 전달되며, kube-proxy 등을 통해 각 노드로 트래픽이 분산됩니다.
-
이로 인해 Leader Node에 장애가 발생하면 트래픽이 중단될 수 있습니다.
- Leader Node가 장애가 발생하면, 다른 노드가 Leader Node로 승격되어 IP 주소를 광고합니다.
이때 새로운 Leader Node가 IP 주소를 GARP로 광고하기 전에 일정 시간 동안 트래픽이 중단될 수 있습니다.
Cilium Layer 2 (L2) Announcement Using ARP
- Docs
- L2 Announcements은 로컬 영역 네트워크에서 서비스를 가시화하고 도달할 수 있도록 하는 기능입니다.
- 이 기능은 주로 사무실이나 캠퍼스 네트워크와 같은 BGP 기반 라우팅 없이 네트워크 내에서 온프레미스 배포를 목적으로 합니다.
- 이 기능을 사용하면 ExternalIPs 또는 LoadBalancer IP에 대한 ARP 쿼리에 응답합니다. 이러한 IP는 여러 노드에 있는 VIP(Virtual IP)이므로 각 서비스에 대해 한 번에 하나의 노드가 ARP 쿼리에 응답하고 MAC 주소로 응답합니다. 이 노드는 서비스 로드 밸런싱 기능으로 로드 밸런싱을 수행하여 north/south(내부<=>외부) 로드 밸런서 역할을 합니다.
- 이 기능의 장점은 각 서비스가 고유한 IP를 사용할 수 있어 여러 서비스가 동일한 포트 번호를 사용할
수 있다는 점입니다. NodePort를 사용할 때 트래픽을 보낼 호스트를 결정하는 것은 클라이언트의 몫이며,
노드가 다운되면 IP+Port 조합을 사용할 수 없게 됩니다. L2 Announcements를 통해 서비스 VIP는 다른 노드로 마이그레이션하면 계속 작동하게 됩니다.
[k8s 클러스터 외부] webpod 서비스를 LoadBalancer External IP로 호출확인
# 모니터링 : router VM
$ arping -i eth1 $LBIP -c 100000
# => ARPING 192.168.10.211
# Timeout
# Timeout
# ...
# <span style="color: green;">👉 설정 업그레이드 및 CiliumL2AnnouncementPolicy을 통해 정책 설정 시점부터 통신이 됩니다.</span>
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.211): index=0 time=1.101 msec
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.211): index=1 time=1.967 msec
# ...
# 설정 업그레이드
$ helm upgrade cilium cilium/cilium --namespace kube-system --version 1.18.0 --reuse-values \
--set l2announcements.enabled=true
# => Release "cilium" has been upgraded. Happy Helming!
$ kubectl rollout restart -n kube-system ds/cilium
$ watch -d kubectl get pod -A
# 확인
$ kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg config --all | grep EnableL2Announcements
# => EnableL2Announcements : true
$ cilium config view | grep enable-l2
# => enable-l2-announcements true
# enable-l2-neigh-discovery false
# 정책 설정 : arp 광고하게 될 service 와 node 지정(controlplane 제외) -> 설정 직후 arping 확인!
## 제약사항 : L2 ARP 모드에서 LB IPPool 은 같은 네트워크 대역에서만 유효. -> k8s-w0 을 제외한 이유. 포함 시 리더 노드 선정 시 동작 실패 상황 발생!
$ cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1" # not v2
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
serviceSelector:
matchLabels:
app: webpod
nodeSelector:
matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- k8s-w0
interfaces:
- ^eth[1-9]+
externalIPs: true
loadBalancerIPs: true
EOF
# => ciliuml2announcementpolicy.cilium.io/policy1 created
# <span style="color: green;">👉 정책 설정 후 부터 arping이 성공합니다.</span>
# 확인
$ kubectl -n kube-system get lease
$ kubectl -n kube-system get lease | grep "cilium-l2announce"
# => NAME HOLDER AGE
# cilium-l2announce-default-webpod k8s-w1 2m56s
# ...
# 현재 리더 역할 노드 확인
$ kubectl -n kube-system get lease/cilium-l2announce-default-webpod -o yaml | yq
# => ...
# spec:
# acquireTime: "2025-08-09T12:01:16.067503Z"
# holderIdentity: k8s-w1
# leaseDurationSeconds: 15
# leaseTransitions: 0
# renewTime: "2025-08-09T12:05:03.883829Z"
# 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
# => cilium-9dp58 cilium-tj7tw cilium-nftrl
# 현재 해당 IP에 대한 리더가 위치한 노드의 cilium-agent 파드 내에서 정보 확인
$ kubectl exec -n kube-system $CILIUMPOD0 -- cilium-dbg shell -- db/show l2-announce
# => IP NetworkInterface
$ kubectl exec -n kube-system $CILIUMPOD1 -- cilium-dbg shell -- db/show l2-announce
# => IP NetworkInterface
# 192.168.10.211 eth1
# <span style="color: green;">👉 리더 파드가 k8s-w1에 있기 때문에 k8s-w1의 IP와 Network Interface만 나옵니다.</span>
$ kubectl exec -n kube-system $CILIUMPOD2 -- cilium-dbg shell -- db/show l2-announce
# => IP NetworkInterface
# 로그 확인
$ kubectl -n kube-system logs ds/cilium | grep "l2"
# router VM : LBIP로 curl 요청 확인
$ arping -i eth1 $LBIP -c 1000
$ curl --connect-timeout 1 $LBIP
# => Hostname: webpod-697b545f57-b46tz
# IP: 127.0.0.1
# IP: ::1
# IP: 172.20.0.131
# IP: fe80::48ca:7aff:fee8:da55
# RemoteAddr: 172.20.1.197:44454
# GET / HTTP/1.1
# Host: 192.168.10.211
# User-Agent: curl/8.5.0
# Accept: */*
# VIP 에 대한 mac 주소가 리더 노드의 mac 주소와 동일함을 확인
$ arp -a
# => ? (192.168.10.211) at 08:00:27:8e:9b:f8 [ether] on eth1
# ? (192.168.10.101) at 08:00:27:8e:9b:f8 [ether] on eth1
# ? (192.168.10.100) at 08:00:27:42:b2:8c [ether] on eth1
# ...
$ curl -s $LBIP
$ curl -s $LBIP | grep Hostname
# => Hostname: webpod-697b545f57-24ck5
$ curl -s $LBIP | grep RemoteAddr
# => RemoteAddr: 192.168.10.200:57078
# 리더 노드가 아닌 다른 노드에 webpod 통신 시, SNAT 됨 : arp 동작(리더 노드)으로 인한 제약 사항
$ while true; do curl -s --connect-timeout 1 $LBIP | grep Hostname; sleep 0.1; done
$ while true; do curl -s --connect-timeout 1 $LBIP | grep RemoteAddr; sleep 0.1; done
# => RemoteAddr: 192.168.10.200:57284
# RemoteAddr: 192.168.10.200:57294
# RemoteAddr: 172.20.1.197:57306 # <span style="color: green;">👉 leader node인 k8s-w1이 아닌 다른 노드의 파드가 응답한 경우 SNAT으로 인해 다른 IP가 표시됨</span>
# RemoteAddr: 172.20.1.197:57310
# RemoteAddr: 192.168.10.200:57364
# ...
L2 Announcement 리더 노드에 주입 후 Failover 확인
# 신규 터미널 (router) : 반복 호출
$ while true; do curl -s --connect-timeout 1 $LBIP | grep Hostname; sleep 0.1; done
# 현재 리더 노드 확인
$ kubectl -n kube-system get lease | grep "cilium-l2announce"
# => NAME HOLDER AGE
# cilium-l2announce-default-webpod k8s-w1 19m
# 리더 노드 강제 reboot
$ sshpass -p 'vagrant' ssh vagrant@k8s-w1 sudo reboot
# <span style="color: green;">👉 리더 노드 재부팅시 curl이 타임아웃 되어 통신이 안 됩니다.</span>
# 신규 터미널 (router) : arp 변경(갱신) 확인
$ arp -a
# => ? (192.168.10.211) at <span style="color: green;">08:00:27:42:b2:8c</span> [ether] on eth1
# ? (192.168.10.101) at 08:00:27:8e:9b:f8 [ether] on eth1
# ? (192.168.10.100) at 08:00:27:42:b2:8c [ether] on eth1
# ...
# <span style="color: green;">👉 VIP(192.168.10.211)의 MAC 주소가 k8s-ctr의 eth1로 변경되었습니다.</span>
# 현재 리더 노드 확인
$ kubectl -n kube-system get lease | grep "cilium-l2announce"
# => cilium-l2announce-default-webpod k8s-ctr 24m
# <span style="color: green;">👉 예상대로 k8s-ctr 노드가 리더 노드로 승격되었습니다.</span>
Service LB-IPAM 기능
Service 추가시 동작
#
$ cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-web
labels:
app: netshoot-web
spec:
replicas: 3
selector:
matchLabels:
app: netshoot-web
template:
metadata:
labels:
app: netshoot-web
spec:
terminationGracePeriodSeconds: 0
containers:
- name: netshoot
image: nicolaka/netshoot
ports:
- containerPort: 8080
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
command: ["sh", "-c"]
args:
- |
while true; do
{ echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nOK from \$POD_NAME"; } | nc -l -p 8080 -q 1;
done
EOF
# => deployment.apps/netshoot-web created
#
$ cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: netshoot-web
labels:
app: netshoot-web
spec:
type: LoadBalancer
selector:
app: netshoot-web
ports:
- name: http
port: 80
targetPort: 8080
EOF
# => service/netshoot-web created
# LB IP 확인
$ kubectl get svc netshoot-web
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# netshoot-web LoadBalancer 10.96.170.130 <span style="color: green;">192.168.10.212</span> 80:30240/TCP 8s
#
$ cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1" # not v2
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy2
spec:
serviceSelector:
matchLabels:
app: netshoot-web
nodeSelector:
matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- k8s-w0
interfaces:
- ^eth[1-9]+
externalIPs: true
loadBalancerIPs: true
EOF
# => ciliuml2announcementpolicy.cilium.io/policy2 created
# Service 별로 리더 노드가 다름 : 즉, 외부 인입 시 Service 별로 나름 분산(?) 처리..
$ kubectl -n kube-system get lease | grep "cilium-l2announce"
# => cilium-l2announce-default-netshoot-web k8s-w1 36s
# cilium-l2announce-default-webpod k8s-ctr 116m
# 호출 확인
## LBIP로 curl 요청 확인 : k8s 노드들에서 LB EXIP로 통신 가능!
$ kubectl get svc netshoot-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# => 192.168.10.212
$ LB2IP=$(kubectl get svc netshoot-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ curl -s $LB2IP
# => OK from netshoot-web-5c59d94bd4-jsh8p
## 신규터미널 (router)
$ LB2IP=192.168.10.212
$ arping -i eth1 $LB2IP -c 2
# => ARPING 192.168.10.212
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.212): index=0 time=452.250 usec
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.212): index=1 time=580.375 usec
#
# --- 192.168.10.212 statistics ---
# 2 packets transmitted, 2 packets received, 0% unanswered (0 extra)
# rtt min/avg/max/std-dev = 0.452/0.516/0.580/0.064 ms
$ curl -s $LB2IP
# => OK from netshoot-web-5c59d94bd4-5zb78
- 특이하게도 Service별로 리더 노드가 다릅니다. 서비스가 여러개인 경우에는 서비스 별로 분산되는 효과가 있어서 리더 노드에 부하가 집중되는 문제가 다소 완화될 것으로 보입니다.
Requesting IPs
- 특정 Service의 External IP를 직접 설정할 수 있습니다. Docs
# Service netshoot-web 에 EX-IP를 직접 지정 변경
$ kubectl edit svc netshoot-web
# 또는 k9s → svc → <e> edit
---
## metadata.annotations 아래 아래 추가
annotations:
"lbipam.cilium.io/ips": "192.168.10.215"
---
# => service/netshoot-web edited
#
$ kubectl get svc netshoot-web
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# netshoot-web LoadBalancer 10.96.170.130 <span style="color: green;">192.168.10.215</span> 80:30240/TCP 9m6s
# <span style="color: green;">👉 annotation을 통해 지정한 EXTERNAL-IP(192.168.10.215)가 설정되었습니다.</span>
#
$ kubectl get svc netshoot-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# => 192.168.10.215
$ LB2IP=$(kubectl get svc netshoot-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ curl -s $LB2IP
# => OK from netshoot-web-5c59d94bd4-5zb78
Sharing Keys
- External IP 1개를 각기 다른 Port를 통해서 사용할 수 있습니다. Docs
#
$ cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: netshoot-web2
labels:
app: netshoot-web
spec:
type: LoadBalancer
selector:
app: netshoot-web
ports:
- name: http
port: 8080
targetPort: 8080
EOF
# => service/netshoot-web2 created
#
$ kubectl get svc -l app=netshoot-web
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# netshoot-web LoadBalancer 10.96.170.130 192.168.10.215 80:30240/TCP 12m
# netshoot-web2 LoadBalancer 10.96.75.231 192.168.10.212 8080:31278/TCP 14s
# Service netshoot-web에 annotations 추가
$ kubectl edit svc netshoot-web
# 또는 k9s → svc → <e> edit
---
## metadata.annotations 아래 아래 추가
annotations:
"lbipam.cilium.io/ips": "192.168.10.215"
"lbipam.cilium.io/sharing-key": "1234"
---
# => service/netshoot-web edited
# 동일하게 netshoot-web2 서비스에도 annotations 추가
$ kubectl edit svc netshoot-web2
# => service/netshoot-web2 edited
#
$ kubectl get svc -l app=netshoot-web
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# netshoot-web LoadBalancer 10.96.170.130 192.168.10.215 80:30240/TCP 18m
# netshoot-web2 LoadBalancer 10.96.75.231 192.168.10.215 8080:31278/TCP 5m36s
# sharing-key 사용되는 IP는 모든 같은 리더 노드 사용
$ kubectl -n kube-system get lease | grep "cilium-l2announce"
# => cilium-l2announce-default-netshoot-web k8s-w1 17m
# cilium-l2announce-default-netshoot-web2 k8s-w1 5m45s
# cilium-l2announce-default-webpod k8s-ctr 133m
# 호출 확인
$ curl -s $LB2IP
# => OK from netshoot-web-5c59d94bd4-5zb78
# <span style="color: green;">👉 netshoot-web 서비스 응답</span>
$ curl -s $LB2IP:8080
# => OK from netshoot-web-5c59d94bd4-4fsnp
# <span style="color: green;">👉 netshoot-web2 서비스 응답</span>
# 신규터미널 (router)
$ LB2IP=192.168.10.215
$ arping -i eth1 $LB2IP -c 2
# => ARPING 192.168.10.215
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.215): index=0 time=1.236 msec
# 60 bytes from 08:00:27:8e:9b:f8 (192.168.10.215): index=1 time=372.519 usec
#
# --- 192.168.10.215 statistics ---
# 2 packets transmitted, 2 packets received, 0% unanswered (0 extra)
# rtt min/avg/max/std-dev = 0.373/0.804/1.236/0.432 ms
$ curl -s $LB2IP
# => OK from netshoot-web-5c59d94bd4-jsh8p
$ curl -s $LB2IP:8080
# => OK from netshoot-web-5c59d94bd4-5zb78
마치며
이번 포스트에서도 노드의 파드들간 통신에 대해서 알아보았습니다.
가장 관심 있게 본 점은 Cilium의 LoadBalancer IPAM 기능을 통해서 MetalLB (L2 모드)를 대체할 수 있다는 것입니다.
MetalLB라는 추가적인 구성요소 없이 Cilium 만으로 LoadBalancer Service를 구현할 수 있다는 점이 매력적인것 같습니다.
점점 더 Cilium의 매력에 빠져드는것 같습니다. 다음 주에도 Cilium에 대해서 더 알아보도록 하겠습니다.
- 약어 소개
- S.IP : Source IP, 출발지(소스) IP
- D.IP : Destination IP, 도착치(목적지) IP
- S.Port : Source Port, 출발지(소스) 포트
- D.Port : Destination Port, 도착지(목적지) 포트
- NAT : Network Address Translation, 네트워크 주소 변환
- SNAT : Source IP 를 NAT 처리, 일반적으로 출발지 IP를 변환
- DNAT : Destination IP 를 NAT 처리, 일반적으로 목적지 IP와 목적지 포트를 변환