
들어가며
안녕하세요! Devlos입니다.
이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 Cilium Study 3주 차 주제인 "Networking"에 대해서 정리한 내용입니다. 노드에서 파드간의 통신을 상세하게 살펴봅니다.
실습환경 구성
이번 주차 실습 환경은 mac M3 Pro max 환경에서 실습을 진행했고, VirtualBox + Vagrant로 환경을 구성했어요.
실습 리소스를 줄이기 위해 Contrl Plane 한대와 Data Plane 한대로 클러스터를 구성하고, 가상 사내망 연동 환경을 구성하기 위해
라우터 한대를 추가적으로 생성합니다.
💡 핵심 구성 요소:
- Control Plane (k8s-ctr): Kubernetes 마스터 노드, API 서버 및 컨트롤 플레인 컴포넌트 실행
- Worker Node (k8s-w1): 실제 워크로드가 실행되는 워커 노드
- Router (router): 외부 네트워크와의 통신을 위한 라우팅 역할

- 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.17.6' # Cilium CNI Version : https://github.com/cilium/cilium/tags
N = 1 # max number of worker nodes
# 중요: 버전 호환성 확인 필수
# - Kubernetes 1.33.2와 Cilium 1.17.6은 호환되는 버전
# - Containerd 1.7.27은 최신 안정 버전
# 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 # 중요: Cilium BPF 프로그램 실행을 위한 충분한 메모리
vb.linked_clone = true
end
subconfig.vm.host_name = "k8s-ctr"
subconfig.vm.network "private_network", ip: "192.168.10.100" # 중요: 고정 IP 할당으로 안정적인 네트워킹
subconfig.vm.network "forwarded_port", guest: 22, host: 60000, auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ K8SV, CONTAINERDV ] # 중요: 기본 시스템 설정
subconfig.vm.provision "shell", path: "k8s-ctr.sh", args: [ N, CILIUMV, K8SV ] # 중요: Kubernetes + Cilium 설치
subconfig.vm.provision "shell", path: "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: "init_cfg.sh", args: [ K8SV, CONTAINERDV]
subconfig.vm.provision "shell", path: "k8s-w.sh"
subconfig.vm.provision "shell", path: "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.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "router.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 # 중요: Cilium BPF 프로그램 실행을 위한 보안 정책 비활성화
echo "[TASK 3] Disable and turn off SWAP"
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab # 중요: Kubernetes 요구사항 - 스왑 비활성화
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 # 중요: IP 포워딩 활성화
# enable br_netfilter for iptables
modprobe br_netfilter # 중요: 브리지 네트워크에서 iptables 필터링 활성화
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 # 중요: systemd cgroup 드라이버 사용 (Kubernetes 요구사항)
# 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, Cilium CNI 설치, 편리성 설정(k, kc)
- 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" # 🔧 중요: 노드 IP 명시적 지정으로 네트워킹 안정성 확보
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"
#!/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="kubernetes" --set k8s.requireIPv4PodCIDR=true --set ipv4NativeRoutingCIDR=10.244.0.0/16 \
--set routingMode=native --set autoDirectNodeRoutes=true --set endpointRoutes.enabled=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
# - kubeProxyReplacement=true: kube-proxy 대신 Cilium이 서비스 프록시 역할
# - bpf.masquerade=true: BPF 기반 NAT 처리로 성능 향상
# - routingMode=native: 네이티브 라우팅으로 최적화된 패킷 처리
# - hubble.enabled=true: 네트워크 관찰성 도구 활성화
#--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16 \
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
for (( i=1; i<=$1; i++ )); do echo "192.168.10.10$i k8s-w$i" >> /etc/hosts; done
echo "[TASK 11] Install Prometheus & Grafana"
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.17.6/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] 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 ">>>> K8S Controlplane Config End <<<<"
--set endpointHealthChecking.enabled=falseand --set healthChecking=falsedisable endpoint health checking entirely.- However it is recommended that those features be enabled initially on a smaller cluster (3-10 nodes) where it can be used to detect potential packet loss due to firewall rules or hypervisor settings. - Docs
- k8s-w.sh : kubeadm join
- 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"
#!/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 <<<<"
- 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: 10.10.0.0/16
via: 192.168.10.200 # 외부 네트워크(10.10.0.0/16)로의 라우팅을 router(192.168.10.200)를 통해 설정
EOT
echo ">>>> Route Add Config End <<<<"
- router.sh : router 역할의 (웹) 서버
#!/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
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 # 중요: 라우터 역할을 위한 IP 포워딩 활성화
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 # 중요: 가상 사내망 1번 네트워크
ip link add loop2 type dummy
ip link set loop2 up
ip addr add 10.10.2.200/24 dev loop2 # 중요: 가상 사내망 2번 네트워크
echo "[TASK 5] Install Packages"
export DEBIAN_FRONTEND=noninteractive
apt update -qq >/dev/null 2>&1
apt-get install net-tools jq tree ngrep tcpdump arping -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 <<<<"
아래의 명령을 통해 전체 설치가 가능합니다.
mkdir cilium-lab && cd cilium-lab
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/3w/Vagrantfile
vagrant up
실습 환경 기본 정보 확인
cat /etc/hosts
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w1 hostname
#
ifconfig | grep -iEA1 'eth[0-9]:'
# 클러스터 정보 확인
kubectl cluster-info
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
kubectl describe cm -n kube-system kubelet-config
# 노드 정보 : 상태, INTERNAL-IP 확인
kubectl get node -owide
# 파드 정보 : 상태, 파드 IP 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
kubectl get ciliumnode -o json | grep podCIDRs -A2
kubectl get pod -A -owide
# ipam 모드 확인
cilium config view | grep ^ipam
ipam kubernetes
# iptables 확인
iptables-save
iptables -t nat -S
iptables -t filter -S
iptables -t mangle -S
iptables -t raw -S
✅ Cluster Scope 변경 후 CiliumNode 재할당 실행 결과 요약
1. CiliumNode 삭제 후 새로운 CIDR 할당
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"172.20.2.0/24"
],
--
"podCIDRs": [
"172.20.1.0/24"
],
- 새로운 CIDR 할당: Cluster Scope 모드에서 설정한 172.20.0.0/16 대역으로 변경
- k8s-ctr: 172.20.2.0/24 (새로운 CIDR)
- k8s-w1: 172.20.1.0/24 (새로운 CIDR)
2. 파드 IP 변경 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-ff767b4b6-p85nb 7002 ready 172.20.1.253
cilium-monitoring prometheus-6b6cbb7997-nvkw5 12874 ready 172.20.1.67
default curl-pod 44328 ready 172.20.2.167
default webpod-5b576b7db-597jh 7392 ready 172.20.2.169
default webpod-5b576b7db-gm5s7 7392 ready 172.20.1.58
kube-system coredns-674b8bbfcf-49grd 19280 ready 172.20.2.60
kube-system coredns-674b8bbfcf-fjqnb 19280 ready 172.20.1.28
kube-system hubble-relay-6f6686b9d-b656h 53799 ready 172.20.1.174
kube-system hubble-ui-645fd49bc7-jld4j 11625 ready 172.20.1.78
- IP 대역 변경: 모든 파드가 새로운 172.20.x.x 대역에서 IP 할당받음
- 노드별 분산: k8s-ctr 노드는 172.20.2.x, k8s-w1 노드는 172.20.1.x 대역 사용
- 모든 파드 정상: 모든 엔드포인트가 ready 상태로 정상 동작
3. 라우팅 테이블 변경 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# 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.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.1.0/24 via 192.168.10.101 dev eth1 proto kernel
172.20.2.60 dev lxc4aba2e3dd4e1 proto kernel scope link
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100
- 새로운 라우팅: 172.20.1.0/24와 172.20.2.0/24 대역에 대한 라우팅 규칙 추가
- 노드 간 통신: k8s-ctr에서 k8s-w1으로의 라우팅이 192.168.10.101을 통해 설정됨
4. 파드 재시작 과정
# 기존 파드들 (10.244.x.x 대역 사용)
kubectl get pod -A -owide | grep 10.244.
cilium-monitoring grafana-5c69859d9-gftmc 0/1 Running 0 16m 10.244.0.106 k8s-ctr <none> <none>
cilium-monitoring prometheus-6fc896bc5d-vwg49 1/1 Running 0 16m 10.244.0.22 k8s-ctr <none> <none>
default curl-pod 1/1 Running 0 13m 10.244.0.167 k8s-ctr <none> <none>
default webpod-697b545f57-86kx5 1/1 Running 0 13m 10.244.0.253 k8s-ctr <none> <none>
default webpod-697b545f57-s4d2l 1/1 Running 0 13m 10.244.1.81 k8s-w1 <none> <none>
# 파드 재시작 명령어
kubectl -n kube-system rollout restart deploy/hubble-relay deploy/hubble-ui
kubectl -n cilium-monitoring rollout restart deploy/prometheus deploy/grafana
kubectl rollout restart deploy/webpod
kubectl delete pod curl-pod
- 기존 파드: 10.244.x.x 대역에서 IP 할당받은 상태
- 재시작 필요: 새로운 CIDR 대역을 사용하려면 파드 재시작 필요
5. 새로운 파드 배포
# 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
- 새로운 파드: 172.20.2.167 IP 할당받음 (새로운 CIDR 대역)
- 정상 배포: 새로운 CIDR 대역에서 정상적으로 IP 할당
1. 네트워크 구성 확인
호스트 파일 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 vagrant
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.2.1 k8s-ctr k8s-ctr
192.168.10.100 k8s-ctr
192.168.10.101 k8s-w1
모든 노드의 IP 주소가 hosts 파일에 등록되어 있음
Worker 노드 연결 테스트
(⎈|HomeLab:N/A) root@k8s-ctr:~# sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w1 hostname
Warning: Permanently added 'k8s-w1' (ED25519) to the list of known hosts.
k8s-w1
Control Plane에서 Worker 노드로 SSH 연결 성공
네트워크 인터페이스 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# 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
eth0은 NAT 인터페이스, eth1은 프라이빗 네트워크 인터페이스
2. Kubernetes 클러스터 정보
클러스터 CIDR 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# 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",
Pod CIDR은 10.244.0.0/16, Service CIDR은 10.96.0.0/16
kubeadm 설정 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl describe cm -n kube-system kubeadm-config
Name: kubeadm-config
Namespace: kube-system
Labels: <none>
Annotations: <none>
Data
====
ClusterConfiguration:
----
apiServer: {}
apiVersion: kubeadm.k8s.io/v1beta4
caCertificateValidityPeriod: 87600h0m0s
certificateValidityPeriod: 8760h0m0s
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
encryptionAlgorithm: RSA-2048
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: v1.33.2
networking:
dnsDomain: cluster.local
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/16
proxy: {}
scheduler: {}
Kubernetes v1.33.2, Pod 서브넷 10.244.0.0/16, 서비스 서브넷 10.96.0.0/16
3. 노드 및 파드 상태
노드 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get node -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-ctr Ready control-plane 12m v1.33.2 192.168.10.100 <none> Ubuntu 24.04.2 LTS 6.8.0-53-generic containerd://1.7.27
k8s-w1 Ready <none> 9m57s v1.33.2 192.168.10.101 <none> Ubuntu 24.04.2 LTS 6.8.0-53-generic containerd://1.7.27
모든 노드가 Ready 상태, containerd 1.7.27 사용
Pod CIDR 할당
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
k8s-ctr 10.244.0.0/24
k8s-w1 10.244.1.0/24
각 노드별로 Pod CIDR이 분할되어 할당됨
Cilium 노드 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"10.244.0.0/24"
],
--
"podCIDRs": [
"10.244.1.0/24"
],
Cilium이 각 노드의 Pod CIDR을 인식하고 있음
4. 파드 및 서비스 상태
전체 파드 목록
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -A -owide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cilium-monitoring grafana-5c69859d9-b67fh 1/1 Running 0 12m 10.244.0.156 k8s-ctr <none> <none>
cilium-monitoring prometheus-6fc896bc5d-k5gn4 1/1 Running 0 12m 10.244.0.116 k8s-ctr <none> <none>
kube-system cilium-envoy-h4f28 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system cilium-envoy-r49dx 1/1 Running 0 10m 192.168.10.101 k8s-w1 <none> <none>
kube-system cilium-h45vc 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system cilium-lx5t6 1/1 Running 0 10m 192.168.10.101 k8s-w1 <none> <none>
kube-system cilium-operator-5bc66f5b9b-cwxmz 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system coredns-674b8bbfcf-l2chl 1/1 Running 0 12m 10.244.0.45 k8s-ctr <none> <none>
kube-system coredns-674b8bbfcf-pcnwg 1/1 Running 0 12m 10.244.0.157 k8s-ctr <none> <none>
kube-system etcd-k8s-ctr 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system hubble-relay-5dcd46f5c-bqcbh 1/1 Running 0 12m 10.244.0.61 k8s-ctr <none> <none>
kube-system hubble-ui-76d4965bb6-5kdvd 2/2 Running 0 12m 10.244.0.199 k8s-ctr <none> <none>
kube-system kube-apiserver-k8s-ctr 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system kube-controller-manager-k8s-ctr 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
kube-system kube-proxy-hbxzr 1/1 Running 0 10m 192.168.10.100 k8s-ctr <none> <none>
kube-system kube-proxy-lbcwr 1/1 Running 0 10m 192.168.10.101 k8s-w1 <none> <none>
kube-system kube-scheduler-k8s-ctr 1/1 Running 0 12m 192.168.10.100 k8s-ctr <none> <none>
local-path-storage local-path-provisioner-74f9666bc9-4c6c6 1/1 Running 0 12m 10.244.0.50 k8s-ctr <none> <none>
모든 파드가 Running 상태, Cilium Agent, Hubble, 모니터링 스택 정상 동작
5. Cilium 설정
IPAM 모드 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^ipam
ipam kubernetes
ipam-cilium-node-update-rate 15s
kubernetes IPAM 모드 사용, 노드별 Pod CIDR 할당
6. iptables 규칙 분석
iptables 규칙 요약
(⎈|HomeLab:N/A) root@k8s-ctr:~# iptables-save
# Generated by iptables-save v1.8.10 (nf_tables) on Fri Aug 1 11:14:34 2025
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:CILIUM_POST_mangle - [0:0]
:CILIUM_PRE_mangle - [0:0]
:KUBE-IPTABLES-HINT - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-PROXY-CANARY - [0:0]
-A PREROUTING -m comment --comment "cilium-feeder: CILIUM_PRE_mangle" -j CILIUM_PRE_mangle
-A POSTROUTING -m comment --comment "cilium-feeder: CILIUM_POST_mangle" -j CILIUM_POST_mangle
...
Cilium과 kube-proxy 규칙이 공존하며, NOTRACK 규칙으로 성능 최적화
즉, 핵심 구성 요소 요약하면 다음과 같습니다.
네트워크 구성
- Control Plane: 192.168.10.100
- Worker Node: 192.168.10.101
- Pod CIDR: 10.244.0.0/16 (노드별 분할)
- Service CIDR: 10.96.0.0/16
실행 중인 서비스
- Cilium Agent: 각 노드에서 실행
- Hubble UI: NodePort 30003
- Prometheus: NodePort 30001
- Grafana: NodePort 30002
- CoreDNS: 클러스터 DNS 서비스
성공 지표
- 모든 노드 Ready 상태
- 모든 파드 Running 상태
- Cilium kubernetes IPAM 모드 정상 동작
- iptables 규칙 최적화 적용
- 외부 접근 가능한 모니터링 스택
IPAM(IP Address Manager)
IPAM(IP Address Manager)는 Kubernetes 클러스터에서 Pod IP 주소를 관리하는 핵심 컴포넌트입니다. 기본적으로 Kubernetes Control Plane의 kube-controller-manager가 클러스터 CIDR을 관리하며, 각 노드별로 Pod CIDR을 분할하여 할당합니다.
기본 동작 방식:
- kube-controller-manager: 클러스터 전체의 Pod CIDR(10.244.0.0/16)을 관리
- 노드별 CIDR 할당: 각 노드에 대해 Pod CIDR의 일부를 할당 (예: k8s-ctr → 10.244.0.0/24, k8s-w1 → 10.244.1.0/24)
- CNI 플러그인: 할당된 CIDR 내에서 개별 Pod IP를 동적으로 할당
Cilium IPAM 모드:
- kubernetes: 기본 모드로 kube-controller-manager의 CIDR 할당을 사용
- cluster-pool: Cilium이 직접 CIDR을 관리하여 더 유연한 IP 할당 제공
- multi-pool: 베타 기능으로 노드별 다중 CIDR 지원
| Feature | Kubernetes Host Scope | Cluster Scope (default) | Multi-Pool (Beta) | CRD-backed | AWS ENI |
|---|---|---|---|---|---|
| Tunnel routing | ✅ | ✅ | ❌ | ❌ | ❌ |
| Direct routing | ✅ | ✅ | ✅ | ✅ | ✅ |
| CIDR Configuration | Kubernetes | Cilium | Cilium | External | External (AWS) |
| Multiple CIDRs per cluster | ❌ | ✅ | ✅ | N/A | N/A |
| Multiple CIDRs per node | ❌ | ❌ | ✅ | N/A | N/A |
| Dynamic CIDR/IP allocation | ❌ | ❌ | ✅ | ✅ | ✅ |
기존 클러스터의 IPAM 모드를 변경하게되면 연결 중단이 발생할 수 있기 때문에 주의해야 합니다.
스터디에서는 Cluster Scope를 추천했습니다.
클러스터 정보 확인
# 클러스터 정보 확인
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
# ipam 모드 확인
cilium config view | grep ^ipam
# 노드별 파드에 할당되는 IPAM(PodCIDR) 정보 확인
# --allocate-node-cidrs=true 로 설정된 kube-controller-manager에서 CIDR을 자동 할당함
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
kc describe pod -n kube-system kube-controller-manager-k8s-ctr
kubectl get ciliumnode -o json | grep podCIDRs -A2
# 파드 정보 : 상태, 파드 IP 확인
kubectl get ciliumendpoints.cilium.io -A
✅ 실행 결과 요약
1. 클러스터 CIDR 설정 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# 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",
- Pod CIDR: 10.244.0.0/16
- Service CIDR: 10.96.0.0/16
2. Cilium IPAM 모드 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^ipam
ipam kubernetes
ipam-cilium-node-update-rate 15s
- 현재 kubernetes IPAM 모드 사용
- 노드 업데이트 주기: 15초
3. 노드별 Pod CIDR 할당
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
k8s-ctr 10.244.0.0/24
k8s-w1 10.244.1.0/24
- k8s-ctr: 10.244.0.0/24 (256개 IP)
- k8s-w1: 10.244.1.0/24 (256개 IP)
4. kube-controller-manager 설정 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kc describe pod -n kube-system kube-controller-manager-k8s-ctr
--allocate-node-cidrs=true
--cluster-cidr=10.244.0.0/16 #kube controller mamager에서 cidr 를 사용한다.
--service-cluster-ip-range=10.96.0.0/16
--allocate-node-cidrs=true: 노드별 CIDR 자동 할당 활성화--cluster-cidr=10.244.0.0/16: 전체 클러스터 Pod CIDR--service-cluster-ip-range=10.96.0.0/16: 서비스 CIDR
5. Cilium 노드 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"10.244.0.0/24"
],
--
"podCIDRs": [
"10.244.1.0/24"
],
- Cilium이 각 노드의 Pod CIDR을 정확히 인식
6. Cilium 엔드포인트 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-5c69859d9-gftmc 7002 ready 10.244.0.106
cilium-monitoring prometheus-6fc896bc5d-vwg49 12874 ready 10.244.0.22
default curl-pod 44328 ready 10.244.0.167
default webpod-697b545f57-86kx5 7392 ready 10.244.0.253
default webpod-697b545f57-s4d2l 7392 ready 10.244.1.81
kube-system coredns-674b8bbfcf-gtd49 19280 ready 10.244.0.108
kube-system coredns-674b8bbfcf-wj4dk 19280 ready 10.244.0.58
local-path-storage local-path-provisioner-74f9666bc9-vb8ss 24125 ready 10.244.0.86
- 모든 파드가 k8s-ctr 노드의 CIDR(10.244.0.0/24)에서 IP 할당
- 모든 엔드포인트가 ready 상태로 정상 동작
- Security Identity가 각 파드별로 고유하게 할당됨
Kubernetes Host Scope 확인

