들어가며

이번 포스트에서는 Overlay Network인 VXLAN을 통한 파드간 통신과 Kubernetes 서비스의 외부 노출, LB-IPAM 등을 통한 통신을 살펴보겠습니다.


실습 환경 구성

  • 이번 실습에서는 다음 그림과 같이 k8s-w0를 별도의 네트워크에 배치하고 router를 통해 k8s-w0와 k8s-ctr/w1 노드간 통신을 확인합니다. img.png
    • 기본 배포 가상 머신 : 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 설정

Docs

# [커널 구성 옵션] 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)
#    ...

img.png 파드에서 나갈때 패킷 흐름

img_1.png 파드로 인입시 패킷 흐름

  • 이상과 같이 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 등과 유사한 네트워크 가상화 캡슐화 프로토콜로 좀 더 발전된 기능을 제공합니다.
  • 주요 특징은 다음과 같습니다. img.png 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

클러스터 내부를 외부에 노출하는 방법의 발전단계

  1. 파드 생성 : K8S 클러스터 내부에서만 접속 img.png
  2. 서비스(Cluster Type) 연결 : K8S 클러스터 내부에서만 접속 img_1.png
    • 동일한 애플리케이션의 다수의 파드의 접속을 용이하게 하기 위한 서비스에 접속
    • 고정 접속(호출) 방법을 제공 : 흔히 말하는 ‘고정 Virtual IP’ 와 ‘Domain주소’ 생성

  3. 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속 img_2.png
    • 서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type) 도 있습니다.

Service 종류

ClusterIP 타입

img.png

  • 동일한 애플리케이션을 실행하는 여러 Pod에 접속을 용이하기 위해 사용합니다.
  • ClusterIP는 Cluster 내부에서만 접근이 가능하며 외부에서는 접근이 불가능합니다.
  • iptables 의 NAT 기능을 이용하여 Pod에 접근하며, 동일한 iptables 분산룰을 각 노드에 적용합니다.

NodePort 타입

img.png

  • NodePort는 ClusterIP와 같이 Cluster 내부에서 접근이 가능하며, 외부에서도 접근이 가능합니다.
  • NodePort도 ClusterIP와 같이 iptables의 NAT 기능을 이용하여 Pod에 접근하며, 각 노드에 NodePort를 할당합니다.
  • 외부에서는 NodePort를 통해 각 노드에 접근 할 수 있습니다.

LoadBalancer 타입

img.png

  • LoadBalancer도 외부에서 접근이 가능하며, 클라우드 서비스에서 제공하는 LoadBalancer를 사용합니다. (AWS의 경우 ELB(Elastic Load Balancer)가 사용됩니다.)
  • 온프레미스 환경에서도 MetalLB와 같은 LoadBalancer를 사용할 수 있습니다.

서비스의 구조

서비스를 선언시 porttargetPort, 그리고 label selector 를 사용합니다. 각각의 역할은 다음과 같습니다.

  • port : 서비스가 listen 할 포트를 지정합니다.
  • targetPort : 대상 파드의 port를 지정합니다.
  • label selector : 대상 파드를 특정합니다.

img.png

kube-proxy 모드

  • kube-proxy는 쿠버네티스 클러스터에서 서비스의 트래픽을 파드로 라우팅하는 역할을 합니다.
  • 선택사항이지만, 반드시 kube-proxy를 대체할 수 있는 대안이 배포되어야 합니다. (예) cilium 등)
  • kube-proxy는 서비스 통신 동작에 대한 설정을 관리합니다. 데몬셋으로 배포되어 모든 노드에 파드가 생성됩니다.
  • kube-proxy 모드의 종류는 userspace proxy 모드, iptables proxy 모드, ipvs proxy 모드, nftables proxy 모드 등이 있습니다.

userspace proxy 모드 (현재는 미사용)

img.png 출처 : https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6

  • 기초적인 모드이며 사용자 영역의 kube-proxy를 통해 NIC1으로 들어온 패킷을 NIC2로 전달하여 목적 파드로 전달합니다.
  • 이렇게 하는 과정에서 커널영역(netfilter)과 사용자영역(kube-proxy)를 오가는 과정에서 스위칭에 의한 오버헤드가 발생하는 단점이 있습니다.

iptables proxy 모드

img.png 출처 : https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6

  • 쿠버네티스 설치시 기본 모드이며, userspace proxy 모드와는 달리 kube-proxy는 트래픽 전달에 직접 관여하지는 않고, netfilter(iptables)를 사용하여 트래픽을 전달합니다.
  • iptables proxy 모드는 트래픽 전달 과정에서 kube-proxy를 경유하지 않고, 커널 영역과 사용자 영역 전환이 필요하지 않아서, 유저스페이스 proxy 모드에 비해 오버헤드가 줄어듭니다.
  • 단점으로는 iptables 규칙이 많아 질 경우 모든 규칙 평가 하는데 지연이 발생할 수 있습니다.
  • 또한 장애시 모든 규칙을 확인하기 어려워 장애 처리에 불리합니다.

ipvs proxy 모드

img.png

  • 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

img.png

  • 앞에서 알아보았던 모든 모드들이 netfilter 기반인데 반해, eBPF 모드 + XDP 는 netfilter 전 단계에서 트래픽 라우팅을 처리하여 훨씬 효율 적입니다. calico나 cilium을 사용하여서 eBPF 모드를 사용할 수 있습니다.

Service LB-IPAM

img.png 출처 : 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 PlaneL2 Announcements / L2 Aware LB 등의 기능과 함께 사용됩니다. LB-IPAM이 서비스 객체 및 기타 기능에 IP를 할당하고, L2 Announcements를 통해서 IP를 노출하고, L2 Aware LB를 통해서 IP를 라우팅합니다.
    • LB IPAM은 항상 활성화되어 있지만 휴면 상태입니다. 첫 번째 IP 풀이 클러스터에 추가되면 컨트롤러가 활성화됩니다.
    • 기능
      • Service Selectors - Docs
      • Disabling a Pool - Docs
      • Service 사용 확인 - Docs
      • LoadBalancerClass : BGP or L2 지정 - Docs
      • Requesting IPs : 특정 Service에 EX-IP를 직접 설정 - Docs
      • Sharing Keys : EX-IP 1개를 각기 다른 Port 를 통해서 사용 - Docs
  • [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)를 광고하는 예시입니다. img.png img_1.png

  • MetalLB Layer 2 모드에서는 기본적으로 모든 트래픽이 Leader Node로 전달되며, kube-proxy 등을 통해 각 노드로 트래픽이 분산됩니다.
  • 이로 인해 Leader Node에 장애가 발생하면 트래픽이 중단될 수 있습니다. img_2.png

  • Leader Node가 장애가 발생하면, 다른 노드가 Leader Node로 승격되어 IP 주소를 광고합니다. 이때 새로운 Leader Node가 IP 주소를 GARP로 광고하기 전에 일정 시간 동안 트래픽이 중단될 수 있습니다. img_3.png

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는 다른 노드로 마이그레이션하면 계속 작동하게 됩니다. img.png img_1.png
[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를 구현할 수 있다는 점이 매력적인것 같습니다. :relaxed:

점점 더 Cilium의 매력에 빠져드는것 같습니다. 다음 주에도 Cilium에 대해서 더 알아보도록 하겠습니다. :smile:

  • 약어 소개
    • 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와 목적지 포트를 변환