이 그림은 Kubernetes Host Scope IPAM 모드에서 Pod IP 할당 과정을 보여줍니다.
kube-controller-manager: 클러스터 전체 Pod CIDR(10.244.0.0/16)을 관리
노드별 CIDR 할당: 각 노드에 Pod CIDR의 일부를 할당
- k8s-ctr: 10.244.0.0/24 (256개 IP)
- k8s-w1: 10.244.1.0/24 (256개 IP)
Cilium Agent: 각 노드에서 할당된 CIDR 내에서 Pod IP를 동적으로 할당
Pod 배포: 각 노드에 배포된 Pod는 해당 노드의 CIDR에서 IP를 할당받음
샘플 애플리케이션 배포
# 샘플 애플리케이션 배포
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webpod
spec:
replicas: 2
selector:
matchLabels:
app: webpod
template:
metadata:
labels:
app: webpod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- sample-app
topologyKey: "kubernetes.io/hostname"
containers:
- name: webpod
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: webpod
labels:
app: webpod
spec:
selector:
app: webpod
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
EOF
# k8s-ctr 노드에 curl-pod 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
labels:
app: curl
spec:
nodeName: k8s-ctr
containers:
- name: curl
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 배포 확인
kubectl get deploy,svc,ep webpod -owide
kubectl get endpointslices -l app=webpod
kubectl get ciliumendpoints # IP 확인
kubectl exec -it -n kube-system ds/cilium -c cilium-agent -- cilium-dbg endpoint list
# 통신 확인
kubectl exec -it curl-pod -- curl webpod | grep Hostname
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s webpod | grep Hostname; sleep 1; done'
# hubble ui 웹 접속 주소 확인 : default 네임스페이스 확인
NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo -e "http://$NODEIP:30003"
# hubble relay 포트 포워딩 실행
cilium hubble port-forward&
hubble status
# flow log 모니터링
hubble observe -f --protocol tcp --to-pod curl-pod
hubble observe -f --protocol tcp --from-pod curl-pod
hubble observe -f --protocol tcp --pod curl-pod
도
kubectl exec -it curl-pod -- curl webpod | grep Hostname
kubectl exec -it curl-pod -- curl webpod | grep Hostname
혹은
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s webpod | grep Hostname; sleep 1; done'
# tcpdump 확인 : 파드 IP 확인
tcpdump -i eth1 tcp port 80 -nn
#
tcpdump -i eth1 tcp port 80 -w /tmp/http.pcap
#
termshark -r /tmp/http.pcap
✅ 실행 결과 요약
1. 애플리케이션 배포 결과
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get deploy,svc,ep webpod -owide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/webpod 2/2 2 2 20s webpod traefik/whoami app=webpod
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/webpod ClusterIP 10.96.41.47 <none> 80/TCP 20s app=webpod
NAME ENDPOINTS AGE
endpoints/webpod 10.244.0.75:80,10.244.1.154:80 20s
- webpod 배포 성공: 2개 레플리카 모두 Ready 상태
- Service 생성: ClusterIP 10.96.41.47로 로드밸런싱 제공
- Endpoint 확인: 두 파드가 서로 다른 노드에 배포됨
- k8s-ctr: 10.244.0.75:80
- k8s-w1: 10.244.1.154:80
2. EndpointSlice 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get endpointslices -l app=webpod
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
webpod-d5j4f IPv4 80 10.244.0.75,10.244.1.154 24s
- EndpointSlice 생성: 두 파드의 엔드포인트가 정상적으로 등록됨
- 로드밸런싱 준비: Service가 두 파드에 대한 트래픽 분산 가능
3. Cilium 엔드포인트 정보
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints
NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
curl-pod 10008 ready 10.244.0.106
webpod-697b545f57-8wv9n 58965 ready 10.244.0.75
webpod-697b545f57-v6kxr 58965 ready 10.244.1.154
- IP 할당 확인: 각 파드가 해당 노드의 CIDR에서 IP 할당받음
- curl-pod: 10.244.0.106 (k8s-ctr 노드)
- webpod-8wv9n: 10.244.0.75 (k8s-ctr 노드)
- webpod-v6kxr: 10.244.1.154 (k8s-w1 노드)
4. 통신 테스트 결과
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
Hostname: webpod-697b545f57-8wv9n
- Service 통신 성공: curl-pod에서 webpod Service로 정상 통신
- 로드밸런싱 동작: Service가 두 파드 중 하나로 트래픽 전달
5. Hubble 모니터링 결과
(⎈|HomeLab:N/A) root@k8s-ctr:~# hubble status
Healthcheck (via localhost:4245): Ok
Current/Max Flows: 4,313/8,190 (52.66%)
Flows/s: 66.85
Connected Nodes: 2/2
- Hubble 정상 동작: 2개 노드 모두 연결됨
6. 네트워크 플로우 분석
Aug 1 16:15:21.485: default/curl-pod:32834 (ID:10008) -> default/webpod-697b545f57-v6kxr:80 (ID:58965) to-endpoint FORWARDED (TCP Flags: SYN)
Aug 1 16:15:21.485: default/curl-pod:32834 (ID:10008) <- default/webpod-697b545f57-v6kxr:80 (ID:58965) to-network FORWARDED (TCP Flags: SYN, ACK)
- 노드 간 통신: k8s-ctr의 curl-pod → k8s-w1의 webpod 통신 성공
- TCP 핸드셰이크: SYN, SYN-ACK, ACK 정상 교환
- Cilium 라우팅: 노드 간 패킷이 Cilium을 통해 정상 라우팅됨
7. 패킷 캡처 결과
01:15:38.958138 IP 10.244.0.106.34592 > 10.244.1.154.80: Flags [S], seq 2199527423, win 64240, options [mss 1460,sackOK,TS val 1276054724 ecr 0,nop,wscale 7], length 0
01:15:38.958769 IP 10.244.1.154.80 > 10.244.0.106.34592: Flags [S.], seq 3181100303, ack 2199527424, win 65160, options [mss 1460,sackOK,TS val 1392236782 ecr 1276054724,nop,wscale 7], length 0
- 실제 패킷 확인: 노드 간 실제 IP 패킷 교환 확인
- HTTP 통신: GET / HTTP/1.1 요청과 HTTP/1.1 200 OK 응답
- TCP 연결: 정상적인 TCP 연결 수립 및 종료
8. termshark로 확인

Cluster Scope로 변경

Cluster scope는 CIDR를 Cilium Operator가 관리하게 됩니다.
따라서 이 모드는 Kubernetes 호스트 범위 모드와 유사합니다.
차이점은 Kubernetes가 Kubernetes v1.Node 리소스를 통해 노드별 PodCIDR을 할당하는 대신, Cilium 운영자가 v2.CiliumNode 리소스(CRD)를 통해 노드별 PodCIDR을 관리한다는 점입니다.
이 모드의 장점은 Kubernetes가 노드별 PodCIDR을 나눠주도록 구성되는 것에 의존하지 않는다는 점입니다.
최소 마스크 길이는 /30이며 권장 최소 마스크 길이는 /29 이상입니다. 2개 주소는 예약됨(네트워크, 브로드캐스트 주소)10.0.0.0/8 is the default pod CIDR. clusterPoolIPv4PodCIDRList
Cluster Scope 변경 실습
# 반복 요청 해두기
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s webpod | grep Hostname; sleep 1; done'
# Cluster Scopre 로 설정 변경
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-value
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16
kubectl -n kube-system rollout restart deploy/cilium-operator # 오퍼레이터 재시작 필요
kubectl -n kube-system rollout restart ds/cilium
# 변경 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
kubectl get ciliumnode -o json | grep podCIDRs -A2
kubectl get ciliumendpoints.cilium.io -A
✅ 실행 결과 요약
1. Helm 업그레이드 성공
(⎈|HomeLab:N/A) root@k8s-ctr:~# helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16
Release "cilium" has been upgraded. Happy Helming!
NAME: cilium
LAST DEPLOYED: Sat Aug 2 01:37:30 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 3
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.
Your release version is 1.18.0.
- 업그레이드 성공: Cilium 1.18.0으로 정상 업그레이드
- 설정 적용: Cluster Scope IPAM 모드로 변경 완료
2. Cilium 컴포넌트 재시작
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system rollout restart deploy/cilium-operator
deployment.apps/cilium-operator restarted
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system rollout restart ds/cilium
daemonset.apps/cilium restarted
3. 노드별 Pod CIDR 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
k8s-ctr 10.244.0.0/24
k8s-w1 10.244.1.0/24
- 기존 CIDR 유지: Kubernetes가 할당한 Pod CIDR가 그대로 유지되는 것을 확인
- k8s-ctr: 10.244.0.0/24
- k8s-w1: 10.244.1.0/24
4. Cilium 노드 정보 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"10.244.0.0/24"
],
--
"podCIDRs": [
"10.244.1.0/24"
],
- Cilium 인식: Cilium이 각 노드의 Pod CIDR가 이전에 설정된 상황이라 새로 세팅한 172.20.0.0/16 대역으로 할당 되지 않음
5. Cilium 엔드포인트 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-5c69859d9-gftmc 7002 ready 10.244.0.106
cilium-monitoring prometheus-6fc896bc5d-vwg49 12874 ready 10.244.0.22
default curl-pod 44328 ready 10.244.0.167
default webpod-697b545f57-86kx5 7392 ready 10.244.0.253
default webpod-697b545f57-s4d2l 7392 ready 10.244.1.81
kube-system coredns-674b8bbfcf-gtd49 19280 ready 10.244.0.108
kube-system coredns-674b8bbfcf-wj4dk 19280 ready 10.244.0.58
local-path-storage local-path-provisioner-74f9666bc9-vb8ss 24125 ready 10.244.0.86
- IP 할당 확인: 각 파드의 IP 역시 새로 할당한 대역으로 설정 되지 않음
- k8s-ctr 노드: 10.244.0.x 대역
- k8s-w1 노드: 10.244.1.x 대역
이러한 문제를 해결하기 위해서는 기존에 할당된 cilium node 정보를 삭제해 줘야 합니다.
Ciliumnode 정보를 삭제 후 재할당
kubectl delete ciliumnode k8s-w1
kubectl -n kube-system rollout restart ds/cilium
kubectl get ciliumnode -o json | grep podCIDRs -A2
kubectl get ciliumendpoints.cilium.io -A
#
kubectl delete ciliumnode k8s-ctr
kubectl -n kube-system rollout restart ds/cilium
kubectl get ciliumnode -o json | grep podCIDRs -A2
kubectl get ciliumendpoints.cilium.io -A # 파드 IP 변경 되는가?
# 노드의 poccidr static routing 자동 변경 적용 확인
ip -c route
sshpass -p 'vagrant' ssh vagrant@k8s-w1 ip -c route
# 직접 rollout restart 하자!
kubectl get pod -A -owide | grep 10.244.
#Pod의 IP가 안바뀌기 때문에, 재시작을 시켜줘야한다.
kubectl -n kube-system rollout restart deploy/hubble-relay deploy/hubble-ui
kubectl -n cilium-monitoring rollout restart deploy/prometheus deploy/grafana
kubectl rollout restart deploy/webpod
kubectl delete pod curl-pod
#
cilium hubble port-forward&
# 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
# 파드 IP 변경 확인!
kubectl get ciliumendpoints.cilium.io -A
# 반복 요청
kubectl exec -it curl-pod -- sh -c 'while true; do curl -s webpod | grep Hostname; sleep 1; done'
✅ 실행 결과 요약
1. CiliumNode CIDR 할당 완료
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"172.20.2.0/24"
],
--
"podCIDRs": [
"172.20.1.0/24"
],
- 새로운 CIDR 적용: Cluster Scope 모드에서 설정한 172.20.0.0/16 대역으로 완전히 변경
- 노드별 분할: k8s-ctr (172.20.2.0/24), k8s-w1 (172.20.1.0/24)
2. 파드 IP 할당 완료
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-ff767b4b6-p85nb 7002 ready 172.20.1.253
cilium-monitoring prometheus-6b6cbb7997-nvkw5 12874 ready 172.20.1.67
default curl-pod 44328 ready 172.20.2.167
default webpod-5b576b7db-597jh 7392 ready 172.20.2.169
default webpod-5b576b7db-gm5s7 7392 ready 172.20.1.58
kube-system coredns-674b8bbfcf-49grd 19280 ready 172.20.2.60
kube-system coredns-674b8bbfcf-fjqnb 19280 ready 172.20.1.28
kube-system hubble-relay-6f6686b9d-b656h 53799 ready 172.20.1.174
kube-system hubble-ui-645fd49bc7-jld4j 11625 ready 172.20.1.78
- 모든 파드 새 IP: 기존 10.244.x.x → 새로운 172.20.x.x 대역으로 완전히 변경
- 노드별 분산: k8s-ctr 노드 파드들은 172.20.2.x, k8s-w1 노드 파드들은 172.20.1.x 사용
- 모든 파드 정상: 모든 엔드포인트가 ready 상태로 정상 동작
3. 라우팅 테이블 업데이트
(⎈|HomeLab:N/A) root@k8s-ctr:~# 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.0.0/16 via 192.168.10.200 dev eth1 proto static
172.20.1.0/24 via 192.168.10.101 dev eth1 proto kernel
172.20.2.60 dev lxc4aba2e3dd4e1 proto kernel scope link
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100
- 새로운 라우팅 규칙: 172.20.1.0/24와 172.20.2.0/24 대역에 대한 라우팅 자동 추가
- 노드 간 통신: k8s-ctr에서 k8s-w1으로의 라우팅이 192.168.10.101을 통해 설정
4. 파드 재시작 과정
# 기존 파드들 (10.244.x.x 대역 사용 중)
kubectl get pod -A -owide | grep 10.244.
cilium-monitoring grafana-5c69859d9-gftmc 0/1 Running 0 16m 10.244.0.106 k8s-ctr <none> <none>
cilium-monitoring prometheus-6fc896bc5d-vwg49 1/1 Running 0 16m 10.244.0.22 k8s-ctr <none> <none>
default curl-pod 1/1 Running 0 13m 10.244.0.167 k8s-ctr <none> <none>
default webpod-697b545f57-86kx5 1/1 Running 0 13m 10.244.0.253 k8s-ctr <none> <none>
default webpod-697b545f57-s4d2l 1/1 Running 0 13m 10.244.1.81 k8s-w1 <none> <none>
# 파드 재시작 명령어 실행
kubectl -n kube-system rollout restart deploy/hubble-relay deploy/hubble-ui
kubectl -n cilium-monitoring rollout restart deploy/prometheus deploy/grafana
kubectl rollout restart deploy/webpod
kubectl delete pod curl-pod
- 기존 파드 상태: 10.244.x.x 대역에서 IP 할당받은 상태
- 재시작 필요성: 새로운 CIDR 대역 적용을 위해 파드 재시작 필요
5. 새로운 파드 배포
# 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
# 정상 할당 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-ff767b4b6-p85nb 7002 ready 172.20.1.253
cilium-monitoring prometheus-6b6cbb7997-nvkw5 12874 ready 172.20.1.67
default curl-pod 44328 ready 172.20.2.167
default webpod-5b576b7db-597jh 7392 ready 172.20.2.169
default webpod-5b576b7db-gm5s7 7392 ready 172.20.1.58
kube-system coredns-674b8bbfcf-49grd 19280 ready 172.20.2.60
kube-system coredns-674b8bbfcf-fjqnb 19280 ready 172.20.1.28
kube-system hubble-relay-6f6686b9d-b656h 53799 ready 172.20.1.174
kube-system hubble-ui-645fd49bc7-jld4j 11625 ready 172.20.1.78
- 새로운 파드: 172.20.2.167 IP 할당 (새로운 CIDR 대역)
- 정상 배포: 새로운 CIDR 대역에서 정상적으로 IP 할당 및 동작
Routing
라우팅 방식은 크게 Encapsulation 방식과, Native-Routing 방식으로 나뉩니다.
1. Encapsulation 방식
- Cilium 기본 네트워킹 인프라에서 요구 사항이 가장 적은 모드이기 때문에 Encapsulation 모드에서 자동으로 실행됩니다.
- 이 모드에서는 모든 클러스터 노드가 UDP 기반 캡슐화 프로토콜 VXLAN 또는 Geneve를 사용하여 터널의 메시를 형성합니다.
- VXLAN (Virtual Extensible LAN):
- 가상 확장 가능한 LAN을 만드는 프로토콜
- 기존 이더넷 프레임을 UDP 패킷으로 캡슐화
- 대규모 클라우드 환경에서 널리 사용되는 표준 프로토콜
- 24비트 VNI(Virtual Network Identifier)로 최대 16M개의 가상 네트워크 지원
- Geneve (Generic Network Virtualization Encapsulation):
- VXLAN의 개선된 버전으로 더 유연한 메타데이터 지원
- 가변 길이 옵션 필드로 다양한 정보 전송 가능
- Cilium의 보안 정책, 라벨 등의 메타데이터를 효율적으로 전송
- 더 나은 확장성과 유연성 제공
- VXLAN (Virtual Extensible LAN):
- Cilium 노드 간의 모든 트래픽이 캡슐화됩니다.
- 캡슐화는 일반 노드 간 연결에 의존합니다. 이는 Cilium 노드가 이미 서로 연결될 수 있다면 모든 라우팅 요구 사항이 이미 충족된다는 것을 의미합니다.
- 기본 네트워크는 IPv4를 지원해야 합니다. 기본 네트워크와 방화벽은 캡슐화된 패킷을 허용해야 합니다:
- VXLAN (default) : UDP 8472
- Geneve : UDP 6081
Encapsulation 방식의 장점
- 단순성 Simplicity
- 클러스터 노드를 연결하는 네트워크는 PodCIDR을 인식할 필요가 없습니다.
- 클러스터 노드는 여러 라우팅 또는 링크 계층 도메인을 생성할 수 있습니다.
- 클러스터 노드가 IP/UDP를 사용하여 서로 연결할 수 있는 한 기본 네트워크의 토폴로지는 중요하지 않습니다.
- 정체성 맥락 Identity context
- 캡슐화 프로토콜은 네트워크 패킷과 함께 메타데이터를 전송할 수 있게 해줍니다.
- Cilium은 소스 보안 ID와 같은 메타데이터를 전송하는 이 기능을 활용합니다.
- The identity transfer is an optimization designed to avoid one identity lookup on the remote node.
Encapulation 방식의 단점- MTU Overhead
- 캡슐화 헤더를 추가하면 페이로드에 사용할 수 있는 유효 MTU가 네이티브 라우팅(VXLAN의 경우 네트워크 패킷당 50바이트)보다 낮아집니다.
- 이로 인해 특정 네트워크 연결에 대한 최대 처리량이 낮아집니다.
- 점보 프레임(1500바이트당 오버헤드 50바이트 대 9000바이트당 오버헤드 50바이트)을 활성화함으로써 크게 완화될 수 있습니다.
- 설정
tunnel-protocol: Set the encapsulation protocol tovxlanorgeneve, defaults tovxlan.tunnel-port: Set the port for the encapsulation protocol. Defaults to8472forvxlanand6081forgeneve.
2. Native-Routing 방식

이 그림은 Native Routing 방식에서 라우팅 테이블이 어떻게 구성되는지를 보여줍니다.
- 라우팅 테이블 확장: 각 노드가 다른 노드의 Pod CIDR을 인식
- 직접 통신: 파드 간 통신이 네트워크 인프라를 통해 직접 이루어짐
- 캡슐화 없음: VXLAN/Geneve 같은 터널링 없이 직접 라우팅(Cilium이 실행되는 네트워크의 라우팅 기능을 활용합니다.)
- 네이티브 라우팅 데이터 경로는 라우팅 모드에서 네이티브로 활성화되며 네이티브 패킷 전달 모드를 활성화합니다.
- 네이티브 라우팅 모드에서는 Cilium이 다른 로컬 엔드포인트로 주소 지정되지 않은 모든 패킷을 Linux 커널의 라우팅 하위 시스템에 위임합니다.
- 이는 패킷이 로컬 프로세스가 패킷을 방출한 것처럼 라우팅된다는 것을 의미합니다.
- 따라서 클러스터 노드를 연결하는 네트워크는 PodCIDR을 라우팅할 수 있어야 합니다.
- PodCIDR 라우팅 방안 1
- 각 개별 노드는 다른 모든 노드의 모든 포드 IP를 인식하고 이를 표현하기 위해 Linux 커널 라우팅 테이블에 삽입됩니다.
- 모든 노드가 단일 L2 네트워크를 공유하는 경우
auto-direct-node-routes: true하여 이 문제를 해결할 수 있습니다. - 그렇지 않으면 BGP 데몬과 같은 추가 시스템 구성 요소를 실행하여 경로를 배포해야 합니다.
- PodCIDR 라우팅 방안 2
- 노드 자체는 모든 포드 IP를 라우팅하는 방법을 모르지만 다른 모든 포드에 도달하는 방법을 아는 라우터가 네트워크에 존재합니다.
- 이 시나리오에서는 Linux 노드가 이러한 라우터를 가리키는 기본 경로를 포함하도록 구성됩니다.
routing-mode: native: Enable native routing mode.ipv4-native-routing-cidr: x.x.x.x/y: Set the CIDR in which native routing can be performed.auto-direct-node-routes: true: 동일 L2 네트워크 공유 시, 걱 노드의 PodCIDR에 대한 Linux 커널 라우팅 테이블에 삽입.
Native Routing 확인
# 31. 현재 파드 배포 상태 확인
kubectl get pod -owide
# 2. Webpod 파드 IP 주소 확인
export WEBPODIP1=$(kubectl get -l app=webpod pods --field-selector spec.nodeName=k8s-ctr -o jsonpath='{.items[0].status.podIP}')
export WEBPODIP2=$(kubectl get -l app=webpod pods --field-selector spec.nodeName=k8s-w1 -o jsonpath='{.items[0].status.podIP}')
echo $WEBPODIP1 $WEBPODIP2
# 3. 노드 간 통신 테스트
# curl-pod에서 k8s-w1 노드의 webpod로 ping 테스트 (Native Routing 동작 확인)
kubectl exec -it curl-pod -- ping $WEBPODIP2
# 4. 라우팅 테이블 확인
# 각 노드의 라우팅 테이블에서 다른 노드의 Pod CIDR이 추가되었는지 확인
ip -c route
sshpass -p 'vagrant' ssh vagrant@k8s-w1 ip -c route
# 5. Hubble 모니터링 설정
# Hubble UI 포트포워딩 및 실시간 플로우 모니터링
cilium hubble port-forward&
hubble observe -f --pod curl-pod
# 6. 네트워크 패킷 캡처
# ICMP 패킷을 실시간으로 모니터링 (ping 테스트 시 패킷 확인)
tcpdump -i eth1 icmp
# 7. 패킷 캡처 및 분석
# ICMP 패킷을 파일로 저장하고 termshark로 시각적 분석
tcpdump -i eth1 icmp -w /tmp/icmp.pcap
termshark -r /tmp/icmp.pcap
✅ 실행 결과 요약
1. 라우팅 테이블 확인
# Control Plane 노드 라우팅 테이블
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c route
172.20.1.0/24 via 192.168.10.101 dev eth1 proto kernel # k8s-w1 노드의 Pod CIDR을 192.168.10.101로 라우팅
172.20.2.167 dev lxc99ac9a6ef232 proto kernel scope link # 로컬 파드 curl-pod의 네트워크 인터페이스
172.20.2.169 dev lxc75a79aa0074c proto kernel scope link # 로컬 파드 webpod의 네트워크 인터페이스
# Worker 노드 라우팅 테이블
root@k8s-w1:~# ip -c route
172.20.1.58 dev lxcd808a16e9344 proto kernel scope link # 로컬 파드 webpod의 네트워크 인터페이스
172.20.2.0/24 via 192.168.10.100 dev eth1 proto kernel # k8s-ctr 노드의 Pod CIDR을 192.168.10.100으로 라우팅
2. Hubble 플로우 모니터링
# curl-pod에서 webpod로 ping 테스트 시 실시간 플로우 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# hubble observe -f --pod curl-pod
Aug 1 17:15:48.550: default/curl-pod (ID:44328) -> default/webpod-5b576b7db-gm5s7 (ID:7392) to-network FORWARDED (ICMPv4 EchoRequest)
Aug 1 17:15:48.551: default/curl-pod (ID:44328) <- default/webpod-5b576b7db-gm5s7 (ID:7392) to-endpoint FORWARDED (ICMPv4 EchoReply)
- 노드 간 통신: k8s-ctr의 curl-pod(172.20.2.167) → k8s-w1의 webpod(172.20.1.58)로 ICMP 통신
- 양방향 통신: EchoRequest와 EchoReply가 정상적으로 교환됨
3. 실제 패킷 캡처
# ICMP 패킷 실시간 모니터링
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 icmp
02:16:09.030366 IP 172.20.2.167 > 172.20.1.58: ICMP echo request, id 4105, seq 185, length 64
02:16:09.030821 IP 172.20.1.58 > 172.20.2.167: ICMP echo reply, id 4105, seq 185, length 64
Masquerading

Masquerading은 공유기 내부의 IP가 외부로 나갈때 공유기의 IP로 나갈 때를 의미합니다.
쿠버네티스에서는 Pod가 클러스터 외부로 요청할 때 Node의 IP 주소로 Masquerading 됩니다.
그 이유는 포드에 사용되는 IPv4 주소는 일반적으로 RFC1918 개인 주소 블록에서 할당되므로 공개적으로 라우팅할 수 없기 때문입니다.
Cilium을 사용하게 되면 노드의 IP 주소가 이미 네트워크에서 라우팅 가능하기 때문에 클러스터를 떠나는 모든 트래픽의 소스 IP 주소를 자동으로 masquerade 합니다.
만약 masquerade 기능을 OFF 하려면, enable-ipv4-masquerade: false , enable-ipv6-masquerade: false 설정을 하면 됩니다.
- 기본 동작은 로컬 노드의 IP 할당 CIDR 내에서 모든 목적지를 제외하는 것입니다.
- 더 넓은 네트워크에서 포드 IP를 라우팅할 수 있는 경우, 해당 네트워크는
ipv4-native-routing-cidr: 10.0.0/8(또는 IPv6 주소의 경우ipv6-native-routing-cidr: fd00:/100) 옵션을 사용하여 지정할 수 있습니다. 이 경우 해당 CIDR 내의 모든 목적지는 masquerade 되지 않습니다.
(실습 환경에 이미 설정되어 있습니다.) - 구현 방식 : eBPF-based 방식과 iptables-based 방식이 있습니다.
# 실습 환경 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -c cilium-agent -- cilium status | grep Masquerading
Masquerading: BPF [eth0, eth1] 172.20.0.0/16 [IPv4: Enabled, IPv6: Disabled]
- 지정되지 않은 경우, 프로그램은 BPF NodePort 장치 감지 메커니즘에 의해 선택된 장치에 자동으로 연결됩니다.
- 이를 수동으로 변경하려면
deviceshelm 옵션을 사용하세요. - The eBPF-based masquerading can masquerade packets of the following L4 protocols: TCP, UDP, ICMP
- 기본적으로
ipv4-native-routing-cidr범위를 벗어난 IP 주소로 향하는 포드의 모든 패킷은 Masquerading되지만, 다른 (클러스터) 노드(Node IP)로 향하는 패킷은 제외됩니다. eBPF 마스커딩이 활성화되면 포드에서 클러스터 노드의 External IP로의 트래픽도 Masquerading 되지 않습니다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ipv4-native-routing-cidr
ipv4-native-routing-cidr 172.20.0.0/16
# Masquerading 되지 않고, Pod IP가 그대로 나감
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:31:47.216625 IP 172.20.2.167 > 192.168.10.101: ICMP echo request, id 4111, seq 1, length 64
02:31:47.217133 IP 192.168.10.101 > 172.20.2.167: ICMP echo reply, id 4111, seq 1, length 64
02:31:48.230420 IP 172.20.2.167 > 192.168.10.101: ICMP echo request, id 4111, seq 2, length 64
02:31:48.230893 IP 192.168.10.101 > 172.20.2.167: ICMP echo reply, id 4111, seq 2, length 64
Masquerading 실습

router : 사내망 10.10.0.0/16 대역 통신과 연결, k8s 에 join 되지 않은 (Web) 서버, loop1/loop2 dump 인터페이스 배치
현재 설정 확인
# 현재 설정 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -c cilium-agent -- cilium status | grep Masquerading
Masquerading: BPF [eth0, eth1] 172.20.0.0/16 [IPv4: Enabled, IPv6: Disabled]
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ipv4-native-routing-cidr
ipv4-native-routing-cidr 172.20.0.0/16
# 통신 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s webpod | grep Hostname
Hostname: webpod-5b576b7db-597jh
router eth1 192.168.10.200 통신 확인
# 터미널 2개 사용
[k8s-ctr] tcpdump -i eth1 icmp -nn 혹은 hubble observe -f --pod curl-pod
[router] tcpdump -i eth1 icmp -nn
# router eth1 192.168.10.200 로 ping >> IP 확인해보자!
kubectl exec -it curl-pod -- ping 192.168.10.101
kubectl exec -it curl-pod -- ping 192.168.10.200
...
---
# 터미널 2개 사용
[k8s-ctr] tcpdump -i eth1 tcp port 80 -nnq 혹은 hubble observe -f --pod curl-pod
[router] tcpdump -i eth1 tcp port 80 -nnq
# router eth1 192.168.10.200 로 curl >> IP 확인해보자!
kubectl exec -it curl-pod -- curl -s webpod
kubectl exec -it curl-pod -- curl -s webpod
kubectl exec -it curl-pod -- curl -s 192.168.10.200
✅ 실행 결과 요약
### 클러스터 내부 통신 (Pod IP가 그대로 라우팅)
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping 192.168.10.101
PING 192.168.10.101 (192.168.10.101) 56(84) bytes of data.
64 bytes from 192.168.10.101: icmp_seq=1 ttl=63 time=0.615 ms
64 bytes from 192.168.10.101: icmp_seq=2 ttl=63 time=0.447 ms
--- 192.168.10.101 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1006ms
rtt min/avg/max/mdev = 0.447/0.531/0.615/0.084 ms
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:39:29.304471 IP 172.20.2.167 > 192.168.10.101: ICMP echo request, id 4128, seq 1, length 64
02:39:29.304958 IP 192.168.10.101 > 172.20.2.167: ICMP echo reply, id 4128, seq 1, length 64
02:39:30.310325 IP 172.20.2.167 > 192.168.10.101: ICMP echo request, id 4128, seq 2, length 64
02:39:30.310739 IP 192.168.10.101 > 172.20.2.167: ICMP echo reply, id 4128, seq 2, length 64
### 클러스터 외부 통신(Masqerading 됨)
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- ping 192.168.10.200
PING 192.168.10.200 (192.168.10.200) 56(84) bytes of data.
64 bytes from 192.168.10.200: icmp_seq=1 ttl=63 time=0.715 ms
64 bytes from 192.168.10.200: icmp_seq=2 ttl=63 time=0.521 ms
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:40:54.092463 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 4134, seq 1, length 64
02:40:54.092697 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 4134, seq 1, length 64
root@router:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:40:53.692224 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 4134, seq 1, length 64
02:40:53.692236 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 4134, seq 1, length 64
router loop1/2 통신 확인
✅ 실행 결과 요약
1. 네트워크 인터페이스 및 라우팅 확인
# Control Plane 노드의 네트워크 인터페이스 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -br -c -4 addr
eth1 UP 192.168.10.100/24 # 노드의 실제 IP 주소
cilium_host@cilium_net UP 172.20.2.41/32 # Cilium 가상 인터페이스
# 외부 네트워크(10.10.0.0/16)로의 정적 라우팅 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c route | grep static
10.10.0.0/16 via 192.168.10.200 dev eth1 proto static # router(192.168.10.200)를 통해 외부 네트워크 접근
2. 외부 네트워크 통신 테스트
# curl-pod에서 router의 loop1 인터페이스(10.10.1.200)로 HTTP 요청
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.1.200
<h1>Web Server : router</h1> # 정상 응답 확인
# TCP 패킷 캡처 - Masquerading 동작 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 tcp port 80 -nnq
02:44:58.592843 IP 192.168.10.100.43024 > 10.10.1.200.80: tcp 0 # 출발지: 노드 IP(192.168.10.100)
02:44:58.593357 IP 10.10.1.200.80 > 192.168.10.100.43024: tcp 0 # 목적지: 외부 네트워크(10.10.1.200)
3. Router에서의 패킷 확인
# Router에서 동일한 패킷 확인 - Masquerading이 적용된 패킷
root@router:~# tcpdump -i eth1 tcp port 80 -nnq
02:44:58.314953 IP 192.168.10.100.43024 > 10.10.1.200.80: tcp 0 # 출발지 IP가 노드 IP로 변경됨
02:44:58.314983 IP 10.10.1.200.80 > 192.168.10.100.43024: tcp 0 # 응답도 노드 IP로 돌아옴
4. 두 번째 외부 네트워크 통신
# curl-pod에서 router의 loop2 인터페이스(10.10.2.200)로 HTTP 요청
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.2.200
<h1>Web Server : router</h1> # 정상 응답 확인
# TCP 패킷 캡처 - 다른 포트 사용
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 tcp port 80 -nnq
02:45:53.165609 IP 192.168.10.100.54428 > 10.10.2.200.80: tcp 0 # 새로운 포트(54428) 사용
02:45:53.165965 IP 10.10.2.200.80 > 192.168.10.100.54428: tcp 0 # 정상적인 응답
Cilium의 eBPF 구현
eBPF란?
- Extended Berkeley Packet Filter의 약자로, 리눅스 커널에서 안전하고 효율적으로 프로그램을 실행할 수 있게 해주는 기술입니다.
- 네트워크 패킷 처리, 시스템 콜 추적, 성능 모니터링 등 다양한 용도로 사용됩니다.
- Cilium은 eBPF를 활용하여 전통적인 iptables 기반 네트워킹보다 더 빠르고 유연한 네트워크 정책을 구현합니다.
- 커널 공간에서 직접 실행되므로 사용자 공간과 커널 공간 간의 컨텍스트 스위칭 오버헤드가 없어 성능이 우수합니다.
ip-masq-agent란?
- 쿠버네티스 환경에서 파드가 외부 네트워크로 나갈 때, 어떤 대역은 Masquerading(소스 IP를 노드 IP로 변환)하고, 어떤 대역은 변환하지 않을지 제어하는 역할을 합니다.
Cilium은 eBPF 기반 ip-masq-agent를 내장하고 있어, 고성능으로 이 기능을 제공합니다. - 주요 설정 옵션
- nonMasqueradeCIDRs
이 옵션에 지정된 CIDR 대역으로 나가는 트래픽은 Masquerading(소스 IP 변환)이 적용되지 않습니다.
예시: 사내망, ClusterMesh, Native-Routing 등에서 파드 IP 그대로 통신이 필요할 때 이 대역을 추가합니다. - masqLinkLocal / masqLinkLocalIPv6
Link-local 주소(169.254.0.0/16 등)로의 트래픽에 대해 Masquerading 적용 여부를 제어합니다.
- nonMasqueradeCIDRs
- 동작 방식
설정 파일이 비어있을 경우
ip-masq-agent는 기본적으로 다음과 같은 non-masquerade CIDR을 자동으로 적용합니다.
설정 파일에 CIDR을 추가하면 해당 대역으로 나가는 트래픽은 파드 IP가 그대로 유지되어 나가고, 그 외의 대역은 노드 IP로 변환되어 나갑니다.10.0.0.0/8 # RFC1918 사설 네트워크(Private, 내부망) 172.16.0.0/12 # RFC1918 사설 네트워크(Private, 내부망) 192.168.0.0/16 # RFC1918 사설 네트워크(Private, 내부망) 100.64.0.0/10 # CGNAT(Carrier-Grade NAT) 대역, ISP 내부망 등에서 사용 192.0.0.0/24 # IETF Protocol Assignments (특수 목적) 192.0.2.0/24 # TEST-NET-1, 문서/예제용(공개 인터넷 라우팅 불가) 192.88.99.0/24 # IPv6 to IPv4 relay(Deprecated, 과거 6to4 릴레이용) 198.18.0.0/15 # 네트워크 장비 벤치마크/테스트용(RFC2544) 198.51.100.0/24 # TEST-NET-2, 문서/예제용(공개 인터넷 라우팅 불가) 203.0.113.0/24 # TEST-NET-3, 문서/예제용(공개 인터넷 라우팅 불가) 240.0.0.0/4 # Reserved for future use(예약, 현재 사용되지 않음)
ipMasqAgent 설정
# 아래 설정값은 cilium 데몬셋 자동 재시작됨
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set ipMasqAgent.enabled=true --set ipMasqAgent.config.nonMasqueradeCIDRs='{10.10.1.0/24,10.10.2.0/24}'
cilium hubble port-forward&
# ip-masq-agent configmap 생성 확인
kubectl get cm -n kube-system ip-masq-agent -o yaml | yq
kc describe cm -n kube-system ip-masq-agent
k9s
#
cilium config view | grep -i ip-masq
enable-ip-masq-agent true
#
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg bpf ipmasq list
✅ 실행 결과 요약
(⎈|HomeLab:N/A) root@k8s-ctr:~# helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set ipMasqAgent.enabled=true --set ipMasqAgent.config.nonMasqueradeCIDRs='{10.10.1.0/24,10.10.2.0/24}'
Release "cilium" has been upgraded. Happy Helming!
NAME: cilium
LAST DEPLOYED: Sat Aug 2 02:59:28 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 5
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.
Your release version is 1.18.0.
For any further help, visit https://docs.cilium.io/en/v1.18/gettinghelp
(⎈|HomeLab:N/A) root@k8s-ctr:~# # kubectl get cm -n kube-system ip-masq-agent -o yaml | yq
{
"apiVersion": "v1",
"data": {
"config": "{\"nonMasqueradeCIDRs\":[\"10.10.1.0/24\",\"10.10.2.0/24\"]}"
},
"kind": "ConfigMap",
"metadata": {
"annotations": {
"meta.helm.sh/release-name": "cilium",
"meta.helm.sh/release-namespace": "kube-system"
router loop1/2 통신 확인
# 터미널 2개 사용
[k8s-ctr] tcpdump -i eth1 tcp port 80 -nnq 혹은 hubble observe -f --pod curl-pod
[router] tcpdump -i eth1 tcp port 80 -nnq
# router eth1 192.168.10.200 로 curl >> IP 확인
kubectl exec -it curl-pod -- curl -s 10.10.1.200
kubectl exec -it curl-pod -- curl -s 10.10.2.200
✅ 실행 결과 요약
1. Pod에서 외부 서비스로의 통신 시도
# curl-pod에서 외부 서비스(10.10.1.200, 10.10.2.200)로 통신 시도
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.1.200
^Ccommand terminated with exit code 130 # 통신 실패로 인한 강제 종료
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.2.200
^Ccommand terminated with exit code 130 # 통신 실패로 인한 강제 종료
# 통신이 안되는 이유 분석을 위한 패킷 캡처
root@router:~# tcpdump -i eth1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
03:01:12.708409 IP 172.20.2.167.42798 > 10.10.1.200.80: tcp 0 # Pod의 IP(172.20.2.167)를 그대로 사용하여 라우터로 통신이 들어옴. 하지만 응답은 없다
03:01:13.761650 IP 172.20.2.167.42798 > 10.10.1.200.80: tcp 0
03:01:39.385237 IP 172.20.2.167.47312 > 10.10.2.200.80: tcp 0
03:01:40.385621 IP 172.20.2.167.47312 > 10.10.2.200.80: tcp 0
# 라우터에 Pod CIDR 대역이 등록되어 있지 않기 때문에 엉뚱한 곳으로 응답이 가게 됨
root@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
2. 라우터에 Pod CIDR 대역 추가로 통신 해결
# 통신하려면 router에 Node별 Pod CIDR 대역을 추가해 줘야한다.
root@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
# Pod CIDR 대역을 라우터에 추가
root@router:~# ip route add 172.20.1.0/24 via 192.168.10.100 # k8s-ctr 노드의 Pod CIDR
ip route add 172.20.0.0/24 via 192.168.10.101 # k8s-w1 노드의 Pod CIDR
# 추가된 라우팅 테이블 확인
root@router:~# ip -c route | grep 172.20
172.20.0.0/24 via 192.168.10.101 dev eth1
172.20.1.0/24 via 192.168.10.100 dev eth1
# 전체 라우팅 테이블 확인
root@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
172.20.0.0/24 via 192.168.10.101 dev eth1 # 추가됨
172.20.1.0/24 via 192.168.10.100 dev eth1 # 추가됨
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.200
CoreDNS 분석
# 파드의 DNS 설정 정보 확인
kubectl exec -it curl-pod -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local # DNS 검색 도메인 설정
nameserver 10.96.0.10 # CoreDNS 서비스 IP
options ndots:5 # 도메인에 점이 5개 미만이면 검색 도메인을 추가
# kubelet의 DNS 설정 확인
cat /var/lib/kubelet/config.yaml | grep cluster -A1
clusterDNS:
- 10.96.0.10 # CoreDNS 서비스 IP
clusterDomain: cluster.local # 클러스터 도메인
# CoreDNS 서비스와 엔드포인트 확인
kubectl get svc,ep -n kube-system kube-dns
kubectl get svc,ep -n kube-system kube-dns
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 105m
NAME ENDPOINTS AGE
endpoints/kube-dns 172.20.1.28:53,172.20.2.60:53,172.20.1.28:53 + 3 more... 105m
# CoreDNS 파드 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns
NAME READY STATUS RESTARTS AGE
coredns-674b8bbfcf-49grd 1/1 Running 0 90m
coredns-674b8bbfcf-fjqnb 1/1 Running 0 91m
# CoreDNS 파드 상세 정보 확인
kc describe pod -n kube-system -l k8s-app=kube-dns
...
config-volume:
Type: ConfigMap (a volume populated by a ConfigMap)
Name: coredns
Optional: false
...
# CoreDNS 설정 확인
kc describe cm -n kube-system coredns
...
Corefile:
----
.:53 { # 모든 도메인 요청을 53포트에서 수신
errors # DNS 응답 중 에러가 발생할 경우 로그 출력
health { # health 엔드포인트를 제공하여 상태 확인 가능
lameduck 5s # 종료 시 5초간 lameduck 모드로 트래픽을 점차 줄이며 종료
}
ready # ready 엔드포인트 제공, 8181 포트의 HTTP 엔드포인트가, 모든 플러그인이 준비되었다는 신호를 보내면 200 OK 를 반환
kubernetes cluster.local in-addr.arpa ip6.arpa { # Kubernetes DNS 플러그인 설정(클러스터 내부 도메인 처리), cluster.local: 클러스터 도메인
pods insecure # 파드 IP로 DNS 조회 허용 (보안 없음)
fallthrough in-addr.arpa ip6.arpa # 해당 도메인에서 결과 없으면 다음 플러그인으로 전달
ttl 30 # 캐시 타임 (30초)
}
prometheus :9153 # Prometheus metrics 수집 가능
forward . /etc/resolv.conf { # CoreDNS가 모르는 도메인은 지정된 업스트림(보통 외부 DNS)으로 전달, .: 모든 쿼리
max_concurrent 1000 # 병렬 포워딩 최대 1000개
}
cache 30 { # DNS 응답 캐시 기능, 기본 캐시 TTL 30초
disable success cluster.local # 성공 응답 캐시 안 함 (cluster.local 도메인)
disable denial cluster.local # NXDOMAIN 응답도 캐시 안 함
}
loop # 간단한 전달 루프(loop)를 감지하고, 루프가 발견되면 CoreDNS 프로세스를 중단(halt).
reload # Corefile 이 변경되었을 때 자동으로 재적용, 컨피그맵 설정을 변경한 후에 변경 사항이 적용되기 위하여 약 2분정도 소요.
loadbalance # 응답에 대하여 A, AAAA, MX 레코드의 순서를 무작위로 선정하는 라운드-로빈 DNS 로드밸런서.
}
# 호스트의 DNS 설정 확인
cat /etc/resolv.conf
nameserver 127.0.0.53 # 호스트의 DNS 서버
options edns0 trust-ad
search .
# DNS 해석기 상태 확인
resolvectl
Link 2 (eth0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.0.2.3
DNS Servers: 10.0.2.3
# Hubble 포트포워딩 시작
cilium hubble port-forward&
# DNS 트래픽 모니터링
hubble observe -f --port 53
# UDP DNS 트래픽 모니터링
hubble observe -f --port 53 --protocol UDP
# 파드와 CoreDNS 파드의 IP 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
curl-pod 1/1 Running 0 90m 172.20.2.167 k8s-ctr <none> <none>
webpod-5b576b7db-597jh 1/1 Running 0 90m 172.20.2.169 k8s-ctr <none> <none>
webpod-5b576b7db-gm5s7 1/1 Running 0 90m 172.20.1.58 k8s-w1 <none> <none>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-674b8bbfcf-49grd 1/1 Running 0 93m 172.20.2.60 k8s-ctr <none> <none>
coredns-674b8bbfcf-fjqnb 1/1 Running 0 93m 172.20.1.28 k8s-w1 <none> <none>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
# CoreDNS를 1개로 스케일 다운하여 단일 인스턴스로 테스트
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl scale deployment -n kube-system coredns --replicas 1
deployment.apps/coredns scaled
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-674b8bbfcf-fjqnb 1/1 Running 0 94m 172.20.1.28 k8s-w1 <none> <none>
# CoreDNS 캐시 메트릭 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl kube-dns.kube-system.svc:9153/metrics | grep coredns_cache_ | grep -v ^#
coredns_cache_entries{server="dns://:53",type="denial",view="",zones="."} 1 # 캐시된 거부 응답 수
coredns_cache_entries{server="dns://:53",type="success",view="",zones="."} 0 # 캐시된 성공 응답 수
coredns_cache_misses_total{server="dns://:53",view="",zones="."} 1399 # 캐시 미스 총 수
coredns_cache_requests_total{server="dns://:53",view="",zones="."} 1399 # 캐시 요청 총 수
# DNS 트래픽 패킷 캡처
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i any udp port 53 -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
03:20:58.348590 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 25218+ [1au] A? kube-dns.kube-system.svc.default.svc.cluster.local. (91) # curl-pod에서 CoreDNS로 DNS 쿼리
03:20:58.348630 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 25218+ [1au] A? kube-dns.kube-system.svc.default.svc.cluster.local. (91) # eth1 인터페이스로 전송
03:20:58.348776 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 14268+ [1au] AAAA? kube-dns.kube-system.svc.default.svc.cluster.local. (91) # IPv6 쿼리
03:20:58.348779 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 14268+ [1au] AAAA? kube-dns.kube-system.svc.default.svc.cluster.local. (91) # IPv6 쿼리 전송
03:20:58.351066 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 25218 NXDomain*- 0/1/1 (184) # NXDomain 응답 (도메인 없음)
03:20:58.351797 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 14268 NXDomain*- 0/1/1 (184) # IPv6 NXDomain 응답
03:20:58.352061 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 46788+ [1au] A? kube-dns.kube-system.svc.svc.cluster.local. (71) # 다음 검색 도메인으로 쿼리
03:20:58.352070 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 46788+ [1au] A? kube-dns.kube-system.svc.svc.cluster.local. (71) # 전송
03:20:58.352702 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 12151+ [1au] AAAA? kube-dns.kube-system.svc.svc.cluster.local. (71) # IPv6 쿼리
03:20:58.352705 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 12151+ [1au] AAAA? kube-dns.svc.cluster.local. (71) # 전송
03:20:58.354060 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 12151 NXDomain*- 0/1/1 (164) # NXDomain 응답
03:20:58.354582 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 46788 NXDomain*- 0/1/1 (164) # NXDomain 응답
03:20:58.354923 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 38032+ [1au] A? kube-dns.kube-system.svc.cluster.local. (67) # 마지막 검색 도메인으로 쿼리
03:20:58.354937 lxc99ac9a6ef232 In IP 172.20.2.167.53309 > 172.20.1.28.53: 23275+ [1au] AAAA? kube-dns.kube-system.svc.cluster.local. (67) # IPv6 쿼리
03:20:58.354938 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 38032+ [1au] A? kube-dns.kube-system.svc.cluster.local. (67) # 전송
03:20:58.354949 eth1 Out IP 172.20.2.167.53309 > 172.20.1.28.53: 23275+ [1au] AAAA? kube-dns.kube-system.svc.cluster.local. (67) # 전송
03:20:58.355719 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 23275*- 0/1/1 (160) # NXDomain 응답
03:20:58.355719 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53309: 38032*- 1/0/1 A 10.96.0.10 (121) # 성공 응답 (CoreDNS 서비스 IP 반환)
(⎈|HomeLab:N/A) root@k8s-ctr:~# hubble observe -f --port 53
# Hubble을 통한 DNS 트래픽 모니터링
Aug 1 18:20:58.329: default/curl-pod:53309 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # curl-pod에서 CoreDNS로 DNS 쿼리 전송
Aug 1 18:20:58.331: default/curl-pod:53309 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # CoreDNS에서 curl-pod로 응답
Aug 1 18:20:58.348: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # 서비스 IP로의 DNS 쿼리 추적
Aug 1 18:20:58.348: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # 서비스 IP를 실제 파드 IP로 변환
Aug 1 18:20:58.349: default/curl-pod:53309 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # 변환된 IP로 DNS 쿼리 전송
Aug 1 18:20:58.351: default/curl-pod:53309 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # CoreDNS에서 응답 수신
Aug 1 18:20:58.351: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 응답 경로 추적
Aug 1 18:20:58.351: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 실제 파드 IP를 서비스 IP로 역변환
Aug 1 18:20:58.352: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 다음 DNS 쿼리 응답 추적
Aug 1 18:20:58.352: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 역변환
Aug 1 18:20:58.354: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 추가 DNS 쿼리 응답
Aug 1 18:20:58.354: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 역변환
Aug 1 18:20:58.354: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 마지막 DNS 쿼리 응답
Aug 1 18:20:58.354: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 역변환
Aug 1 18:20:58.355: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:44328) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 성공 응답 추적
Aug 1 18:20:58.356: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 최종 역변환
Aug 1 18:20:58.356: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # IPv6 쿼리 응답
Aug 1 18:20:58.356: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # IPv6 응답 역변환
# nslookup을 통한 DNS 쿼리 디버깅
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- nslookup -debug webpod
;; Got recursion not available from 10.96.0.10 # CoreDNS는 재귀 쿼리를 지원하지 않음
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
------------
QUESTIONS:
webpod.default.svc.cluster.local, type = A, class = IN # A 레코드 쿼리
ANSWERS:
-> webpod.default.svc.cluster.local
internet address = 10.96.43.148 # 서비스 IP 반환
ttl = 30
AUTHORITY RECORDS:
ADDITIONAL RECORDS:
------------
Name: webpod.default.svc.cluster.local
Address: 10.96.43.148
;; Got recursion not available from 10.96.0.10
------------
QUESTIONS:
webpod.default.svc.cluster.local, type = AAAA, class = IN # IPv6 AAAA 레코드 쿼리
ANSWERS:
AUTHORITY RECORDS:
-> cluster.local
origin = ns.dns.cluster.local
mail addr = hostmaster.cluster.local
serial = 1754072440
refresh = 7200
retry = 1800
expire = 86400
minimum = 30
ttl = 30
ADDITIONAL RECORDS:
------------
# webpod 서비스에 대한 DNS 쿼리 패킷 캡처
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i any udp port 53 -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
03:22:23.564808 lxc99ac9a6ef232 In IP 172.20.2.167.41437 > 172.20.1.28.53: 12174+ A? webpod.default.svc.cluster.local. (50) # curl-pod에서 CoreDNS로 webpod A 레코드 쿼리
03:22:23.564854 eth1 Out IP 172.20.2.167.41437 > 172.20.1.28.53: 12174+ A? webpod.default.svc.cluster.local. (50) # eth1 인터페이스로 전송
03:22:23.565902 eth1 In IP 172.20.1.28.53 > 172.20.2.167.41437: 12174*- 1/0/0 A 10.96.43.148 (98) # CoreDNS에서 서비스 IP(10.96.43.148) 응답
03:22:23.568907 lxc99ac9a6ef232 In IP 172.20.2.167.60047 > 172.20.1.28.53: 46335+ AAAA? webpod.default.svc.cluster.local. (50) # IPv6 AAAA 레코드 쿼리
03:22:23.569057 eth1 Out IP 172.20.2.167.60047 > 172.20.1.28.53: 46335+ AAAA? webpod.default.svc.cluster.local. (50) # 전송
03:22:23.570021 eth1 In IP 172.20.1.28.53 > 172.20.2.167.60047: 46335*- 0/1/0 (143) # AAAA 레코드 없음 응답
# webpod DNS 쿼리에 대한 Hubble 모니터링
(⎈|HomeLab:N/A) root@k8s-ctr:~# hubble observe -f --port 53
Aug 1 18:22:23.546: default/curl-pod:41437 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # A 레코드 쿼리 전송
Aug 1 18:22:23.546: default/curl-pod:41437 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # A 레코드 응답 수신
Aug 1 18:22:23.550: default/curl-pod:60047 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # AAAA 레코드 쿼리 전송
Aug 1 18:22:23.550: default/curl-pod:60047 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # AAAA 레코드 응답 수신
Aug 1 18:22:23.565: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # 서비스 IP로의 DNS 쿼리 추적
Aug 1 18:22:23.565: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # 서비스 IP를 실제 파드 IP로 변환
Aug 1 18:22:23.565: default/curl-pod:41437 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # 변환된 IP로 DNS 쿼리 전송
Aug 1 18:22:23.566: default/curl-pod:41437 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # CoreDNS에서 응답 수신
Aug 1 18:22:23.566: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 응답 경로 추적
Aug 1 18:22:23.566: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 실제 파드 IP를 서비스 IP로 역변환
Aug 1 18:22:23.568: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # AAAA 쿼리 추적
Aug 1 18:22:23.568: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # AAAA 쿼리 변환
Aug 1 18:22:23.569: default/curl-pod:60047 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # AAAA 쿼리 전송
Aug 1 18:22:23.570: default/curl-pod:60047 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # AAAA 응답 수신
Aug 1 18:22:23.570: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # AAAA 응답 경로 추적
Aug 1 18:22:23.570: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # AAAA 응답 역변환
# 외부 도메인(google.com)에 대한 DNS 쿼리 디버깅
(⎈|HomeLab:N/A) root@k8s-ctr:~#
kubectl exec -it curl-pod -- nslookup -debug google.com
;; Got recursion not available from 10.96.0.10 # CoreDNS는 재귀 쿼리를 지원하지 않음
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
------------
QUESTIONS:
google.com.default.svc.cluster.local, type = A, class = IN # 첫 번째 검색 도메인으로 쿼리
ANSWERS:
AUTHORITY RECORDS:
-> cluster.local
origin = ns.dns.cluster.local
mail addr = hostmaster.cluster.local
serial = 1754072440
refresh = 7200
retry = 1800
expire = 86400
minimum = 30
ttl = 30
ADDITIONAL RECORDS:
------------
** server can't find google.com.default.svc.cluster.local: NXDOMAIN # 도메인 없음
;; Got recursion not available from 10.96.0.10 # CoreDNS는 재귀 쿼리를 지원하지 않음
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
------------
QUESTIONS:
google.com.svc.cluster.local, type = A, class = IN # 두 번째 검색 도메인으로 쿼리
ANSWERS:
AUTHORITY RECORDS:
-> cluster.local
origin = ns.dns.cluster.local
mail addr = hostmaster.cluster.local
serial = 1754072440
refresh = 7200
retry = 1800
expire = 86400
minimum = 30
ttl = 30
ADDITIONAL RECORDS:
------------
** server can't find google.com.svc.cluster.local: NXDOMAIN # 도메인 없음
;; Got recursion not available from 10.96.0.10 # CoreDNS는 재귀 쿼리를 지원하지 않음
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
------------
QUESTIONS:
google.com.cluster.local, type = A, class = IN # 세 번째 검색 도메인으로 쿼리
ANSWERS:
AUTHORITY RECORDS:
-> cluster.local
origin = ns.dns.cluster.local
mail addr = hostmaster.cluster.local
serial = 1754072440
refresh = 7200
retry = 1800
expire = 86400
minimum = 30
ttl = 30
ADDITIONAL RECORDS:
------------
** server can't find google.com.cluster.local: NXDOMAIN # 도메인 없음
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
------------
QUESTIONS:
google.com, type = A, class = IN # 마지막으로 정확한 도메인으로 쿼리
ANSWERS:
-> google.com
internet address = 142.251.42.174 # Google의 IPv4 주소
ttl = 30
AUTHORITY RECORDS:
ADDITIONAL RECORDS:
------------
Non-authoritative answer:
Name: google.com
Address: 142.251.42.174
------------
QUESTIONS:
google.com, type = AAAA, class = IN # IPv6 AAAA 레코드 쿼리
ANSWERS:
-> google.com
has AAAA address 2404:6800:4004:827::200e # Google의 IPv6 주소
ttl = 30
AUTHORITY RECORDS:
ADDITIONAL RECORDS:
------------
Name: google.com
Address: 2404:6800:4004:827::200e
# google.com DNS 쿼리에 대한 패킷 캡처
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i any udp port 53 -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
03:23:06.595155 lxc99ac9a6ef232 In IP 172.20.2.167.46356 > 172.20.1.28.53: 35486+ A? google.com.default.svc.cluster.local. (54) # 첫 번째 검색 도메인으로 쿼리
03:23:06.595210 eth1 Out IP 172.20.2.167.46356 > 172.20.1.28.53: 35486+ A? google.com.default.svc.cluster.local. (54) # 전송
03:23:06.596734 eth1 In IP 172.20.1.28.53 > 172.20.2.167.46356: 35486 NXDomain*- 0/1/0 (147) # NXDomain 응답 (도메인 없음)
03:23:06.598947 lxc99ac9a6ef232 In IP 172.20.2.167.41342 > 172.20.1.28.53: 5115+ A? google.com.svc.cluster.local. (46) # 두 번째 검색 도메인으로 쿼리
03:23:06.598976 eth1 Out IP 172.20.2.167.41342 > 172.20.1.28.53: 5115+ A? google.com.svc.cluster.local. (46) # 전송
03:23:06.600201 eth1 In IP 172.20.1.28.53 > 172.20.2.167.41342: 5115 NXDomain*- 0/1/0 (139) # NXDomain 응답
03:23:06.604387 lxc99ac9a6ef232 In IP 172.20.2.167.55410 > 172.20.1.28.53: 8461+ A? google.com.cluster.local. (42) # 세 번째 검색 도메인으로 쿼리
03:23:06.604418 eth1 Out IP 172.20.2.167.55410 > 172.20.1.28.53: 8461+ A? google.com.cluster.local. (42) # 전송
03:23:06.605575 eth1 In IP 172.20.1.28.53 > 172.20.2.167.55410: 8461 NXDomain*- 0/1/0 (135) # NXDomain 응답
03:23:06.608833 lxc99ac9a6ef232 In IP 172.20.2.167.53335 > 172.20.1.28.53: 47927+ A? google.com. (28) # 정확한 도메인으로 쿼리
03:23:06.608955 eth1 Out IP 172.20.2.167.53335 > 172.20.1.28.53: 47927+ A? google.com. (28) # 전송
03:23:06.638799 eth1 In IP 172.20.1.28.53 > 172.20.2.167.53335: 47927 1/0/0 A 142.251.42.174 (54) # Google IPv4 주소 응답
03:23:06.642693 lxc99ac9a6ef232 In IP 172.20.2.167.40569 > 172.20.1.28.53: 12635+ AAAA? google.com. (28) # IPv6 AAAA 레코드 쿼리
03:23:06.642796 eth1 Out IP 172.20.2.167.40569 > 172.20.1.28.53: 12635+ AAAA? google.com. (28) # 전송
03:23:06.657651 eth1 In IP 172.20.1.28.53 > 172.20.2.167.40569: 12635 1/0/0 AAAA 2404:6800:4004:827::200e (66) # Google IPv6 주소 응답
# google.com DNS 쿼리에 대한 Hubble 모니터링
(⎈|HomeLab:N/A) root@k8s-ctr:~# hubble observe -f --port 53
Aug 1 18:23:06.576: default/curl-pod:46356 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # 첫 번째 검색 도메인 쿼리 전송
Aug 1 18:23:06.577: default/curl-pod:46356 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # NXDomain 응답 수신
Aug 1 18:23:06.580: default/curl-pod:41342 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # 두 번째 검색 도메인 쿼리 전송
Aug 1 18:23:06.580: default/curl-pod:41342 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # NXDomain 응답 수신
Aug 1 18:23:06.585: default/curl-pod:55410 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # 세 번째 검색 도메인 쿼리 전송
Aug 1 18:23:06.586: default/curl-pod:55410 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # NXDomain 응답 수신
Aug 1 18:23:06.590: default/curl-pod:53335 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # 정확한 도메인 쿼리 전송
Aug 1 18:23:06.595: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # 서비스 IP로의 DNS 쿼리 추적
Aug 1 18:23:06.595: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # 서비스 IP를 실제 파드 IP로 변환
Aug 1 18:23:06.595: default/curl-pod:46356 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # 변환된 IP로 DNS 쿼리 전송
Aug 1 18:23:06.597: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 응답 경로 추적
Aug 1 18:23:06.597: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 실제 파드 IP를 서비스 IP로 역변환
Aug 1 18:23:06.597: default/curl-pod:46356 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # CoreDNS에서 응답 수신
Aug 1 18:23:06.598: 10.0.2.3:53 (world) <> kube-system/coredns-674b8bbfcf-fjqnb (ID:19280) pre-xlate-rev TRACED (UDP) # 외부 DNS 서버와의 통신 추적
Aug 1 18:23:06.598: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # 다음 DNS 쿼리 추적
Aug 1 18:23:06.598: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # 변환
Aug 1 18:23:06.599: default/curl-pod:41342 (ID:44328) -> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-network FORWARDED (UDP) # 두 번째 쿼리 전송
Aug 1 18:23:06.600: default/curl-pod:41342 (ID:44328) <- kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) to-endpoint FORWARDED (UDP) # 응답 수신
Aug 1 18:23:06.600: kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) <> default/curl-pod (ID:44328) pre-xlate-rev TRACED (UDP) # 응답 경로 추적
Aug 1 18:23:06.600: 10.96.0.10:53 (world) <> default/curl-pod (ID:44328) post-xlate-rev TRANSLATED (UDP) # 역변환
Aug 1 18:23:06.600: 10.0.2.3:53 (world) <> kube-system/coredns-674b8bbfcf-fjqnb (ID:19280) pre-xlate-rev TRACED (UDP) # 외부 DNS 서버와의 통신
Aug 1 18:23:06.600: 10.0.2.3:53 (world) <> kube-system/coredns-674b8bbfcf-fjqnb (ID:19280) pre-xlate-rev TRACED (UDP) # 추가 외부 DNS 통신
Aug 1 18:23:06.601: kube-system/coredns-674b8bbfcf-fjqnb:59851 (ID:19280) -> 10.0.2.3:53 (world) to-network FORWARDED (UDP) # CoreDNS에서 외부 DNS로 쿼리 전송
Aug 1 18:23:06.601: 10.0.2.3:53 (world) <> kube-system/coredns-674b8bbfcf-fjqnb (ID:19280) pre-xlate-rev TRACED (UDP) # 외부 DNS 응답 추적
Aug 1 18:23:06.601: 10.0.2.3:53 (world) <> kube-system/coredns-674b8bbfcf-fjqnb (ID:19280) pre-xlate-rev TRACED (UDP) # 추가 외부 DNS 응답
Aug 1 18:23:06.604: default/curl-pod (ID:44328) <> 10.96.0.10:53 (world) pre-xlate-fwd TRACED (UDP) # 성공 응답 추적
Aug 1 18:23:06.604: default/curl-pod (ID:44328) <> kube-system/coredns-674b8bbfcf-fjqnb:53 (ID:19280) post-xlate-fwd TRANSLATED (UDP) # 성공 응답 변환
# 캐시 내용 확인
(⎈|HomeLab:N/A) root@k8s-ctr:~#
kubectl exec -it curl-pod -- curl kube-dns.kube-system.svc:9153/metrics | grep coredns_cache_ | grep -v ^#
coredns_cache_entries{server="dns://:53",type="denial",view="",zones="."} 1 # 캐시된 거부 응답 수
coredns_cache_entries{server="dns://:53",type="success",view="",zones="."} 0 # 캐시된 성공 응답 수
coredns_cache_misses_total{server="dns://:53",view="",zones="."} 1413 # 캐시 미스 총 수
coredns_cache_requests_total{server="dns://:53",view="",zones="."} 1413 # 캐시 요청 총 수
NodeLocalDNS란 (대규모 환경에서 사용!)

모든 Pod들의 요청이 CoreDNS를 거치게 되면 IP Table의 부하가 발생할 수 있으므로, 이를 해결하기 위한 방법으로 Node Local DNS cache를 사용할 수 있습니다.
NodeLocalDNS의 개념과 장점:
NodeLocal DNSCache는 클러스터 노드에서 DNS 캐싱 에이전트를 DaemonSet으로 실행하여 클러스터 DNS 성능을 향상시킵니다.- 현재 아키텍처에서 '
ClusterFirst' DNS 모드의 Pod들은 DNS 쿼리를 위해 kube-dns 서비스 IP에 접근합니다. - 이는 kube-proxy에 의해 추가된 iptables 규칙을 통해 kube-dns/CoreDNS 엔드포인트로 변환됩니다.
- 이 새로운 아키텍처를 통해 Pod들은 동일한 노드에서 실행되는 DNS 캐싱 에이전트에 접근하여 iptables DNAT 규칙과 연결 추적을 우회할 수 있습니다.
- 로컬 캐싱 에이전트는 클러스터 호스트 이름(기본적으로 "cluster.local" 접미사)의 캐시 누락에 대해 kube-dns 서비스에 쿼리합니다.
- 현재 DNS 아키텍처에서는 로컬 kube-dns/CoreDNS 인스턴스가 없는 경우 DNS QPS가 가장 높은 Pod가 다른 노드에 접근해야 할 수도 있습니다. 로컬 캐시를 사용하면 이러한 시나리오에서 지연 시간을 개선하는 데 도움이 됩니다.
- 성능 개선 효과: ptables DNAT 및 연결 추적을 우회하면 연결 추적 레이스 컨디션을 줄이고 UDP DNS 항목이 연결 추적 테이블을 채우는 것을 방지하는 데 도움이 됩니다.
- 로컬 캐싱 에이전트에서 kube-dns 서비스로의 연결은 TCP로 업그레이드할 수 있습니다. TCP 연결 추적 항목은 시간 초과가 필요한 UDP 항목과 달리 연결 종료 시 제거됩니다(기본값
nf_conntrack_udp_timeout은 30초). - DNS 쿼리를 UDP에서 TCP로 업그레이드하면 삭제된 UDP 패킷과 DNS 타임아웃으로 인한 꼬리 지연 시간이 보통 최대 30초(3회 재시도 + 10초 타임아웃)까지 줄어듭니다. 노드 로컬 캐시가 UDP DNS 쿼리를 수신하므로 애플리케이션을 변경할 필요가 없습니다.
- 네거티브 캐싱을 다시 활성화하여 kube-dns 서비스에 대한 쿼리 수를 줄일 수 있습니다.
설치 방법
- NodeLocal DNSCache의 로컬 수신 IP 주소는 클러스터의 기존 IP와 충돌하지 않도록 보장할 수 있는 모든 주소일 수 있습니다.
- 예를 들어, IPv4의 '링크-로컬' 범위 '
169.254.0.0/16' 또는 IPv6 'fd00::/8'의 '유니크 로컬 주소' 범위와 같은 로컬 범위의 주소를 사용하는 것이 좋습니다. - 매니페스트의 변수를 올바른 값으로 대체:
- kube-proxy가 IPTABLES 모드에서 실행 중인 경우:
__PILLAR__CLUSTER__DNS__and__PILLAR__UPSTREAM__SERVERS__는node-local-dns포드에 의해 채워집니다.- 이 모드에서는
node-local-dnsPod가 kube-dns 서비스 IP와 를 모두 수신하므로, Pod는 IP 주소 중 하나를 사용하여 DNS 레코드를 조회할 수 있습니다.
# 매니페스트 파일의 변수들을 실제 값으로 치환 sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml- kube-proxy가 IPVS 모드에서 실행 중인 경우:
- 이 모드에서는
node-local-dnsPod가 에서만 수신합니다. - IPVS 로드 밸런싱에 사용되는 인터페이스가 이미 이 주소를 사용하고 있기 때문에
node-local-dns인터페이스는 kube-dns 클러스터 IP를 바인딩할 수 없습니다. __PILLAR__UPSTREAM__SERVERS__는node-local-dnsPod에 의해 채워집니다.
- 이 모드에서는
# IPVS 모드용 변수 치환 sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/,__PILLAR__DNS__SERVER__//g; s/__PILLAR__CLUSTER__DNS__/$kubedns/g" nodelocaldns.yaml
- kube-proxy가 IPTABLES 모드에서 실행 중인 경우:
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}` # CoreDNS의 ClusterIP domain=<cluster-domain> # 보통 기본값 cluster.local 사용 localdns=<node-local-address> # NodeLocal DNSCache를 위해 선택된 로컬 수신 IP 주소- 배포
kubectl create -f nodelocaldns.yaml node-local-dnsPod가 활성화되면 각 클러스터 노드의kube-system네임스페이스에서 실행됩니다.
NodeLocalDNS 실습
# iptables 확인
iptables-save | tee before.txt
#
wget https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
# kubedns 는 coredns 서비스의 ClusterIP를 변수 지정
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain='cluster.local' ## default 값
localdns='169.254.20.10' ## default 값 -> Node Local DNS 로 Pod가 확인하고 없으면 Core DNS 로 가게끔
echo $kubedns $domain $localdns
# iptables 모드 사용 중으로 아래 명령어 수행
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml
# nodelocaldns 설치
kubectl apply -f nodelocaldns.yaml
#
kubectl get pod -n kube-system -l k8s-app=node-local-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-local-dns-9xrv9 1/1 Running 0 41s 192.168.10.100 k8s-ctr <none> <none>
#
kubectl edit cm -n kube-system node-local-dns # 'cluster.local' 과 '.:53' 에 log, debug 추가
kubectl -n kube-system rollout restart ds node-local-dns
kubectl describe cm -n kube-system node-local-dns
...
cluster.local:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health 169.254.20.10:8080
}
...
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
...
# iptables 확인 : 규칙 업데이트까지 다소 시간 소요!
iptables-save | tee after.txt
diff before.txt after.txt
##
iptables -t filter -S | grep -i dns
-A INPUT -d 10.96.0.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 10.96.0.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 169.254.20.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 169.254.20.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 10.96.0.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 10.96.0.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 169.254.20.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 169.254.20.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
##
iptables -t raw -S | grep -i dns
-A PREROUTING -d 10.96.0.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 10.96.0.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 169.254.20.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 169.254.20.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A OUTPUT -s 10.96.0.10/32 -p tcp -m tcp --sport 8080 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A OUTPUT -d 10.96.0.10/32 -p tcp -m tcp --dport 8080 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
...
# logs :
kubectl -n kube-system logs -l k8s-app=kube-dns -f
kubectl -n kube-system logs -l k8s-app=node-local-dns -f
#
kubectl exec -it curl-pod -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
#
kubectl exec -it curl-pod -- nslookup webpod
kubectl exec -it curl-pod -- nslookup google.com
#
kubectl delete pod curl-pod
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
labels:
app: curl
spec:
containers:
- name: curl
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
kubectl exec -it curl-pod -- cat /etc/resolv.conf
# 로그 확인 시 현재 nodelocaldns 미활용!
kubectl -n kube-system logs -l k8s-app=kube-dns -f
kubectl -n kube-system logs -l k8s-app=node-local-dns -f
#
kubectl exec -it curl-pod -- nslookup webpod
kubectl exec -it curl-pod -- nslookup google.com
✅ 실행 결과 요약
(⎈|HomeLab:N/A) root@k8s-ctr:~# wget https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
--2025-08-02 04:03:11-- https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
Resolving github.com (github.com)... 20.200.245.247
Connecting to github.com (github.com)|20.200.245.247|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml [following]
--2025-08-02 04:03:11-- https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.109.133, 185.199.110.133, 185.199.111.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5377 (5.3K) [text/plain]
Saving to: 'nodelocaldns.yaml'
nodelocaldns.yaml 100%[====================>] 5.25K --.-KB/s in 0s
2025-08-02 04:03:12 (146 MB/s) - 'nodelocaldns.yaml' saved [5377/5377]
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
(⎈|HomeLab:N/A) root@k8s-ctr:~# domain='cluster.local'
(⎈|HomeLab:N/A) root@k8s-ctr:~# localdns='169.254.20.10'
(⎈|HomeLab:N/A) root@k8s-ctr:~# echo $kubedns $domain $localdns
10.96.0.10 cluster.local 169.254.20.10
(⎈|HomeLab:N/A) root@k8s-ctr:~# sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml
(⎈|HomeLab:N/A) root@k8s-ctr:~# vim nodelocaldns.yaml
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl apply -f nodelocaldns.yaml
serviceaccount/node-local-dns created
service/kube-dns-upstream created
configmap/node-local-dns created
daemonset.apps/node-local-dns created
service/node-local-dns created
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -n kube-system -l k8s-app=node-local-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-local-dns-4zczh 0/1 ContainerCreating 0 5s 192.168.10.100 k8s-ctr <none> <none>
node-local-dns-h6mrn 0/1 ContainerCreating 0 5s 192.168.10.101 k8s-w1 <none> <none>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl edit cm -n kube-system node-local-dns
configmap/node-local-dns edited
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system rollout restart ds node-local-dns
daemonset.apps/node-local-dns restarted
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl describe cm -n kube-system node-local-dns
Name: node-local-dns
Namespace: kube-system
Labels: addonmanager.kubernetes.io/mode=Reconcile
Annotations: <none>
Data
====
Corefile:
----
cluster.local:53 {
log
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health 169.254.20.10:8080
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
log
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
BinaryData
====
(⎈|HomeLab:N/A) root@k8s-ctr:~# diff before.txt after.txt
1c1
< # Generated by iptables-save v1.8.10 (nf_tables) on Sat Aug 2 04:02:18 2025
---
> # Generated by iptables-save v1.8.10 (nf_tables) on Sat Aug 2 04:06:15 2025
19,20c19,20
...
> -A INPUT -d 10.96.0.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A INPUT -d 10.96.0.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A INPUT -d 169.254.20.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A INPUT -d 169.254.20.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
64a85,88
> -A OUTPUT -s 10.96.0.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A OUTPUT -s 10.96.0.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A OUTPUT -s 169.254.20.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
> -A OUTPUT -s 169.254.20.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
...
# Cilium의 eBPF를 사용하기 때문에, 세팅이 되어 있어도 Core Dns를 사용하지 않아서 NodeLocal DNS를 사용하지 않음
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl -n kube-system logs -l k8s-app=kube-dns -f
[INFO] 192.168.10.100:40750 - 30286 "HINFO IN 2645219354702984660.6393601186467338274.in-addr.arpa. tcp 70 false 65535" NXDOMAIN qr,rd,ra 154 0.909633293s
[INFO] 10.0.2.15:46356 - 61437 "HINFO IN 9139433505333512684.6001984212670760861.cluster.local. tcp 71 false 65535" NXDOMAIN qr,aa,rd 164 0.000574917s
[INFO] 10.0.2.15:46382 - 52920 "HINFO IN 1431158704855412969.8142403122521456660.ip6.arpa. tcp 66 false 65535" NXDOMAIN qr,rd,ra 142 0.075756666s
[INFO] 10.0.2.15:46366 - 65271 "HINFO IN 1750462968553614302.5527208912869364394.in-addr.arpa. tcp 70 false 65535" NXDOMAIN qr,rd,ra 154 0.245231709s
[INFO] 192.168.10.100:37208 - 49724 "HINFO IN 59537071377988402.4483150371250018294.cluster.local. tcp 69 false 65535" NXDOMAIN qr,aa,rd 162 0.000739458s
[INFO] 192.168.10.100:37196 - 63782 "HINFO IN 1836266893652452245.1152214656702760190.ip6.arpa. tcp 66 false 65535" NXDOMAIN qr,rd,ra 142 0.090780583s
[INFO] 192.168.10.100:37224 - 15783 "HINFO IN 916977063610790040.1584862186733241454.in-addr.arpa. tcp 69 false 65535" NXDOMAIN qr,rd,ra 153 0.233278834s
[INFO] 10.0.2.15:44966 - 15006 "HINFO IN 7376589713844483082.2083312749648297410.cluster.local. tcp 71 false 65535" NXDOMAIN qr,aa,rd 164 0.000153042s
[INFO] 10.0.2.15:44958 - 62420 "HINFO IN 926710957443248427.3229660542340556501.ip6.arpa. tcp 65 false 65535" NXDOMAIN qr,rd,ra 141 0.080801834s
[INFO] 10.0.2.15:44946 - 41149 "HINFO IN 1410372477300746093.9207712647495735737.in-addr.arpa. tcp 70 false 65535" NXDOMAIN qr,rd,ra 154 0.209030042s
(⎈|HomeLab:N/A) root@k8s-ctr:~kubectl -n kube-system logs -l k8s-app=node-local-dns -f-f
[INFO] Added back nodelocaldns rule - {filter INPUT [-p tcp -d 10.96.0.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {filter INPUT [-p udp -d 10.96.0.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -s 10.96.0.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p udp -s 10.96.0.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {filter OUTPUT [-p tcp -s 10.96.0.10 --sport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {filter OUTPUT [-p udp -s 10.96.0.10 --sport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -d 10.96.0.10 --dport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p udp -d 10.96.0.10 --dport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -d 10.96.0.10 --dport 8080 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -s 10.96.0.10 --sport 8080 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {filter INPUT [-p tcp -d 10.96.0.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {filter INPUT [-p udp -d 10.96.0.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -s 10.96.0.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p udp -s 10.96.0.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {filter OUTPUT [-p tcp -s 10.96.0.10 --sport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {filter OUTPUT [-p udp -s 10.96.0.10 --sport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -d 10.96.0.10 --dport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p udp -d 10.96.0.10 --dport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -d 10.96.0.10 --dport 8080 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -s 10.96.0.10 --sport 8080 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
Cilium - Local Redirect Policy 실습
이를 해결하기 위해 Cilium Local Redirect Policy를 사용합니다.
- IP 주소와 Port/Protocol tuple 또는 Kubernetes Service 로 향하는 포드 트래픽을 eBPF를 사용하여, 노드 내 백엔드 포드로 로컬로 리디렉션할 수 있도록 하는 Cilium의 로컬 리디렉션 정책을 구성하는 방법을 설명합니다.
- 백엔드 포드의 네임스페이스는 정책의 네임스페이스와 일치해야 합니다.
- CiliumLocalRedirectPolicy는 CustomResourceDefinition으로 구성되어 있습니다.
#
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-value
kubectl rollout restart deploy cilium-operator -n kube-system
kubectl rollout restart ds cilium -n kube-system
#
wget https://raw.githubusercontent.com/cilium/cilium/1.17.6/examples/kubernetes-local-redirect/node-local-dns.yaml
kubedns=$(kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP})
sed -i "s/__PILLAR__DNS__SERVER__/$kubedns/g;" node-local-dns.yaml
vi -d nodelocaldns.yaml node-local-dns.yaml
## before
args: [ "-localip", "169.254.20.10,10.96.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
## after
args: [ "-localip", "169.254.20.10,10.96.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream", "-skipteardown=true", "-setupinterface=false", "-setupiptables=false" ]
# 배포
# Modify Node-local DNS cache's deployment yaml to pass these additional arguments to node-cache:
## -skipteardown=true, -setupinterface=false, and -setupiptables=false.
# Modify Node-local DNS cache's deployment yaml to put it in non-host namespace by setting hostNetwork: false for the daemonset.
# In the Corefile, bind to 0.0.0.0 instead of the static IP.
kubectl apply -f node-local-dns.yaml
#
kubectl edit cm -n kube-system node-local-dns # log, debug 추가
kubectl -n kube-system rollout restart ds node-local-dns
kubectl describe cm -n kube-system node-local-dns
...
cluster.local:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health 169.254.20.10:8080
}
...
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
...
# iptables 확인 : 규칙 업데이트까지 다소 시간 소요!
iptables-save | tee after.txt
diff before.txt after.txt
##
iptables -t filter -S | grep -i dns
-A INPUT -d 10.96.0.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 10.96.0.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 169.254.20.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A INPUT -d 169.254.20.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 10.96.0.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 10.96.0.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 169.254.20.10/32 -p udp -m udp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
-A OUTPUT -s 169.254.20.10/32 -p tcp -m tcp --sport 53 -m comment --comment "NodeLocal DNS Cache: allow DNS traffic" -j ACCEPT
##
iptables -t raw -S | grep -i dns
-A PREROUTING -d 10.96.0.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 10.96.0.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 169.254.20.10/32 -p udp -m udp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A PREROUTING -d 169.254.20.10/32 -p tcp -m tcp --dport 53 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A OUTPUT -s 10.96.0.10/32 -p tcp -m tcp --sport 8080 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
-A OUTPUT -d 10.96.0.10/32 -p tcp -m tcp --dport 8080 -m comment --comment "NodeLocal DNS Cache: skip conntrack" -j NOTRACK
...
# logs :
kubectl -n kube-system logs -l k8s-app=kube-dns -f
kubectl -n kube-system logs -l k8s-app=node-local-dns -f
#
kubectl exec -it curl-pod -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
#
kubectl exec -it curl-pod -- nslookup webpod
kubectl exec -it curl-pod -- nslookup google.com
#
kubectl delete pod curl-pod
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl-pod
labels:
app: curl
spec:
containers:
- name: curl
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
kubectl exec -it curl-pod -- cat /etc/resolv.conf
# 로그 확인 시 현재 nodelocaldns 미활용!
kubectl -n kube-system logs -l k8s-app=kube-dns -f
kubectl -n kube-system logs -l k8s-app=node-local-dns -f
#
kubectl exec -it curl-pod -- nslookup webpod
kubectl exec -it curl-pod -- nslookup google.com
✅ 실행 결과 요약
1. nslookup 명령어 실행 결과:
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- nslookup www.google.com
E0802 04:18:26.970491 31225 websocket.go:297] Unknown stream id 1, discarding message
;; Got recursion not available from 10.96.0.10 # CoreDNS는 재귀 쿼리를 지원하지 않음
;; Got recursion not available from 10.96.0.10 # 재귀 쿼리 지원 안함
;; Got recursion not available from 10.96.0.10 # 재귀 쿼리 지원 안함
Server: 10.96.0.10 # CoreDNS 서비스 IP
Address: 10.96.0.10#53
Non-authoritative answer: # 비권한 응답 (캐시된 결과)
Name: www.google.com
Address: 216.58.220.100 # Google의 IPv4 주소
Name: www.google.com
Address: 2404:6800:4005:806::2004 # Google의 IPv6 주소
2. DNS 쿼리 패킷 캡처 분석:
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i any udp port 53 -nn
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
# 첫 번째 검색 도메인으로 쿼리 (ndots:5 설정에 따라)
04:18:27.019098 lxc99ac9a6ef232 In IP 172.20.2.167.41019 > 172.20.2.190.53: 35852+ A? www.google.com.default.svc.cluster.local. (58) # curl-pod에서 NodeLocalDNS로 첫 번째 검색 도메인 쿼리
04:18:27.023104 lxcfba589af5b49 In IP 172.20.2.190.53 > 172.20.2.167.41019: 35852 NXDomain*- 0/1/0 (151) # NXDomain 응답 (도메인 없음)
# 두 번째 검색 도메인으로 쿼리
04:18:27.025761 lxc99ac9a6ef232 In IP 172.20.2.167.42736 > 172.20.2.190.53: 4350+ A? www.google.com.svc.cluster.local. (50) # 두 번째 검색 도메인 쿼리
04:18:27.029686 lxcfba589af5b49 In IP 172.20.2.190.53 > 172.20.2.167.42736: 4350 NXDomain*- 0/1/0 (143) # NXDomain 응답
# 세 번째 검색 도메인으로 쿼리
04:18:27.032251 lxc99ac9a6ef232 In IP 172.20.2.167.54967 > 172.20.2.190.53: 45254+ A? www.google.com.cluster.local. (46) # 세 번째 검색 도메인 쿼리
04:18:27.033103 lxcfba589af5b49 In IP 172.20.2.190.53 > 172.20.2.167.54967: 45254 NXDomain*- 0/1/0 (139) # NXDomain 응답
# 정확한 도메인으로 쿼리 (마지막 시도)
04:18:27.043070 lxc99ac9a6ef232 In IP 172.20.2.167.42729 > 172.20.2.190.53: 9937+ A? www.google.com. (32) # 정확한 도메인으로 A 레코드 쿼리
04:18:27.044464 lxcfba589af5b49 In IP 172.20.2.190.55010 > 10.0.2.3.53: 9937+ A? www.google.com. (32) # NodeLocalDNS에서 외부 DNS(10.0.2.3)로 포워딩
04:18:27.044521 eth0 Out IP 10.0.2.15.55010 > 10.0.2.3.53: 9937+ A? www.google.com. (32) # eth0 인터페이스로 외부 전송
04:18:27.056027 eth0 In IP 10.0.2.3.53 > 10.0.2.15.55010: 9937 1/0/0 A 216.58.220.100 (48) # 외부 DNS에서 Google IPv4 주소 응답
04:18:27.056391 lxcfba589af5b49 In IP 172.20.2.190.53 > 172.20.2.167.42729: 9937 1/0/0 A 216.58.220.100 (62) # NodeLocalDNS에서 curl-pod로 응답 전달
# IPv6 AAAA 레코드 쿼리
04:18:27.058880 lxc99ac9a6ef232 In IP 172.20.2.167.51394 > 172.20.2.190.53: 6361+ AAAA? www.google.com. (32) # AAAA 레코드 쿼리
04:18:27.059090 lxcfba589af5b49 In IP 172.20.2.190.55010 > 10.0.2.3.53: 6361+ AAAA? www.google.com. (32) # NodeLocalDNS에서 외부 DNS로 포워딩
04:18:27.059098 eth0 Out IP 10.0.2.15.55010 > 10.0.2.3.53: 6361+ AAAA? www.google.com. (32) # 외부 전송
04:18:27.068792 eth0 In IP 10.0.2.3.53 > 10.0.2.15.55010: 6361 1/0/0 AAAA 2404:6800:4005:806::2004 (60) # 외부 DNS에서 Google IPv6 주소 응답
04:18:27.069075 lxcfba589af5b49 In IP 172.20.2.190.53 > 172.20.2.167.51394: 6361 1/0/0 AAAA 2404:6800:4005:806::2004 (74) # NodeLocalDNS에서 curl-pod로 응답 전달
다음과 같이 Local Redirect Policy가 적용 된 것을 확인할 수 있습니다.
- IP 주소:
172.20.2.167: curl-pod의 IP 주소172.20.2.190: NodeLocalDNS Pod의 IP 주소 (기존 CoreDNS Pod와 다른 IP)10.0.2.3: 외부 DNS 서버
- DNS 쿼리 흐름:
- curl-pod → NodeLocalDNS (172.20.2.190) - 로컬 캐시 확인
- NodeLocalDNS → 외부 DNS (10.0.2.3) - 캐시 미스 시 외부 쿼리
- 외부 DNS → NodeLocalDNS - 응답 수신
- NodeLocalDNS → curl-pod - 최종 응답 전달
- 성능 개선 효과:
- NodeLocalDNS가 로컬에서 DNS 캐싱을 처리하여 네트워크 홉 수 감소
- 캐시된 결과는 즉시 응답, 캐시 미스만 외부 DNS로 전달
- iptables DNAT 규칙 우회로 성능 향상
마치며
이번 3주차 실습을 통해 Kubernetes 클러스터의 네트워킹과 DNS 동작 원리를 깊이 있게 분석했습니다.
특히 DNS 쿼리의 흐름과,처리 과정에서 발생하는 검색 도메인을 통한 순차적 시도와 NodeLocalDNS를 통한 성능 최적화 효과를 직접 확인할 수 있었습니다.
Cilium을 실무에 적용하여 사용하면서 발생할 수 있는 문제점을 스터디를 통해 사전에 파악하여 고민할 수 있는 갚진 시간이었다고 생각되네요. (다만, 아주 맵습니다.. ㅎ)
향후 Cilium 기반 클러스터를 운영할 때 이러한 지식과 경험이 실제 문제 해결에 큰 도움이 될 것으로 기대됩니다.
긴 글 읽어주셔서 감사합니다 :)
'클라우드 컴퓨팅 & NoSQL > [Cilium Study] 실리움 스터디' 카테고리의 다른 글
| [6주차 - Cilium 스터디] Cilium ServiceMesh (25.08.17) (0) | 2025.08.23 |
|---|---|
| [5주차 - Cilium 스터디] BGP Control Plane & ClusterMesh (25.08.10) (4) | 2025.08.16 |
| [4주차 - Cilium 스터디] Networking - 노드에 파드들간 통신 2 & K8S 외부 노출 (25.08.03) (4) | 2025.08.08 |
| [2주차 - Cilium 스터디] (Observabilty) Hubbkem Prometheus/Grafana (25.07.20) (3) | 2025.07.26 |
| [1주차 - Cilium 스터디] Cilium 이란? (25.07.13) (2) | 2025.07.18 |