[KANS 3기] Cilium CNI
들어가며
이번주에는 드디어 CNI의 끝판 왕(제가 아는 한에서)인 Cilium에 대해 알아보겠습니다. 어떤 것들이 가능할지 궁금합니다. KANS 3기 8주차 스터디를 시작하겠습니다.
Cilium
Cilium은 eBPF(Berkeley Packet Filter)를 사용하여 네트워크 보안 및 라우팅을 제공하는 CNI(Container Network Interface) 플러그인입니다. 먼저 Cilium의 근간이 되는 eBPF에 대해 간단히 소개하고, Cilium에 대한 소개와 실습을 진행하겠습니다.
BPF/eBPF 소개
- 소개글 : 링크
기존 리눅스 Netfilter 기반 네트워크 스택
기존의 네트워크 스택은 Netfilter 기반으로 동작하며, 복잡한 네트워크 레이어를 거쳐야하고 이 레이어를 건너뛰기 어렵습니다. 또한 kube-proxy와 같은 userland 프로세스를 통해 네트워크 패킷을 처리합니다. 그러다보니 오버헤드가 커져서 성능이 떨어지고, 룰이 복잡해질 경우 수 많은 룰을 관리해야하는 문제가 있습니다.
eBPF 기반 네트워크 스택
eBPF는 커널 내부에서 동작하는 프로그램을 실행할 수 있으며, 이를 통해 네트워크 스택을 확장할 수 있습니다. 특히 샌드박스 방식을 통해 eBPF 프로그램이 커널에 영향을 미치지 않도록 보호할 수 있습니다. 즉, eBPF 프로그램이 잘못된 동작을 하더라도 커널 패닉등의 발생이 거의 없습니다.
또한 XDP(eXpress Data Path)를 통해 네트워크 패킷을 처리할 수 있으며, 이 XDP는 네트워크 카드(Offloaded mode), 네트워크 드라이버(Native mode), 커널 스페이스(Generic Mode)에서 동작하여 훨씬 빠르게 패킷을 처리할 수 있습니다.
iptables와 eBPF 성능 비교
- eBPF 활용처
- Security (보안) : eBPF는 하드웨어 레벨에서 부터 모든 시스템 콜을 이해하고 처리할 수 있기 때문에 보다 더 세밀하고 강력한 보안 정책을 적용할 수 있습니다.
- Tracing & Profiling (추적 및 프로파일링) : eBPF는 커널 내부의 모든 이벤트를 추적하고 프로파일링 할 수 있습니다. 기존에 해결하기 어려웠던 성능 문제들도 eBPF를 통해 해결할 수 있습니다.
- Networking (네트워킹) : eBPF는 커널 스페이스를 떠나지 않고 새로운 프로토콜을 만든다던지, 라우팅을 구현하는 등의 다양한 네트워크 기능을 만들 수 있습니다.
- Observability (가시성) : 커널내부에서 다양한 소스에서 메트릭을 수집하고, 처리할 수 있고, 일부 데이터만 샘플링하는 것이 아닌 모든 데이터를 수집할 수 있습니다.
Cilium 소개
- Cilium은 기존의 복잡한 네트워크 스택을 eBPF를 통해 간소화하고, 빠르게 처리할 수 있도록 하는 CNI 플러그인입니다.
https://isovalent.com/blog/post/migrating-from-metallb-to-cilium/
- Cilium eBPF는 추가적인 코드 수정이나 설정 변경없이, 리눅스 커널에서 동작하는 Bytecode를 자유롭게 프로그래밍하여 커널에 로딩시켜 동작이 가능합니다. 링크
- 또한 eBPF는 모든 패킷을 가로채기 위해서 수신 NIC의 ingress TC(Traffic Control) hooks를 사용할 수 있습니다.
NIC의 TC Hooks에 eBPF 프로그램이 attach 된 예
- Cilium은 터널모드(VXLAN, GENEVE), 네이티브 라우팅 모드의 2가지 네트워크 모드를 제공합니다.
- 터널모드 : Cilium이 VXLAN(UDP 8472), GENEVE(UDP 6081) 인터페이스를 만들어서 이들을 통해 트래픽을 전달합니다. Encapsulation 모드라고도 합니다.
-
네이티브 라우팅 모드 : Cilium가 패킷 전달을 위해 구성을 변경하지 않고, 외부에서 제공되는 패킷 전달 방법(클라우드 또는 BGP 라우팅등)을 사용합니다. Direct Routing 모드라고도 합니다.
Cilium 네트워크 모드 - 링크
- 2021년 10월 Cilium은 CNCF에 채택되었습니다. 링크
- Googke GKE dataplane과 AWS EKS Anywhere에서 기본 CNI로 Cilium을 사용하고 있습니다. 링크
- Cilium은 Kube-Proxy를 100% 대체 가능합니다.
- iptables를 거의 사용하지 않고도 동작하며, 이를 통해 성능을 향상시킬 수 있습니다.
- 하지만 istio, FHRP, VRRP 등과 같이 iptables 기능을 활용하는 동작들은 이슈가 발생할 수 있으며, 차츰 해결해 나가고 있습니다.
Cilium 아키텍쳐
- 구성요소 - 링크
Cilium 아키텍쳐 - 출처
- Cilium Agent : 데몬셋으로 실행, K8S API 설정으로 부터 ‘네트워크 설정, 네트워크 정책, 서비스 부하분산, 모니터링’ 등을 수행하며, eBPF 프로그램을 관리합니다.
- Cilium Client (CLI) : Cilium 커멘드툴로 eBPF maps 에 직접 접속하여 상태를 확인할 수 있습니다.
- Cilium Operator : K8S 클러스터에 대한 한 번씩 처리해야 하는 작업을 관리합니다.
- Hubble : 네트워크와 보안 모니터링 플랫폼 역할을 하여, Server, Relay, Client, Graphical UI 로 구성되어 있습니다.
- Data Store : Cilium Agent 간의 상태를 저장하고 전파하는 데이터 저장소, 2가지 종류 중 선택(K8S CRDs, Key-Value Store)할 수 있습니다.
Cilium 실습
실습을 통해 Cilium CNI에 대해서 알아보겠습니다. 먼저 Cilium을 설치하고, 기본적인 설정을 확인하고, 네트워크 정책을 설정해보겠습니다.
Cilium 설치
Helm을 통한 설치 및 확인
# 모니터링
$ watch -d kubectl get node,pod -A -owide
#
$ helm repo add cilium https://helm.cilium.io/
# => "cilium" has been added to your repositories
$ helm repo update
# => ...Successfully got an update from the "cilium" chart repository
# Update Complete. ⎈Happy Helming!⎈
#
$ helm install cilium cilium/cilium --version 1.16.3 --namespace kube-system \
--set k8sServiceHost=192.168.10.10 --set k8sServicePort=6443 --set debug.enabled=true \
--set rollOutCiliumPods=true --set routingMode=native --set autoDirectNodeRoutes=true \
--set bpf.masquerade=true --set bpf.hostRouting=true --set endpointRoutes.enabled=true \
--set ipam.mode=kubernetes --set k8s.requireIPv4PodCIDR=true --set kubeProxyReplacement=true \
--set ipv4NativeRoutingCIDR=192.168.0.0/16 --set installNoConntrackIptablesRules=true \
--set hubble.ui.enabled=true --set hubble.relay.enabled=true --set prometheus.enabled=true --set operator.prometheus.enabled=true --set hubble.metrics.enableOpenMetrics=true \
--set hubble.metrics.enabled="{dns:query;ignoreAAAA,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
# => NAME: cilium
# LAST DEPLOYED: Sat Oct 01 07:23:34 2024
# NAMESPACE: kube-system
# STATUS: deployed
# REVISION: 1
# TEST SUITE: None
# NOTES:
# You have successfully installed Cilium with Hubble Relay and Hubble UI.
#
# Your release version is 1.16.3.
#
# For any further help, visit https://docs.cilium.io/en/v1.16/gettinghelp
## 주요 파라미터 설명
# --set debug.enabled=true # cilium 파드에 로그 레벨을 debug 설정
# --set autoDirectNodeRoutes=true # 동일 대역 내의 노드들 끼리는 상대 노드의 podCIDR 대역의 라우팅이 자동으로 설정
# --set endpointRoutes.enabled=true # 호스트에 endpoint(파드)별 개별 라우팅 설정
# --set hubble.relay.enabled=true --set hubble.ui.enabled=true # hubble 활성화
# --set ipam.mode=kubernetes --set k8s.requireIPv4PodCIDR=true # k8s IPAM 활용
# --set kubeProxyReplacement=true # kube-proxy 없이 (최대한) 대처할수 있수 있게
# --set ipv4NativeRoutingCIDR=192.168.0.0/16 # 해당 대역과 통신 시 IP Masq 하지 않음, 보통 사내망 대역을 지정
# --set operator.replicas=1 # cilium-operator 파드 기본 1개
# --set enableIPv4Masquerade=true --set bpf.masquerade=true # 파드를 위한 Masquerade , 추가로 Masquerade 을 BPF 로 처리 >> enableIPv4Masquerade=true 인 상태에서 추가로 bpf.masquerade=true 적용이 가능
# 설정 및 확인
$ ip -c addr
# => ...
# 4: cilium_net@cilium_host: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
# link/ether 3a:03:2f:7e:cc:72 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::3803:2fff:fe7e:cc72/64 scope link
# valid_lft forever preferred_lft forever
# 5: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
# link/ether ca:73:d2:a6:00:b4 brd ff:ff:ff:ff:ff:ff
# inet 172.16.0.227/32 scope global cilium_host
# valid_lft forever preferred_lft forever
# inet6 fe80::c873:d2ff:fea6:b4/64 scope link
# valid_lft forever preferred_lft forever
# ...
$ kubectl get node,pod,svc -A -owide
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# node/k3s-m Ready control-plane,master 3h58m v1.30.5+k3s1 192.168.10.10 <none> Ubuntu 22.04.5 LTS 5.15.0-119-generic containerd://1.7.21-k3s2
# node/k3s-w1 Ready <none> 3h57m v1.30.5+k3s1 192.168.10.101 <none> Ubuntu 22.04.5 LTS 5.15.0-119-generic containerd://1.7.21-k3s2
# node/k3s-w2 Ready <none> 3h55m v1.30.5+k3s1 192.168.10.102 <none> Ubuntu 22.04.5 LTS 5.15.0-119-generic containerd://1.7.21-k3s2
#
# NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# kube-system pod/cilium-envoy-9q7c6 1/1 Running 0 5m19s 192.168.10.102 k3s-w2 <none> <none>
# kube-system pod/cilium-ljv9t 1/1 Running 0 5m19s 192.168.10.101 k3s-w1 <none> <none>
# kube-system pod/cilium-operator-76bb588dbc-gxrqx 1/1 Running 0 5m19s 192.168.10.101 k3s-w1 <none> <none>
# kube-system pod/cilium-q96l4 1/1 Running 0 5m19s 192.168.10.102 k3s-w2 <none> <none>
# kube-system pod/coredns-7b98449c4-x5756 1/1 Running 0 2m59s 172.16.1.21 k3s-w1 <none> <none>
# kube-system pod/hubble-relay-88f7f89d4-fcq2s 1/1 Running 0 5m19s 172.16.1.99 k3s-w1 <none> <none>
# kube-system pod/hubble-ui-59bb4cb67b-r48tz 2/2 Running 0 5m19s 172.16.0.238 k3s-m <none> <none>
# kube-system pod/local-path-provisioner-6795b5f9d8-84m96 1/1 Running 11 (5m28s ago) 3h58m 172.16.2.184 k3s-w2 <none> <none>
# kube-system pod/metrics-server-cdcc87586-g5m2d 1/1 Running 11 (5m10s ago) 3h58m 172.16.2.223 k3s-w2 <none> <none>
# ...
#
# NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# default service/kubernetes ClusterIP 10.10.200.1 <none> 443/TCP 3h58m <none>
# kube-system service/cilium-envoy ClusterIP None <none> 9964/TCP 5m19s k8s-app=cilium-envoy
# kube-system service/hubble-metrics ClusterIP None <none> 9965/TCP 5m19s k8s-app=cilium
# kube-system service/hubble-peer ClusterIP 10.10.200.203 <none> 443/TCP 5m19s k8s-app=cilium
# kube-system service/hubble-relay ClusterIP 10.10.200.175 <none> 80/TCP 5m19s k8s-app=hubble-relay
# kube-system service/hubble-ui ClusterIP 10.10.200.125 <none> 80/TCP 5m19s k8s-app=hubble-ui
# kube-system service/kube-dns ClusterIP 10.10.200.10 <none> 53/UDP,53/TCP,9153/TCP 3h58m k8s-app=kube-dns
# kube-system service/metrics-server ClusterIP 10.10.200.180 <none> 443/TCP 3h58m k8s-app=metrics-server
$ iptables -t nat -S
$ iptables -t filter -S
$ iptables -t raw -S
$ iptables -t mangle -S
$ conntrack -L
$ kubectl get crd | grep cilium
# => ciliumcidrgroups.cilium.io 2024-10-01T11:10:57Z
# ciliumclusterwidenetworkpolicies.cilium.io 2024-10-01T11:10:58Z
# ciliumendpoints.cilium.io 2024-10-01T11:10:57Z
# ciliumexternalworkloads.cilium.io 2024-10-01T11:10:57Z
# ciliumidentities.cilium.io 2024-10-01T11:10:58Z
# ciliuml2announcementpolicies.cilium.io 2024-10-01T11:10:57Z
# ciliumloadbalancerippools.cilium.io 2024-10-01T11:10:57Z
# ciliumnetworkpolicies.cilium.io 2024-10-01T11:10:58Z
# ciliumnodeconfigs.cilium.io 2024-10-01T11:10:57Z
# ciliumnodes.cilium.io 2024-10-01T11:10:57Z
# ciliumpodippools.cilium.io 2024-10-01T11:10:57Z
$ kubectl get ciliumnodes # cilium_host 인터페이스의 IP 확인 : CILIUMINTERNALIP
# => NAME CILIUMINTERNALIP INTERNALIP AGE
# k3s-m 172.16.0.227 192.168.10.10 21m
# k3s-w1 172.16.1.82 192.168.10.101 21m
# k3s-w2 172.16.2.25 192.168.10.102 21m
$ kubectl get ciliumendpoints -A
# => NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# kube-system coredns-7b98449c4-x5756 11970 ready 172.16.1.21
# kube-system hubble-relay-88f7f89d4-fcq2s 4124 ready 172.16.1.99
# kube-system hubble-ui-59bb4cb67b-r48tz 37523 ready 172.16.0.238
# kube-system local-path-provisioner-6795b5f9d8-84m96 11088 ready 172.16.2.184
# kube-system metrics-server-cdcc87586-g5m2d 27624 ready 172.16.2.223
$ kubectl get cm -n kube-system cilium-config -o json | jq
$ kubetail -n kube-system -l k8s-app=cilium --since 1h
$ kubetail -n kube-system -l k8s-app=cilium-envoy --since 1h
# Native XDP 지원 NIC 확인 : https://docs.cilium.io/en/stable/bpf/progtypes/#xdp-drivers
$ ethtool -i enp0s8
# => driver: virtio_net # >= XDP 4.10 부터 지원되는듯 합니다.
# version: 1.0.0
# ...
# https://docs.cilium.io/en/stable/operations/performance/tuning/#bypass-iptables-connection-tracking
$ watch -d kubectl get pod -A # 모니터링
$ helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values --set installNoConntrackIptablesRules=true
# => Release "cilium" has been upgraded. Happy Helming!
# NAME: cilium
# LAST DEPLOYED: Sat Oct 01 11:42:10 2024
# NAMESPACE: kube-system
# STATUS: deployed
# REVISION: 2
# TEST SUITE: None
# NOTES:
# You have successfully installed Cilium with Hubble Relay and Hubble UI.
#
# Your release version is 1.16.3.
#
# For any further help, visit https://docs.cilium.io/en/v1.16/gettinghelp
# 확인: 기존 raw 에 아래 rule 추가 확인
$ iptables -t raw -S | grep notrack
# => -A CILIUM_OUTPUT_raw -d 192.168.0.0/16 -m comment --comment "cilium: NOTRACK for pod traffic" -j CT --notrack
# -A CILIUM_OUTPUT_raw -s 192.168.0.0/16 -m comment --comment "cilium: NOTRACK for pod traffic" -j CT --notrack
# ...
$ conntrack -F
$ conntrack -L
$ conntrack -L |grep -v 2379
Cilium CLI를 통한 확인
# cilium 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{,.sha256sum}
$ sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
$ sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
$ rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
# 테스트
$ cilium status --wait
# => /¯¯\
# /¯¯\__/¯¯\ Cilium: OK
# \__/¯¯\__/ Operator: OK
# /¯¯\__/¯¯\ Envoy DaemonSet: OK
# \__/¯¯\__/ Hubble Relay: OK
# \__/ ClusterMesh: disabled
#
# DaemonSet cilium Desired: 3, Ready: 3/3, Available: 3/3
# DaemonSet cilium-envoy Desired: 3, Ready: 3/3, Available: 3/3
# Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1
# Deployment hubble-relay Desired: 1, Ready: 1/1, Available: 1/1
# Deployment hubble-ui Desired: 1, Ready: 1/1, Available: 1/1
# Containers: cilium Running: 3
# cilium-envoy Running: 3
# cilium-operator Running: 1
# hubble-relay Running: 1
# hubble-ui Running: 1
# Cluster Pods: 5/5 managed by Cilium
# Helm chart version: 1.16.3
# Image versions cilium quay.io/cilium/cilium:v1.16.3@sha256:62d2a09bbef840a46099ac4c69421c90f84f28d018d479749049011329aa7f28: 3
# cilium-envoy quay.io/cilium/cilium-envoy:v1.29.9-1728346947-0d05e48bfbb8c4737ec40d5781d970a550ed2bbd@sha256:42614a44e508f70d03a04470df5f61e3cffd22462471a0be0544cf116f2c50ba: 3
# cilium-operator quay.io/cilium/operator-generic:v1.16.3@sha256:6e2925ef47a1c76e183c48f95d4ce0d34a1e5e848252f910476c3e11ce1ec94b: 1
# hubble-relay quay.io/cilium/hubble-relay:v1.16.3@sha256:feb60efd767e0e7863a94689f4a8db56a0acc7c1d2b307dee66422e3dc25a089: 1
# hubble-ui quay.io/cilium/hubble-ui-backend:v0.13.1@sha256:0e0eed917653441fded4e7cdb096b7be6a3bddded5a2dd10812a27b1fc6ed95b: 1
# hubble-ui quay.io/cilium/hubble-ui:v0.13.1@sha256:e2e9313eb7caf64b0061d9da0efbdad59c6c461f6ca1752768942bfeda0796c6: 1
$ cilium connectivity test
$ cilium config view
# => agent-not-ready-taint-key node.cilium.io/agent-not-ready
# arping-refresh-period 30s
# auto-direct-node-routes true
# bpf-events-drop-enabled true
# bpf-events-policy-verdict-enabled true
# bpf-events-trace-enabled true
# bpf-lb-acceleration disabled
# bpf-lb-external-clusterip false
# bpf-lb-map-max 65536
# bpf-lb-sock false
# bpf-lb-sock-terminate-pod-connections false
# bpf-map-dynamic-size-ratio 0.0025
# bpf-policy-map-max 16384
# bpf-root /sys/fs/bpf
# ...
# ipv4-native-routing-cidr 192.168.0.0/16
# ...
# kube-proxy-replacement true
# kube-proxy-replacement-healthz-bind-address
# max-connected-clusters 255
# mesh-auth-enabled true
# ...
# cilium 데몬셋 파드 내에서 cilium 명령어로 상태 확인
$ export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k3s-m -o jsonpath='{.items[0].metadata.name}')
$ alias c0="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium"
$ c0 status --verbose
# => ...
# KubeProxyReplacement: True [enp0s3 10.0.2.15 fe80::6d:daff:feb3:d4d3, enp0s8 192.168.10.10 fe80::27ff:fe8a:de00 (Direct Routing)]
# ...
# IPAM: IPv4: 4/254 allocated from 172.16.0.0/24,
# Allocated addresses:
# 172.16.0.223 (kube-system/hubble-ui-59bb4cb67b-r48tz)
# 172.16.0.227 (router)
# 172.16.0.26 (health)
# 172.16.0.44 (cilium-test-1/client3-67f959dd9b-ptl65)
# ...
# Routing: Network: Native Host: BPF
# ...
# Device Mode: veth
# Masquerading: BPF [ens5] 192.168.0.0/16 [IPv4: Enabled, IPv6: Disabled]
# ...
# Proxy Status: OK, ip 172.16.0.227, 0 redirects active on ports 10000-20000, Envoy: external
# ...
# KubeProxyReplacement Details:
# Status: True
# Socket LB: Enabled
# Socket LB Tracing: Enabled
# Socket LB Coverage: Full
# Devices: enp0s3 10.0.2.15 fe80::6d:daff:feb3:d4d3, enp0s8 192.168.10.10 fe80::27ff:fe8a:de00 (Direct Routing)
# Mode: SNAT
# Backend Selection: Random
# Session Affinity: Enabled
# Graceful Termination: Enabled
# NAT46/64 Support: Disabled
# XDP Acceleration: Disabled
# Services:
# - ClusterIP: Enabled
# - NodePort: Enabled (Range: 30000-32767)
# - LoadBalancer: Enabled
# - externalIPs: Enabled
# - HostPort: Enabled
# BPF Maps: dynamic sizing: on (ratio: 0.002500)
# ...
# Native Routing 확인 : # 192.168.0.0/16 대역은 IP Masq 없이 라우팅
$ c0 status | grep KubeProxyReplacement
# => KubeProxyReplacement: True [enp0s3 10.0.2.15 fe80::6d:daff:feb3:d4d3, enp0s8 192.168.10.10 fe80::27ff:fe8a:de00 (Direct Routing)]
# enableIPv4Masquerade=true(기본값) , bpf.masquerade=true 확인
$ cilium config view | egrep 'enable-ipv4-masquerade|enable-bpf-masquerade'
# => enable-bpf-masquerade true
# enable-ipv4-masquerade true
$ c0 status --verbose | grep Masquerading
# => Masquerading: BPF [enp0s3, enp0s8] 192.168.0.0/16 [IPv4: Enabled, IPv6: Disabled]
# Configure the eBPF-based ip-masq-agent
# https://docs.cilium.io/en/stable/network/concepts/masquerading/
$ helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values --set ipMasqAgent.enabled=true
# => Release "cilium" has been upgraded. Happy Helming!
# ...
#
$ cilium config view | grep -i masq
# => enable-bpf-masquerade true
# enable-ip-masq-agent true
# ...
$ export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k3s-m -o jsonpath='{.items[0].metadata.name}')
$ alias c0="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium"
$ c0 status --verbose | grep Masquerading
# => Masquerading: BPF (ip-masq-agent) [enp0s3, enp0s8] 192.168.0.0/16 [IPv4: Enabled, IPv6: Disabled]
# <span style="color: green;">👉 BPF가 BPF (ip-masq-agent)로 변경되었습니다.</span>
# <span style="color: green;"> ip-masq-agent는 k8s 클러스터 내부에서 IP 마스커레이딩을 관리하는 컴포넌트로,</span>
# <span style="color: green;"> 마스커레이딩 여부를 결정하여 네트워크 자원 사용을 최적화 해줍니다.</span>
$ kubectl get cm -n kube-system cilium-config -o yaml | grep ip-masq
# => enable-ip-masq-agent: "true"
Cilium 기본정보 확인
변수 & 단축키
# cilium 파드 이름
$ export CILIUMPOD0=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k3s-m -o jsonpath='{.items[0].metadata.name}')
$ export CILIUMPOD1=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k3s-w1 -o jsonpath='{.items[0].metadata.name}')
$ export CILIUMPOD2=$(kubectl get -l k8s-app=cilium pods -n kube-system --field-selector spec.nodeName=k3s-w2 -o jsonpath='{.items[0].metadata.name}')
# 단축키(alias) 지정
$ alias c0="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium"
$ alias c1="kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium"
$ alias c2="kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium"
$ alias c0bpf="kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- bpftool"
$ alias c1bpf="kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- bpftool"
$ alias c2bpf="kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- bpftool"
# Hubble UI 웹 접속
$ kubectl patch -n kube-system svc hubble-ui -p '{"spec": {"type": "NodePort"}}'
# => service/hubble-ui patched
$ HubbleUiNodePort=$(kubectl get svc -n kube-system hubble-ui -o jsonpath={.spec.ports[0].nodePort})
$ echo -e "Hubble UI URL = http://$(curl -s ipinfo.io/ip):$HubbleUiNodePort"
# => Hubble UI URL = http://54.180.146.116:30732
자주 쓰는 Cilium CLI 명령어
# cilium 파드 확인
$ kubectl get pod -n kube-system -l k8s-app=cilium -owide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# cilium-5gx8w 1/1 Running 0 9m12s 192.168.10.102 k3s-w2 <none> <none>
# cilium-92k6s 1/1 Running 0 8m53s 192.168.10.101 k3s-w1 <none> <none>
# cilium-zv22g 1/1 Running 0 9m12s 192.168.10.10 k3s-m <none> <none>
# cilium 파드 재시작
$ kubectl -n kube-system rollout restart ds/cilium
# => daemonset.apps/cilium restarted
# 혹은
$ kubectl delete pod -n kube-system -l k8s-app=cilium
# => pod "cilium-5p4q4" deleted
# pod "cilium-bc7jz" deleted
# pod "cilium-zqs9l" deleted
# cilium 설정 정보 확인
$ cilium config view
# cilium 파드의 cilium 상태 확인
$ c0 status --verbose
# cilium 엔드포인트 확인
$ kubectl get ciliumendpoints -A
# => NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# kube-system coredns-7b98449c4-x5756 11970 ready 172.16.1.21
# kube-system hubble-relay-88f7f89d4-fcq2s 4124 ready 172.16.1.99
# kube-system hubble-ui-59bb4cb67b-r48tz 37523 ready 172.16.0.223
# kube-system local-path-provisioner-6795b5f9d8-84m96 11088 ready 172.16.2.184
# kube-system metrics-server-cdcc87586-g5m2d 27624 ready 172.16.2.223
$ c0 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# 640 Disabled Disabled 37523 k8s:app.kubernetes.io/name=hubble-ui 172.16.0.223 ready
# k8s:app.kubernetes.io/part-of=cilium
# k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
# k8s:io.cilium.k8s.policy.cluster=default
# k8s:io.cilium.k8s.policy.serviceaccount=hubble-ui
# k8s:io.kubernetes.pod.namespace=kube-system
# k8s:k8s-app=hubble-ui
# 1196 Disabled Disabled 1 k8s:node-role.kubernetes.io/control-plane=true ready
# k8s:node-role.kubernetes.io/master=true
# k8s:node.kubernetes.io/instance-type=k3s
# reserved:host
# 1598 Disabled Disabled 4 reserved:health 172.16.0.26 ready
$ c0 bpf endpoint list
# => IP ADDRESS LOCAL ENDPOINT INFO
# 172.16.0.26:0 id=1598 sec_id=4 flags=0x0000 ifindex=17 mac=CE:24:50:27:35:B9 nodemac=FE:57:C1:AD:A6:28
# 172.16.0.223:0 id=640 sec_id=37523 flags=0x0000 ifindex=9 mac=AE:B8:81:A6:B1:28 nodemac=C2:21:B8:92:DA:FD
# 172.16.0.227:0 (localhost)
# 192.168.10.10:0 (localhost)
# 10.0.2.15:0 (localhost)
$ c0 map get cilium_lxc
# => Key Value State Error
# 172.16.0.223:0 id=640 sec_id=37523 flags=0x0000 ifindex=9 mac=AE:B8:81:A6:B1:28 nodemac=C2:21:B8:92:DA:FD sync
# 172.16.0.26:0 id=1598 sec_id=4 flags=0x0000 ifindex=17 mac=CE:24:50:27:35:B9 nodemac=FE:57:C1:AD:A6:28 sync
$ c0 ip list
# Manage the IPCache mappings for IP/CIDR <-> Identity
$ c0 bpf ipcache list
# Service/NAT List 확인
$ c0 service list
# => ID Frontend Service Type Backend
# 1 10.10.200.1:443 ClusterIP 1 => 192.168.10.10:6443 (active)
# ...
# 6 10.10.200.10:9153 ClusterIP 1 => 172.16.1.21:9153 (active)
# 7 10.10.200.180:443 ClusterIP 1 => 172.16.2.223:10250 (active)
# 16 0.0.0.0:30732 NodePort 1 => 172.16.0.223:8081 (active)
# 17 10.0.2.15:30732 NodePort 1 => 172.16.0.223:8081 (active)
# 18 192.168.10.10:30732 NodePort 1 => 172.16.0.223:8081 (active)
$ c0 bpf lb list
# => SERVICE ADDRESS BACKEND ADDRESS (REVNAT_ID) (SLOT)
# 10.10.200.1:443 (0) 0.0.0.0:0 (1) (0) [ClusterIP, non-routable]
# ...
# 10.10.200.10:9153 (0) 0.0.0.0:0 (6) (0) [ClusterIP, non-routable]
# 10.10.200.180:443 (1) 172.16.2.223:10250 (7) (1)
# 0.0.0.0:30732 (0) 0.0.0.0:0 (16) (0) [NodePort, non-routable]
# 192.168.10.10:30732 (0) 0.0.0.0:0 (18) (0) [NodePort]
# 10.0.2.15:30732 (0) 0.0.0.0:0 (17) (0) [NodePort]
$ c0 bpf lb list --revnat
# => ID BACKEND ADDRESS (REVNAT_ID) (SLOT)
# 7 10.10.200.180:443
# 6 10.10.200.10:9153
# ...
# 1 10.10.200.1:443
# 17 10.0.2.15:30732
# 16 0.0.0.0:30732
# 18 192.168.10.10:30732
$ c0 bpf nat list
# => TCP OUT 192.168.10.10:34576 -> 192.168.10.101:4240 XLATE_SRC 192.168.10.10:34576 Created=409sec ago NeedsCT=1
# TCP IN 192.168.10.101:4240 -> 192.168.10.10:34576 XLATE_DST 192.168.10.10:34576 Created=409sec ago NeedsCT=1
# ICMP OUT 192.168.10.10:35656 -> 172.16.1.224:0 XLATE_SRC 192.168.10.10:35656 Created=169sec ago NeedsCT=1
# ICMP OUT 192.168.10.10:35656 -> 192.168.10.102:0 XLATE_SRC 192.168.10.10:35656 Created=169sec ago NeedsCT=1
# ...
# List all open BPF maps
$ c0 map list
# => Name Num entries Num errors Cache enabled
# cilium_lb4_backends_v3 2 0 true
# cilium_lb4_source_range 0 0 true
# cilium_policy_01196 2 0 true
# cilium_policy_01598 3 0 true
# ...
$ c0 map list --verbose
# List contents of a policy BPF map : Dump all policy maps
$ c0 bpf policy get --all
$ c0 bpf policy get --all -n
# cilium monitor
$ c0 monitor -v
# => Listening for events on 4 CPUs with 64x4096 of shared memory
# Press Ctrl-C to quit
# time="2024-10-01T12:11:28Z" level=info msg="Initializing dissection cache..." subsys=monitor
# -> network flow 0x206747a1 , identity health->remote-node state reply ifindex enp0s8 orig-ip 0.0.0.0: 172.16.0.26:4240 -> 192.168.10.102:39142 tcp ACK
# -> endpoint 1598 flow 0x0 , identity remote-node->health state established ifindex lxc_health orig-ip 192.168.10.102: 192.168.10.102:39142 -> 172.16.0.26:4240 tcp ACK
# -> endpoint 640 flow 0x665dd157 , identity host->37523 state new ifindex lxcdd00fea91485 orig-ip 10.0.2.15: 10.0.2.15:37040 -> 172.16.0.223:8081 tcp SYN
# -> stack flow 0xf0f2648f , identity 37523->host state reply ifindex 0 orig-ip 0.0.0.0: 172.16.0.223:8081 -> 10.0.2.15:37040 tcp SYN, ACK
# -> endpoint 640 flow 0x665dd157 , identity host->37523 state established ifindex lxcdd00fea91485 orig-ip 10.0.2.15: 10.0.2.15:37040 -> 172.16.0.223:8081 tcp ACK
# <span style="color: green;">👉 cilium monitor는 자체적으로 마치 tcpdump처럼 패킷의 이동을 모니터링 할 수 있습니다.</span>
$ c0 monitor -v --type l7 # Layer 7 (Application layer) 만을 모니터링할 수도 있습니다.
# => CPU 01: [pre-xlate-rev] cgroup_id: 5161 sock_cookie: 14187, dst [10.0.2.15]:43070 tcp
# CPU 01: [pre-xlate-rev] cgroup_id: 2800 sock_cookie: 14188, dst [172.16.0.223]:8081 tcp
# CPU 01: [pre-xlate-rev] cgroup_id: 5161 sock_cookie: 14189, dst [10.0.2.15]:43080 tcp
# CPU 01: [pre-xlate-rev] cgroup_id: 2800 sock_cookie: 10103, dst [172.16.0.223]:8081 tcp
# ...
네트워크 기본 정보 확인 : k3s-w1/w2 에 SSH 접속 후 ip -c link/route 정보 확인
# 네트워크 인터페이스 정보 확인
$ ip -br -c link
# => lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
# enp0s3 UP 02:6d:da:b3:d4:d3 <BROADCAST,MULTICAST,UP,LOWER_UP>
# enp0s8 UP 08:00:27:35:1b:07 <BROADCAST,MULTICAST,UP,LOWER_UP>
# cilium_net@cilium_host UP 16:9d:bb:67:fc:a0 <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP>
# cilium_host@cilium_net UP 7e:ea:43:69:1e:4b <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP>
# lxcb0b60514c056@if10 UP be:61:78:59:9a:b9 <BROADCAST,MULTICAST,UP,LOWER_UP>
# lxcb6d4cfe53c6a@if12 UP b6:62:74:02:2f:32 <BROADCAST,MULTICAST,UP,LOWER_UP>
# lxc_health@if18 UP aa:7e:99:a8:fd:c3 <BROADCAST,MULTICAST,UP,LOWER_UP>
$ ip -br -c addr
# => lo UNKNOWN 127.0.0.1/8 ::1/128
# enp0s3 UP 10.0.2.15/24 metric 100 fe80::6d:daff:feb3:d4d3/64
# enp0s8 UP 192.168.10.101/24 fe80::a00:27ff:fe35:1b07/64
# cilium_net@cilium_host UP fe80::149d:bbff:fe67:fca0/64
# cilium_host@cilium_net UP 172.16.1.82/32 fe80::7cea:43ff:fe69:1e4b/64
# lxcb0b60514c056@if10 UP fe80::bc61:78ff:fe59:9ab9/64
# lxcb6d4cfe53c6a@if12 UP fe80::b462:74ff:fe02:2f32/64
# lxc_health@if18 UP fe80::a87e:99ff:fea8:fdc3/64
--------------------------------------------
# cilium_net 과 cilium_host 는 veth peer 관계이며, cilium_host 는 파드의 GW IP 주소로 지정되며 32bit 이다
# => 4: cilium_net@cilium_host: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
# link/ether 16:9d:bb:67:fc:a0 brd ff:ff:ff:ff:ff:ff
# inet6 fe80::149d:bbff:fe67:fca0/64 scope link
# valid_lft forever preferred_lft forever
# 5: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
# link/ether 7e:ea:43:69:1e:4b brd ff:ff:ff:ff:ff:ff
# inet 172.16.1.82/32 scope global cilium_host
# valid_lft forever preferred_lft forever
# inet6 fe80::7cea:43ff:fe69:1e4b/64 scope link
# valid_lft forever preferred_lft forever
# proxy arp 는 disable(0) 상태이며, 파드와 연결된 lxc 도 모두 0 이다
# 파드의 32bit ip의 gw 가 각각 연결된 veth 인터페이스의 mac 으로 cilium_host 의 IP/MAC 응답을 처리한다, 어떻게 동작이 되는걸까요? >> eBPF program!!!
$ cat /proc/sys/net/ipv4/conf/cilium_net/proxy_arp
# => 0
$ cat /proc/sys/net/ipv4/conf/cilium_host/proxy_arp
# => 0
# lxc_health 인터페이스는 veth 로 cilium(NET NS 0, 호스트와 다름)과 veth pair 이다 - 링크
# cilium 인터페이스에 파드 IP가 할당되어 있으며, cilium-health-responder 로 동작한다
$ lsns -t net
# => NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
# 4026531840 net 133 1 root unassigned /sbin/init
# 4026532195 net 2 8433 65535 1 /run/netns/cni-0943b57a-e695-904e-87f6-7edb4fb0cd92 /pause
# 4026532350 net 1 18060 root 0 cilium-health-responder --listen 4240 --pidfile /var/run/cilium/state/health-endp
# 4026532359 net 2 10652 65535 2 /run/netns/cni-1b359731-9a53-2db0-eee1-63b0c5643c27 /pause
Hubble
Cillium은 Hubble을 통해 통신 및 서비스와 네트워킹 인프라의 동작에 대한 심층적인 가시성을 완전히 투명한 방식으로 관찰성을 제공합니다. - 참고
- Hubble은 완전히 분산된 네트워킹 및 보안 모니터링 플랫폼입니다
- Cilium 과 eBPF 기반으로 동작하며, 어플리케이션 수정없이도 보다 심도있는 가시성을 제공합니다.
- Hubble은 컨테이너 기반 워크로드 뿐만 아니라 전통적인 표준 리눅스 프로세스나 VM 기반 워크로드에 대해서도 가시성을 제공합니다.
- eBPF를 사용함으로써 전통적인 IP 기반이 아닌 서비스/파드/컨테이너 수준의 네트워크 트래픽에 대해서 보안 가시성 및 통제를 제공할 수 있습니다. 또한 어플리케이션 레이어(L7)에서 필터링 할 수 도 있습니다.
- 기본적으로 Hubble API는 Cilium 에이전트가 실행되는 개별 노드의 범위 내에서 작동합니다.
이는 네트워크 통찰력을 로컬 Cilium 에이전트가 관찰한 트래픽으로 제한합니다.
Hubble CLI(hubble
)를 사용하여 로컬 Unix Domain Socket을 통해 제공된 Hubble API를 쿼리할 수 있습니다. Hubble CLI 바이너리는 기본적으로 Cilium 에이전트 포드에 설치됩니다. -
Hubble Relay를 배포하면 전체 클러스터 또는 ClusterMesh 시나리오의 여러 클러스터에 대한 네트워크 가시성이 제공됩니다. 이 모드에서 Hubble 데이터는 Hubble CLI(
hubble
)를 Hubble Relay 서비스로 지정하거나 Hubble UI를 통해 액세스할 수 있습니다. Hubble UI는 L3/L4 및 L7 계층에서 서비스 종속성 그래프를 자동으로 검색할 수 있는 웹 인터페이스로, 사용자 친화적인 시각화 및 서비스 맵으로서의 데이터 흐름 필터링을 허용합니다.Hubble Relay를 통한 전체 클러스터 모니터링 링크
- 메트릭은 Prometheus로 수집되며, Grafana를 통해 시각화할 수도 있어서 기존의 대시보드와 통합도 가능합니다.
https://cilium.io/blog/2019/11/19/announcing-hubble/
- Hubble UI 화면
서비스 종속성 그래프
- 통제 예시
-
/public/.*
경로에 대한GET
요청만 허용하고, 다른 모든 요청은 거부 -
service1
이topic1
이라는 토픽을 생산하고,service2
가topic1
을 소비하도록 허용하고, 그 외의 모든 카프카 메시지는 거부 - HTTP 헤더에
X-Token: [0-9]+
가 포함된 모든 요청을 허용하고, 그렇지 않은 요청은 거부
-
Hubble UI/CLI 접근 및 확인
# 확인
$ cilium status
# => /¯¯\
# /¯¯\__/¯¯\ Cilium: OK
# \__/¯¯\__/ Operator: OK
# /¯¯\__/¯¯\ Envoy DaemonSet: OK
# \__/¯¯\__/ Hubble Relay: OK
# \__/ ClusterMesh: disabled
#
# DaemonSet cilium Desired: 3, Ready: 3/3, Available: 3/3
# ...
# UI 파드 정보 확인
$ kubectl get pod -n kube-system -l k8s-app=hubble-ui -o wide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# hubble-ui-59bb4cb67b-r48tz 2/2 Running 2 (87m ago) 101m 172.16.0.223 k3s-m <none> <none>
# Hubble UI 웹 접속
$ kubectl patch -n kube-system svc hubble-ui -p '{"spec": {"type": "NodePort"}}'
# => service/hubble-ui patched
$ HubbleUiNodePort=$(kubectl get svc -n kube-system hubble-ui -o jsonpath={.spec.ports[0].nodePort})
$ echo -e "Hubble UI URL = http://$(curl -s ipinfo.io/ip):$HubbleUiNodePort"
# => Hubble UI URL = http://54.180.146.116:30732
## Service NodePort 생성 후 아래 정보 확인!
$ iptables -t nat -S
# => -P PREROUTING ACCEPT
# -P INPUT ACCEPT
# -P OUTPUT ACCEPT
# -P POSTROUTING ACCEPT
# ...
# -N KUBE-EXT-ZGWW2L4XLRSDZ3EF
# -N KUBE-MARK-MASQ
# -N KUBE-NODEPORTS
# ...
# -N KUBE-SEP-UOFUVE4S3JB7NP6T
# -N KUBE-SERVICES
# ...
# # <span style="color: green;">👉 (1)</span>-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# ...
# # <span style="color: green;">👉 (4)</span>-A KUBE-EXT-ZGWW2L4XLRSDZ3EF -m comment --comment "masquerade traffic for kube-system/hubble-ui:http external destinations" -j KUBE-MARK-MASQ
# # <span style="color: green;">👉 (5)</span>-A KUBE-EXT-ZGWW2L4XLRSDZ3EF -j KUBE-SVC-ZGWW2L4XLRSDZ3EF
# -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
# # <span style="color: green;">👉 (3)</span>-A KUBE-NODEPORTS -p tcp -m comment --comment "kube-system/hubble-ui:http" -m tcp --dport 30732 -j KUBE-EXT-ZGWW2L4XLRSDZ3EF
# ...
# # <span style="color: green;">👉 (8)</span>-A KUBE-SEP-UOFUVE4S3JB7NP6T -s 172.16.0.223/32 -m comment --comment "kube-system/hubble-ui:http" -j KUBE-MARK-MASQ
# # <span style="color: green;">👉 (9)</span>-A KUBE-SEP-UOFUVE4S3JB7NP6T -p tcp -m comment --comment "kube-system/hubble-ui:http" -m tcp -j DNAT --to-destination 172.16.0.223:8081
# ...
# # <span style="color: green;">👉 (2)</span>-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
# ...
# # <span style="color: green;">👉 (6)</span>-A KUBE-SVC-ZGWW2L4XLRSDZ3EF ! -s 172.16.0.0/16 -d 10.10.200.125/32 -p tcp -m comment --comment "kube-system/hubble-ui:http cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
# # <span style="color: green;">👉 (7)</span>-A KUBE-SVC-ZGWW2L4XLRSDZ3EF -m comment --comment "kube-system/hubble-ui:http -> 172.16.0.223:8081" -j KUBE-SEP-UOFUVE4S3JB7NP6T
# ...
# <span style="color: green;">👉 (1) PREROUTING => (2) KUBE-SERVICES => (3) KUBE-NODEPORTS </span>
# <span style="color: green;"> if 노드 포트인 30732로 접속:</span>
# <span style="color: green;"> => (4,5) KUBE-EXT-ZGWW2L4XLRSDZ3EF</span>
# <span style="color: green;"> => (6,7) KUBE-SVC-ZGWW2L4XLRSDZ3EF => (8,9) KUBE-SEP-UOFUVE4S3JB7NP6T => 172.16.0.223:8081</span>
$ conntrack -L
$ conntrack -L |grep -v 2379
# Install Hubble Client
$ 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{,.sha256sum}
$ sha256sum --check hubble-linux-${HUBBLE_ARCH}.tar.gz.sha256sum
$ sudo tar xzvfC hubble-linux-${HUBBLE_ARCH}.tar.gz /usr/local/bin
# => hubble
$ rm hubble-linux-${HUBBLE_ARCH}.tar.gz{,.sha256sum}
# Hubble API Access : localhost TCP 4245 Relay 를 통해 접근, observe 를 통해서 flow 쿼리 확인 가능!
$ cilium hubble port-forward &
# => [1] 16534
# CLI 로 Hubble API 상태 확인
$ hubble status
# => Healthcheck (via localhost:4245): Ok
# Current/Max Flows: 12,285/12,285 (100.00%)
# Flows/s: 24.50
# Connected Nodes: 3/3
# query the flow API and look for flows
$ hubble observe
# => Oct 01 13:26:17.501: kube-system/local-path-provisioner-6795b5f9d8-84m96:60994 (ID:11088) -> 192.168.10.10:6443 (kube-apiserver) to-network FORWARDED (TCP Flags: ACK)
# Oct 01 13:26:17.660: kube-system/hubble-ui-59bb4cb67b-r48tz:47372 (ID:37523) -> kube-system/hubble-relay-88f7f89d4-fcq2s:4245 (ID:4124) to-network FORWARDED (TCP Flags: ACK)
# Oct 01 13:26:17.797: 127.0.0.1:36150 (world) <> kube-system/hubble-ui-59bb4cb67b-r48tz (ID:37523) pre-xlate-rev TRACED (TCP)
# Oct 01 13:26:17.914: 192.168.10.102:38716 (host) -> 192.168.10.10:6443 (kube-apiserver) to-network FORWARDED (TCP Flags: ACK)
# Oct 01 13:26:18.011: 127.0.0.1:36160 (world) <> kube-system/hubble-ui-59bb4cb67b-r48tz (ID:37523) pre-xlate-rev TRACED (TCP)
# Oct 01 13:26:18.067: 10.0.2.15:59486 (host) -> kube-system/metrics-server-cdcc87586-g5m2d:10250 (ID:27624) to-endpoint FORWARDED (TCP Flags: SYN)
# Oct 01 13:26:18.067: 10.0.2.15:59486 (host) <- kube-system/metrics-server-cdcc87586-g5m2d:10250 (ID:27624) to-stack FORWARDED (TCP Flags: SYN, ACK)
# ...
# hubble observe --pod netpod
# hubble observe --namespace galaxy --http-method POST --http-path /v1/request-landing
# hubble observe --pod deathstar --protocol http
# hubble observe --pod deathstar --verdict DROPPED
노드간 파드 통신
-
Endpoint to Endpoint 통신 패킷 흐름도
-
Egress to Endpoint 통신 패킷 흐름도
-
Ingress to Endpoint 통신 패킷 흐름도
-
파드 생성 및 확인
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: netpod
labels:
app: netpod
spec:
nodeName: k3s-m
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod1
labels:
app: webpod
spec:
nodeName: k3s-w1
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod2
labels:
app: webpod
spec:
nodeName: k3s-w2
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
EOF
# => pod/netpod created
# pod/webpod1 created
# pod/webpod2 created
# 확인
$ kubectl get pod -o wide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# netpod 1/1 Running 0 36s 172.16.0.147 k3s-m <none> <none>
# webpod1 1/1 Running 0 36s 172.16.1.247 k3s-w1 <none> <none>
# webpod2 1/1 Running 0 36s 172.16.2.84 k3s-w2 <none> <none>
$ c0 status --verbose | grep Allocated -A5
# => Allocated addresses:
# 172.16.0.147 (default/netpod)
# 172.16.0.223 (kube-system/hubble-ui-59bb4cb67b-r48tz [restored])
# 172.16.0.227 (router)
# 172.16.0.26 (health)
# IPv4 BIG TCP: Disabled
$ c1 status --verbose | grep Allocated -A5
# => Allocated addresses:
# 172.16.1.21 (kube-system/coredns-7b98449c4-x5756 [restored])
# 172.16.1.224 (health)
# 172.16.1.247 (default/webpod1)
# 172.16.1.82 (router)
# 172.16.1.99 (kube-system/hubble-relay-88f7f89d4-fcq2s [restored])
$ c2 status --verbose | grep Allocated -A5
# => Allocated addresses:
# 172.16.2.184 (kube-system/local-path-provisioner-6795b5f9d8-84m96 [restored])
# 172.16.2.223 (kube-system/metrics-server-cdcc87586-g5m2d [restored])
# 172.16.2.25 (router)
# 172.16.2.8 (health)
# 172.16.2.84 (default/webpod2)
$ kubectl get ciliumendpoints
# => NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# netpod 27629 ready 172.16.0.147
# webpod1 64309 ready 172.16.1.247
# webpod2 64309 ready 172.16.2.84
$ kubectl get ciliumendpoints -A
# => root@k3s-m:~# kubectl get ciliumendpoints -A
# NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# default netpod 27629 ready 172.16.0.147
# default webpod1 64309 ready 172.16.1.247
# default webpod2 64309 ready 172.16.2.84
# kube-system coredns-7b98449c4-x5756 11970 ready 172.16.1.21
# kube-system hubble-relay-88f7f89d4-fcq2s 4124 ready 172.16.1.99
# kube-system hubble-ui-59bb4cb67b-r48tz 37523 ready 172.16.0.223
# kube-system local-path-provisioner-6795b5f9d8-84m96 11088 ready 172.16.2.184
# kube-system metrics-server-cdcc87586-g5m2d 27624 ready 172.16.2.223
$ c0 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# 640 Disabled Disabled 37523 k8s:app.kubernetes.io/name=hubble-ui 172.16.0.223 ready
# k8s:app.kubernetes.io/part-of=cilium
# k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system
# k8s:io.cilium.k8s.policy.cluster=default
# k8s:io.cilium.k8s.policy.serviceaccount=hubble-ui
# k8s:io.kubernetes.pod.namespace=kube-system
# k8s:k8s-app=hubble-ui
# ...
$ c0 bpf endpoint list
# => IP ADDRESS LOCAL ENDPOINT INFO
# 192.168.10.10:0 (localhost)
# 10.0.2.15:0 (localhost)
# 172.16.0.147:0 id=2724 sec_id=27629 flags=0x0000 ifindex=19 mac=52:59:78:2F:C0:BE nodemac=7E:77:FA:0A:D3:CC
# 172.16.0.26:0 id=1598 sec_id=4 flags=0x0000 ifindex=17 mac=CE:24:50:27:35:B9 nodemac=FE:57:C1:AD:A6:28
# 172.16.0.223:0 id=640 sec_id=37523 flags=0x0000 ifindex=9 mac=AE:B8:81:A6:B1:28 nodemac=C2:21:B8:92:DA:FD
# 172.16.0.227:0 (localhost)
$ c0 map get cilium_lxc
# => Key Value State Error
# 172.16.0.147:0 id=2724 sec_id=27629 flags=0x0000 ifindex=19 mac=52:59:78:2F:C0:BE nodemac=7E:77:FA:0A:D3:CC sync
# 172.16.0.223:0 id=640 sec_id=37523 flags=0x0000 ifindex=9 mac=AE:B8:81:A6:B1:28 nodemac=C2:21:B8:92:DA:FD sync
# 172.16.0.26:0 id=1598 sec_id=4 flags=0x0000 ifindex=17 mac=CE:24:50:27:35:B9 nodemac=FE:57:C1:AD:A6:28 sync
$ c0 ip list
# => IP IDENTITY SOURCE
# 0.0.0.0/0 reserved:world
# 10.0.2.15/32 reserved:host
# reserved:kube-apiserver
# 172.16.0.26/32 reserved:health
# 172.16.0.147/32 k8s:app=netpod custom-resource
# k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default
# ...
- 파드 변수 지정
# 테스트 파드들 IP
$ NETPODIP=$(kubectl get pods netpod -o jsonpath='{.status.podIP}')
$ WEBPOD1IP=$(kubectl get pods webpod1 -o jsonpath='{.status.podIP}')
$ WEBPOD2IP=$(kubectl get pods webpod2 -o jsonpath='{.status.podIP}')
# 단축키(alias) 지정
$ alias p0="kubectl exec -it netpod -- "
$ alias p1="kubectl exec -it webpod1 -- "
$ alias p2="kubectl exec -it webpod2 -- "
- 파드의 ARP 동작 확인
# netpod 네트워크 정보 확인
$ p0 ip -c -4 addr
# => ...
# 18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link-netnsid 0
# inet 172.16.0.147/32 scope global eth0
$ p0 route -n
# => Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 0.0.0.0 172.16.0.227 0.0.0.0 UG 0 0 0 eth0
# 172.16.0.227 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
$ p0 ping -c 1 $WEBPOD1IP && p0 ping -c 1 $WEBPOD2IP
# => PING 172.16.1.247 (172.16.1.247) 56(84) bytes of data.
# 64 bytes from 172.16.1.247: icmp_seq=1 ttl=62 time=0.642 ms
#
# --- 172.16.1.247 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 0.642/0.642/0.642/0.000 ms
# PING 172.16.2.84 (172.16.2.84) 56(84) bytes of data.
# 64 bytes from 172.16.2.84: icmp_seq=1 ttl=62 time=0.716 ms
#
# --- 172.16.2.84 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 0.716/0.716/0.716/0.000 ms
$ p0 curl -s $WEBPOD1IP && p0 curl -s $WEBPOD2IP
# => Hostname: webpod1
# IP: 127.0.0.1
# IP: 172.16.1.247
# RemoteAddr: 172.16.0.147:51692
# GET / HTTP/1.1
# Host: 172.16.1.247
# User-Agent: curl/8.7.1
# Accept: */*
#
# Hostname: webpod2
# IP: 127.0.0.1
# IP: 172.16.2.84
# RemoteAddr: 172.16.0.147:32796
# GET / HTTP/1.1
# Host: 172.16.2.84
# User-Agent: curl/8.7.1
# Accept: */*
$ p0 curl -s $WEBPOD1IP:8080 ; p0 curl -s $WEBPOD2IP:8080
# => command terminated with exit code 7
# command terminated with exit code 7
$ p0 ping -c 1 8.8.8.8 && p0 curl -s wttr.in/seoul
# => PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=39.1 ms
#
# --- 8.8.8.8 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 39.095/39.095/39.095/0.000 ms
# Weather report: seoul
#
# \ / Partly cloudy
# _ /"".-. 16 °C
# \_( ). ← 4 km/h
# /(___(__) 10 km
# 0.0 mm
# ┌─────────────┐
# ┌──────────────────────────────┬───────────────────────┤ Sat 26 Oct ├───────────────────────┬──────────────────────────────┐
# │ Morning │ Noon └──────┬──────┘ Evening │ Night │
# ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
# │ \ / Sunny │ \ / Sunny │ Overcast │ \ / Partly Cloudy │
# │ .-. 16 °C │ .-. 21 °C │ .--. 20 °C │ _ /"".-. 19 °C │
# │ ― ( ) ― ↙ 5-7 km/h │ ― ( ) ― ↙ 5-6 km/h │ .-( ). ← 4-6 km/h │ \_( ). ↙ 4-7 km/h │
# │ `-’ 10 km │ `-’ 10 km │ (___.__)__) 10 km │ /(___(__) 10 km │
# │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ 0.0 mm | 0% │ 0.0 mm | 0% │
# └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
# ┌─────────────┐
# ┌──────────────────────────────┬───────────────────────┤ Sun 27 Oct ├───────────────────────┬──────────────────────────────┐
# │ Morning │ Noon └──────┬──────┘ Evening │ Night │
# ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
# │ Overcast │ Cloudy │ \ / Clear │ \ / Clear │
# │ .--. 16 °C │ .--. 19 °C │ .-. 19 °C │ .-. 17 °C │
# │ .-( ). ← 1 km/h │ .-( ). ↓ 4-5 km/h │ ― ( ) ― ↘ 9-13 km/h │ ― ( ) ― ↘ 5-7 km/h │
# │ (___.__)__) 10 km │ (___.__)__) 10 km │ `-’ 10 km │ `-’ 10 km │
# │ 0.0 mm | 0% │ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │
# └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
# ┌─────────────┐
# ┌──────────────────────────────┬───────────────────────┤ Mon 28 Oct ├───────────────────────┬──────────────────────────────┐
# │ Morning │ Noon └──────┬──────┘ Evening │ Night │
# ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
# │ \ / Sunny │ \ / Partly Cloudy │ Cloudy │ _`/"".-. Patchy rain ne…│
# │ .-. 16 °C │ _ /"".-. 20 °C │ .--. 20 °C │ ,\_( ). 19 °C │
# │ ― ( ) ― ← 5-6 km/h │ \_( ). ← 6-7 km/h │ .-( ). ↙ 6-9 km/h │ /(___(__) ← 8-10 km/h │
# │ `-’ 10 km │ /(___(__) 10 km │ (___.__)__) 10 km │ ‘ ‘ ‘ ‘ 10 km │
# │ / \ 0.0 mm | 0% │ 0.0 mm | 0% │ 0.0 mm | 0% │ ‘ ‘ ‘ ‘ 0.0 mm | 67% │
# └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
# Location: 서울특별시, 대한민국 [37.5666791,126.9782914]
#
# Follow @igor_chubin for wttr.in updates
$ p0 ip -c neigh
# => 172.16.0.227 dev eth0 lladdr 7e:77:fa:0a:d3:cc STALE
# hubble cli 확인
$ hubble observe --pod netpod
# => Oct 01 14:29:21.248: kube-system/coredns-7b98449c4-7kqtg:53 (ID:11970) <> default/netpod (ID:27629) pre-xlate-rev TRACED (UDP)
# Oct 01 14:29:21.248: kube-system/kube-dns:53 (world) <> default/netpod (ID:27629) post-xlate-rev TRANSLATED (UDP)
# Oct 01 14:29:21.248: default/netpod:34162 (ID:27629) -> 5.9.243.187:80 (world) to-network FORWARDED (TCP Flags: SYN)
# Oct 01 14:29:21.481: default/netpod:34162 (ID:27629) -> 5.9.243.187:80 (world) to-network FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:29:21.481: default/netpod:34162 (ID:27629) <- 5.9.243.187:80 (world) to-endpoint FORWARDED (TCP Flags: SYN, ACK)
# Oct 01 14:29:21.481: default/netpod:34162 (ID:27629) -> 5.9.243.187:80 (world) to-network FORWARDED (TCP Flags: ACK)
# Oct 01 14:29:21.816: default/netpod:34162 (ID:27629) <- 5.9.243.187:80 (world) to-endpoint FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:29:21.819: default/netpod:34162 (ID:27629) -> 5.9.243.187:80 (world) to-network FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:29:22.052: default/netpod:34162 (ID:27629) <- 5.9.243.187:80 (world) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:29:22.052: default/netpod:34162 (ID:27629) -> 5.9.243.187:80 (world) to-network FORWARDED (TCP Flags: ACK)
# ...
$ hubble observe --pod webpod1
# => Oct 01 14:33:40.392: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: SYN)
# Oct 01 14:33:40.392: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: SYN, ACK)
# Oct 01 14:33:40.392: default/netpod:60282 (ID:27629) <> default/webpod1 (ID:64309) pre-xlate-rev TRACED (TCP)
# Oct 01 14:33:40.392: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK)
# Oct 01 14:33:40.392: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:33:40.393: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:33:40.393: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:33:40.393: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:33:40.394: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK)
# Oct 01 14:33:40.405: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: SYN)
# Oct 01 14:33:40.406: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: SYN, ACK)
# Oct 01 14:33:40.406: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK)
# Oct 01 14:33:40.406: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:33:40.407: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK, PSH)
# Oct 01 14:33:40.407: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:33:40.408: default/netpod:60282 (ID:27629) <- default/webpod1:80 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
# Oct 01 14:33:40.408: default/netpod:60282 (ID:27629) -> default/webpod1:80 (ID:64309) to-network FORWARDED (TCP Flags: ACK)
# <span style="color: green;">👉 80포트로 접속했을때 정상적인 응답 패킷들</span>
# Oct 01 14:33:43.956: default/netpod:34488 (ID:27629) -> default/webpod1:8080 (ID:64309) to-endpoint FORWARDED (TCP Flags: SYN)
# Oct 01 14:33:43.956: default/netpod:34488 (ID:27629) <- default/webpod1:8080 (ID:64309) to-network FORWARDED (TCP Flags: ACK, RST)
# Oct 01 14:33:43.970: default/netpod:34488 (ID:27629) -> default/webpod1:8080 (ID:64309) to-network FORWARDED (TCP Flags: SYN)
# Oct 01 14:33:43.970: default/netpod:34488 (ID:27629) <- default/webpod1:8080 (ID:64309) to-endpoint FORWARDED (TCP Flags: ACK, RST)
# <span style="color: green;">👉 열려있지 않은 8080포트로 접속했을때 오류가 발생한 패킷들</span>
$ hubble observe --pod webpod2
# BPF maps : 목적지 파드와 통신 시 어느곳으로 보내야 될지 확인할 수 있다
$ c0 map get cilium_ipcache
# => Key Value State Error
# 172.16.2.182/32 identity=27624 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 172.16.2.8/32 identity=4 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 172.16.0.147/32 identity=27629 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.0.223/32 identity=37523 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 192.168.10.10/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.0.26/32 identity=4 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 10.0.2.15/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 192.168.10.102/32 identity=6 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.1.82/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.101, flags=<none> sync
# 172.16.2.237/32 identity=11970 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 172.16.2.216/32 identity=64309 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 0.0.0.0/0 identity=2 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.2.111/32 identity=11088 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 172.16.1.224/32 identity=4 encryptkey=0 tunnelendpoint=192.168.10.101, flags=<none> sync
# 172.16.2.25/32 identity=6 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 192.168.10.101/32 identity=6 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.0.148/32 identity=4124 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
# 172.16.1.247/32 identity=64309 encryptkey=0 tunnelendpoint=192.168.10.101, flags=<none> sync
# 172.16.0.227/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0, flags=<none> sync
$ c0 map get cilium_ipcache | grep $WEBPOD1IP
# => 172.16.1.247/32 identity=64309 encryptkey=0 tunnelendpoint=192.168.10.101, flags=<none> sync
# netpod 의 LXC 변수 지정
#$ LXC=<k3s-m의 가장 나중에 lxc 이름>
$ LXC=lxcd551b3b4058f
# 파드와 veth pair 에 IP가 없습니다! proxy_arp 도 없습니다! 하지만 GW MAC 요청 시 lxc(veth)의 MAC 으로 응답이 옵니다! >> eBPF Magic!
# Cilium hijacks ARP table of POD1, forces the next hop to be the peer end (host side) of the veth pair.
$ ip -c addr show dev $LXC
# => 23: lxcd551b3b4058f@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
# link/ether fa:32:54:4b:16:d7 brd ff:ff:ff:ff:ff:ff link-netns cni-8634ff07-8cb9-608a-1baf-dc929d510a84
# inet6 fe80::f832:54ff:fe4b:16d7/64 scope link
# valid_lft forever preferred_lft forever
webpod1,2와 wttr.in 접속 테스트 후 Hubble UI
서비스 통신 확인
소켓 기반 로드밸런싱 소개
- 참고 : https://velog.io/@haruband/K8SCilium-Socket-Based-LoadBalancing-%EA%B8%B0%EB%B2%95
- 네트워크 기반 로드밸런싱(왼쪽) vs 소켓 기반 로드밸런싱(오른쪽) 비교
위의 그림에서 처럼 네트워크 기반 로드밸런싱은 서비스를 통해 DNAT 되는 과정을 거치는데, 소켓기반 로드밸런싱은 DNAT하는 과정이 필요가 없습니다.
- 위의 그림을 풀어서 설명하자면 위의 예에서 Pod1에서 동작하는 앱이
connect()
시스템 콜을 이용해서 소켓을 연결할때 목적지 주소가 서비스 주소 (192.168.0.1)이면 소켓의 목적지 주소를 바로 백엔드 주소(10.0.0.2)으로 설정합니다. 이후 데이터 전송은 바로 백엔드 주소(10.0.0.2)로 전송되기 때문에 DNAT 변환 및 역변환 과정이 필요없어집니다. - 이는 cilium이 L7에서 파드/서비스의 의미를 이해하고 처리하기 때문입니다!
- 심지어 서비스 주소를 백엔드 주소로 변경하는 것은 시스템콜 레벨에서 이루어지며, 커널에서 패킷이 생성되기도 전입니다.
- 위의 그림을 풀어서 설명하자면 위의 예에서 Pod1에서 동작하는 앱이
- Socket Operations : BPF Socket Operations program은 root cgroup에 연결되며 TCP event(ESTABLISHED)에서 실행합니다.
- Socket send/recv : Socket send/recv 훅은 TCP socket의 모든 송수신 작업에서 실행되며, hook에서 검사/삭제/리다이렉션 할 수 있습니다.
-
connect()
와sendto()
소켓 함수에 연결된 프로그램(connect4
,sendmsg4
)에서는 소켓의 목적지 주소를 백엔드 주소와 포트로 변환하고, cilium_lb4_backends 맵에 백엔드 주소와 포트를 등록해놓습니다. 이후recvmsg()
소켓 함수에 연결된 프로그램(recvmsg4
)에서는 ilium_lb4_reverse_nat 맵을 이용해서 목적지 주소와 포트를 다시 서비스 주소와 포트로 변환합니다.cilium의 소켓 기반 로드밸런싱 동작 방식 - 링크
서비스 생성 및 접속 확인
# 서비스 생성
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Service
metadata:
name: svc
spec:
ports:
- name: svc-webport
port: 80
targetPort: 80
selector:
app: webpod
type: ClusterIP
EOF
# => service/svc created
# 서비스 생성 확인
$ kubectl get svc,ep svc
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/svc ClusterIP 10.10.200.121 <none> 80/TCP 13s
#
# NAME ENDPOINTS AGE
# endpoints/svc 172.16.1.247:80,172.16.2.216:80 13s
# 노드에 iptables 더이상 KUBE-SVC rule 이 생성되지 않는다!
$ iptables-save | grep KUBE-SVC
$ iptables-save | grep CILIUM
# 서비스IP를 변수에 지정
$ SVCIP=$(kubectl get svc svc -o jsonpath='{.spec.clusterIP}')
# Pod1 에서 Service(ClusterIP) 접속 트래픽 발생
$ kubectl exec netpod -- curl -s $SVCIP
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: ::1
# IP: 172.16.2.216
# IP: fe80::844e:b2ff:fed0:7d5
# RemoteAddr: 172.16.0.147:36844
# GET / HTTP/1.1
# Host: 10.10.200.121
# User-Agent: curl/8.7.1
# Accept: */*
$ kubectl exec netpod -- curl -s $SVCIP | grep Hostname
# => Hostname: webpod1
# 지속적으로 접속 트래픽 발생
$ SVCIP=$(kubectl get svc svc -o jsonpath='{.spec.clusterIP}')
$ while true; do kubectl exec netpod -- curl -s $SVCIP | grep Hostname;echo "-----";sleep 1;done
$ kubectl exec netpod -- tcpdump -enni any -q
# => 15:01:03.468433 eth0 Out ifindex 18 52:59:78:2f:c0:be 172.16.0.147.57744 > 172.16.1.247.80: tcp 0
# 15:01:03.468761 eth0 In ifindex 18 7e:77:fa:0a:d3:cc 172.16.1.247.80 > 172.16.0.147.57744: tcp 0
# 15:01:03.468801 eth0 Out ifindex 18 52:59:78:2f:c0:be 172.16.0.147.57744 > 172.16.1.247.80: tcp 0
# 15:01:03.469249 eth0 Out ifindex 18 52:59:78:2f:c0:be 172.16.0.147.57744 > 172.16.1.247.80: tcp 76
# 15:01:03.469852 eth0 In ifindex 18 7e:77:fa:0a:d3:cc 172.16.1.247.80 > 172.16.0.147.57744: tcp 0
# 15:01:03.469852 eth0 In ifindex 18 7e:77:fa:0a:d3:cc 172.16.1.247.80 > 172.16.0.147.57744: tcp 312
# 15:01:03.469888 eth0 Out ifindex 18 52:59:78:2f:c0:be 172.16.0.147.57744 > 172.16.1.247.80: tcp 0
# ...
# <span style="color: green;">👉 파드에서 SVC(ClusterIP) 접속 시 tcpdump 로 확인 >> 파드 내부 캡쳐인데,</span>
# <span style="color: green;"> SVC(10.10.200.121)는 보이지 않고, DNAT 된 web-pod 의 IP가 확인됩니다! It's Magic!</span>
$ kubectl exec netpod -- sh -c "ngrep -tW byline -d eth0 '' 'tcp port 80'"
# => T 2024/10/01 15:02:58.406586 172.16.0.147:48018 -> 172.16.2.216:80 [AP] #4
# GET / HTTP/1.1.
# Host: 10.10.200.121.
# User-Agent: curl/8.7.1.
# Accept: */*.
# ...
# 서비스 정보 확인
$ c0 service list
# => ID Frontend Service Type Backend
# ...
# 11 10.10.200.121:80 ClusterIP 1 => 172.16.1.247:80 (active)
# 2 => 172.16.2.216:80 (active)
$ c0 bpf lb list
# => SERVICE ADDRESS BACKEND ADDRESS (REVNAT_ID) (SLOT)
# 0.0.0.0:30405 (0) 0.0.0.0:0 (8) (0) [NodePort, non-routable]
# ...
# 10.10.200.121:80 (1) 172.16.1.247:80 (11) (1)
# 10.10.200.121:80 (2) 172.16.2.216:80 (11) (2)
# ...
# BPF maps
$ c0 map list --verbose
$ c0 map list --verbose | grep lb
# => ## Map: cilium_lb4_backends_v3
# ## Map: cilium_lb_affinity_match
# ## Map: cilium_lb4_source_range
# ## Map: cilium_lb4_affinity
# ## Map: cilium_lb4_services_v2
# ## Map: cilium_lb4_reverse_nat
# ## Map: cilium_lb4_reverse_sk
# ## Map: cilium_skip_lb4
$ c0 map get cilium_lb4_services_v2
# => Key Value State Error
# ...
# 10.10.200.121:80 (1) 20 0 (11) [0x0 0x0] sync
# 10.10.200.121:80 (0) 0 2 (11) [0x0 0x0] sync
# 10.10.200.121:80 (2) 21 0 (11) [0x0 0x0] sync
# ...
$ c0 map get cilium_lb4_backends_v3
# => Key Value State Error
# ...
# 21 ANY://172.16.2.216 sync
# 20 ANY://172.16.1.247 sync
# ...
$ c0 map get cilium_lb4_reverse_nat
# => Key Value State Error
# ...
# 11 10.10.200.121:80 sync
# ...
$ c0 map get cilium_lb4_reverse_sk
# => Key Value State Error
# ...
# [172.16.2.216]:20480, 22906 [10.10.200.121]:20480, 2816
# [172.16.1.247]:20480, 19024 [10.10.200.121]:20480, 2816
# [172.16.2.216]:20480, 30734 [10.10.200.121]:20480, 2816
# ...
$ c0 map get cilium_lxc
# => Key Value State Error
# 172.16.0.147:0 id=2724 sec_id=27629 flags=0x0000 ifindex=19 mac=52:59:78:2F:C0:BE nodemac=7E:77:FA:0A:D3:CC sync
# 172.16.0.223:0 id=640 sec_id=37523 flags=0x0000 ifindex=9 mac=AE:B8:81:A6:B1:28 nodemac=C2:21:B8:92:DA:FD sync
# 172.16.0.26:0 id=1606 sec_id=4 flags=0x0000 ifindex=21 mac=A2:52:92:B7:C0:D9 nodemac=C2:C0:3A:67:BE:B2 sync
# 172.16.0.148:0 id=655 sec_id=4124 flags=0x0000 ifindex=23 mac=2E:AF:13:F1:DD:88 nodemac=FA:32:54:4B:16:D7 sync
$ c0 map get cilium_ipcache
# => 172.16.2.216/32 identity=64309 encryptkey=0 tunnelendpoint=192.168.10.102, flags=<none> sync
# 172.16.1.247/32 identity=64309 encryptkey=0 tunnelendpoint=192.168.10.101, flags=<none> sync
Prometheus와 Grafana를 통한 Cilium 모니터링
설정
# 배포
$ kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/kubernetes/addons/prometheus/monitoring-example.yaml
# => namespace/cilium-monitoring created
# serviceaccount/prometheus-k8s created
# configmap/grafana-config created
# configmap/grafana-cilium-dashboard created
# configmap/grafana-cilium-operator-dashboard created
# configmap/grafana-hubble-dashboard created
# configmap/grafana-hubble-l7-http-metrics-by-workload created
# configmap/prometheus created
# clusterrole.rbac.authorization.k8s.io/prometheus created
# clusterrolebinding.rbac.authorization.k8s.io/prometheus created
# service/grafana created
# service/prometheus created
# deployment.apps/grafana created
# deployment.apps/prometheus created
$ kubectl get all -n cilium-monitoring
# => NAME READY STATUS RESTARTS AGE
# pod/grafana-65d4578dc4-zcmgz 1/1 Running 0 36s
# pod/prometheus-7cc8784659-bqchf 1/1 Running 0 36s
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/grafana ClusterIP 10.10.200.223 <none> 3000/TCP 36s
# service/prometheus ClusterIP 10.10.200.90 <none> 9090/TCP 36s
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/grafana 1/1 1 1 36s
# deployment.apps/prometheus 1/1 1 1 36s
#
# NAME DESIRED CURRENT READY AGE
# replicaset.apps/grafana-65d4578dc4 1 1 1 36s
# replicaset.apps/prometheus-7cc8784659 1 1 1 36s
# 파드와 서비스 확인
$ kubectl get pod,svc,ep -o wide -n cilium-monitoring
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# pod/grafana-65d4578dc4-zcmgz 1/1 Running 0 49s 172.16.1.208 k3s-w1 <none> <none>
# pod/prometheus-7cc8784659-bqchf 1/1 Running 0 49s 172.16.1.248 k3s-w1 <none> <none>
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# service/grafana ClusterIP 10.10.200.223 <none> 3000/TCP 49s app=grafana
# service/prometheus ClusterIP 10.10.200.90 <none> 9090/TCP 49s app=prometheus
#
# NAME ENDPOINTS AGE
# endpoints/grafana 172.16.1.208:3000 49s
# endpoints/prometheus 172.16.1.248:9090 49s
# NodePort 설정
$ kubectl patch svc grafana -n cilium-monitoring -p '{"spec": {"type": "NodePort"}}'
$ kubectl patch svc prometheus -n cilium-monitoring -p '{"spec": {"type": "NodePort"}}'
# Grafana 웹 접속
$ GPT=$(kubectl get svc -n cilium-monitoring grafana -o jsonpath={.spec.ports[0].nodePort})
$ echo -e "Grafana URL = http://$(curl -s ipinfo.io/ip):$GPT"
# => Grafana URL = http://54.180.146.116:32172
# Prometheus 웹 접속 정보 확인
$ PPT=$(kubectl get svc -n cilium-monitoring prometheus -o jsonpath={.spec.ports[0].nodePort})
$ echo -e "Prometheus URL = http://$(curl -s ipinfo.io/ip):$PPT"
# => Prometheus URL = http://54.180.146.116:30426
grafana , prometheus NodePort 로 웹 접속 후 확인
Prometheus 모니터링 화면
Grafana 모니터링 화면
Network Policy (L3, L4, L7)
Cilium 보안 소개
Cilium은 여러 레벨의 보안 기능을 제공합니다. 문서 그 중에서 다음 3가지를 알아보겠습니다.
- ID 기반 (L3) : 엔드포인트 간의 연결 정책을 정의할 때, 엔드포인트의 ID를 사용합니다. 이 ID는 엔드포인트의 네트워크 주소와 무관하게 유지되며,
k8s의 label을 통해 만들어집니다. 즉, 파드간에 공유할 수 있으며, 네트워크 주소가 변경되어도 유지됩니다.
https://docs.cilium.io/en/stable/security/network/identity/
- 포트기반 (L4) : 엔드포인트 간의 연결 정책을 정의할 때, 포트를 사용합니다. 이는 L3 정책과 함께 사용될 수 있어서,
role=frontend
라는 레이블을 가진 엔드포인트는 443 포트로 outgoing 연결을 허용하고,role=backend
라는 레이블을 가진 엔드포인트는 443 포트로 incoming 연결을 허용하는 등의 정책을 정의할 수 있습니다. - 어플리케이션 (http) 기반 (L7) : HTTP통신과 RPC 프로토콜의 보안을 위해서 어플리케이션 레벨에서 정밀하게 정책을 정의할 수 있습니다. 이는 HTTP 헤더, 메소드, 경로, 쿼리 파라미터 등을 사용하여 정책을 정의할 수 있습니다.
Network Policy 관련 eBPF Datapath
- Prefilter : Prefilter는 XDP 프로그램을 통해 수행되며, 최고의 성능을 위해 네트워크 패킷을 필터링하는 prefilter 규칙들을 제공합니다. 특히 CIDR map들을 사용해 IP 주소를 필터링하는 등의 동작을 할 수 있습니다.
- Endpoint policy : 정책에 따라 패킷을 차단/전달하거나, 서비스로 전달하거나, L7로 정책 전달을 할 수 있습니다.
- Cilium datapath는 L3와 L4 정책을 강제하거나, 패킷과 ID를 매핑하는 역할을 수행합니다.
- L7 policy : L7 정책은 프록시 트래픽을 Cilium의 userspace proxy instance, 즉 Envoy로 전달합니다. Envoy 는 트래픽을 전달하거나
L7 정책에 의해 차단할 수 있습니다.
- 👉 L7 정책은 hook과 Userspace Proxy(envoy)를 사용하기 때문에 성능이 조금 떨어질 수 있습니다.
실습
- 스타워즈에서 영감받은 예제를 통해 Network Policy를 적용해보겠습니다.
- 디플로이먼트(웹 서버, deathstar, replicas 2), 파드(xwing, tiefighter), 서비스(ClusterIP, service/deathstar)
# 배포
$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/minikube/http-sw-app.yaml
# => service/deathstar created
# deployment.apps/deathstar created
# pod/tiefighter created
# pod/xwing created
$ kubectl get all
# => NAME READY STATUS RESTARTS AGE
# pod/deathstar-689f66b57d-mm72v 1/1 Running 0 15s
# pod/deathstar-689f66b57d-pz56p 1/1 Running 0 15s
# pod/tiefighter 1/1 Running 0 15s
# pod/xwing 1/1 Running 0 15s
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/deathstar ClusterIP 10.10.200.162 <none> 80/TCP 15s
# service/kubernetes ClusterIP 10.10.200.1 <none> 443/TCP 8h
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/deathstar 2/2 2 2 15s
#
# NAME DESIRED CURRENT READY AGE
# replicaset.apps/deathstar-689f66b57d 2 2 2 15s
# 파드 라벨 확인
$ kubectl get pod --show-labels
# => NAME READY STATUS RESTARTS AGE LABELS
# deathstar-689f66b57d-mm72v 1/1 Running 0 24s app.kubernetes.io/name=deathstar,class=deathstar,org=empire,pod-template-hash=689f66b57d
# deathstar-689f66b57d-pz56p 1/1 Running 0 24s app.kubernetes.io/name=deathstar,class=deathstar,org=empire,pod-template-hash=689f66b57d
# tiefighter 1/1 Running 0 24s app.kubernetes.io/name=tiefighter,class=tiefighter,org=empire
# xwing 1/1 Running 0 24s app.kubernetes.io/name=xwing,class=xwing,org=alliance
# cilium endpoint 확인
$ kubectl get ciliumendpoints
# => NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
# deathstar-689f66b57d-mm72v 391 ready 172.16.2.35
# deathstar-689f66b57d-pz56p 391 ready 172.16.1.232
# tiefighter 9002 ready 172.16.0.5
# xwing 10812 ready 172.16.2.31
$ c0 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# ...
# 2481 Disabled Disabled 9002 k8s:app.kubernetes.io/name=tiefighter 172.16.0.5 ready
# k8s:class=tiefighter
# k8s:org=empire
$ c1 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# ...
# 1063 Disabled Disabled 391 k8s:app.kubernetes.io/name=deathstar 172.16.1.232 ready
# k8s:class=deathstar
# k8s:org=empire
$ c2 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# ...
# 1695 Disabled Disabled 391 k8s:app.kubernetes.io/name=deathstar 172.16.2.35 ready
# k8s:class=deathstar
# k8s:org=empire
# 2810 Disabled Disabled 10812 k8s:app.kubernetes.io/name=xwing 172.16.2.31 ready
# k8s:class=xwing
# k8s:org=alliance
# 데스스타 SVC(ClusterIP) 접속하여 웹 파드 연결 확인 >> Hubble UI 에서 실시간 확인해보자!
$ kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# => Ship landed
$ kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# => Ship landed
# 확인
$ hubble observe
- Identity-Aware and HTTP-Aware Policy Enforcement
Apply an L3/L4 Policy
- Link & Hubble CLI - 링크- Cilium 에서는 Endpoint IP 대신, 파드의 Labels(라벨)을 사용(기준)하여 보안 정책을 적용합니다.
- IP/Port 필터링을 L3/L4 네트워크 정책이라고 합니다.
- 아래 처럼 ‘org=empire’ Labels(라벨) 부착된 파드만 허용해보겠습니다.
- Cilium 은 stateful connection tracking을 지원하므로 리턴 트래픽은 자동으로 허용됩니다.
# L3/L4 정책 생성
$ cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "L3-L4 policy to restrict deathstar access to empire ships only"
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP
EOF
# => ciliumnetworkpolicy.cilium.io/rule1 created
# 정책 확인
$ kubectl get cnp
# => NAME AGE
# rule1 9s
$ kubectl describe cnp rule1
$ c0 policy get
# => [
# {
# "endpointSelector": {
# "matchLabels": {
# "any:class": "deathstar",
# "any:org": "empire",
# "k8s:io.kubernetes.pod.namespace": "default"
# }
# },
# "ingress": [
# {
# "fromEndpoints": [
# {
# "matchLabels": {
# "any:org": "empire",
# "k8s:io.kubernetes.pod.namespace": "default"
# }
# }
# ],
# "toPorts": [
# {
# "ports": [
# {
# "port": "80",
# "protocol": "TCP"
# }
# ]
# }
# ]
# }
# ],
# ...
# "enableDefaultDeny": {
# "ingress": true,
# "egress": false
# },
# ...
# }
# ]
# 파드 curl 접속 시도 시 파드 sh 접속 후 curl 시도하자!
# 데스스타 SVC(ClusterIP) 접속하여 웹 파드 연결 확인 >> Hubble UI 에서 drop 확인!
$ kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# => Ship landed
$ kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# => (없음)
# <span style="color: green;">👉 접속이 drop 됩니다. xwing은 허용되는 `org=empire`인 엔드포인트가 아닌 `org=alliance`인 엔드포인트이기 때문입니다.</span>
# hubble cli 모니터링
$ hubble observe --pod xwing
$ hubble observe --pod tiefighter
$ hubble observe --pod deathstar
# => Oct 01 16:28:28.825: default/xwing:39224 (ID:10812) <> default/deathstar-689f66b57d-mm72v:80 (ID:391) policy-verdict:none INGRESS DENIED (TCP Flags: SYN)
# Oct 01 16:28:28.825: default/xwing:39224 (ID:10812) <> default/deathstar-689f66b57d-mm72v:80 (ID:391) Policy denied DROPPED (TCP Flags: SYN)
$ hubble observe --pod deathstar --verdict DROPPED
# => Oct 01 16:27:50.894: default/xwing:43148 (ID:10812) <> default/deathstar-689f66b57d-pz56p:80 (ID:391) policy-verdict:none INGRESS DENIED (TCP Flags: SYN)
# Oct 01 16:27:50.894: default/xwing:43148 (ID:10812) <> default/deathstar-689f66b57d-pz56p:80 (ID:391) Policy denied DROPPED (TCP Flags: SYN)
# Oct 01 16:28:24.676: default/xwing:43148 (ID:10812) <> default/deathstar-689f66b57d-pz56p:80 (ID:391) policy-verdict:none INGRESS DENIED (TCP Flags: SYN)
# Oct 01 16:28:24.676: default/xwing:43148 (ID:10812) <> default/deathstar-689f66b57d-pz56p:80 (ID:391) Policy denied DROPPED (TCP Flags: SYN)
# Inspecting the Policy
# If we run cilium endpoint list again we will see that the pods with the label org=empire and class=deathstar
# now have ingress policy enforcement enabled as per the policy above.
# endpoint list 에서 정책 적용 확인
$ c0 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# 2481 Disabled Disabled 9002 k8s:app.kubernetes.io/name=tiefighter 172.16.0.5 ready
# k8s:class=tiefighter
# k8s:org=empire
$ c1 endpoint list | grep deathstar
# => 1063 Enabled Disabled 391 k8s:app.kubernetes.io/name=deathstar 172.16.1.232 ready
# k8s:class=deathstar
$ c2 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# 1695 Enabled Disabled 391 k8s:app.kubernetes.io/name=deathstar 172.16.2.35 ready
# k8s:class=deathstar
# k8s:org=empire
# 2810 Disabled Disabled 10812 k8s:app.kubernetes.io/name=xwing 172.16.2.31 ready
# k8s:class=xwing
# k8s:org=alliance
- Identity-Aware and HTTP-Aware Policy Enforcement Apply and Test HTTP-aware L7 Policy - Docs
- HTTP L7 필터링을 적용 : PUT /v1/exhaust-port 요청을 차단해보겠습니다.
# 데스스타 SVC(ClusterIP) 접속
$ kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
# => Panic: deathstar exploded
# ...
# POST /v1/request-landing API 호출만 허용 정책으로 기존 정책 내용을 업데이트(configured)!
$ cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "L7 policy to restrict access to specific HTTP call"
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "POST"
path: "/v1/request-landing"
EOF
# 정책 확인
$ kubectl describe ciliumnetworkpolicies
# => Name: rule1
# Namespace: default
# Labels: <none>
# Annotations: <none>
# API Version: cilium.io/v2
# Kind: CiliumNetworkPolicy
# Metadata:
# Creation Timestamp: 2024-10-01T16:24:53Z
# Generation: 2
# Resource Version: 32642
# UID: d3527eec-5832-4273-b805-c006c728a8af
# Spec:
# Description: L7 policy to restrict access to specific HTTP call
# Endpoint Selector:
# Match Labels:
# Class: deathstar
# Org: empire
# Ingress:
# From Endpoints:
# Match Labels:
# Org: empire
# To Ports:
# Ports:
# Port: 80
# Protocol: TCP
# Rules:
# Http:
# Method: POST
# Path: /v1/request-landing
# ...
$ c0 policy get
# 모니터링
$ c1 monitor -v --type l7
$ c2 monitor -v --type l7
# => <- Request http from 0 ([k8s:app.kubernetes.io/name=tiefighter k8s:class=tiefighter k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]) to 1695 ([k8s:app.kubernetes.io/name=deathstar k8s:class=deathstar k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]), identity 9002->391, verdict Forwarded POST http://deathstar.default.svc.cluster.local/v1/request-landing => 0
# <- Response http to 0 ([k8s:app.kubernetes.io/name=tiefighter k8s:class=tiefighter k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]) from 1695 ([k8s:app.kubernetes.io/name=deathstar k8s:class=deathstar k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]), identity 391->9002, verdict Forwarded POST http://deathstar.default.svc.cluster.local/v1/request-landing => 200
$ hubble observe --pod deathstar
$ hubble observe --pod deathstar --verdict DROPPED
# 접근 테스트
$ kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# => Ship landed
$ kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
# => Access denied
## hubble cli 에 차단 로그 확인
$ hubble observe --pod deathstar --verdict DROPPED
# => Oct 01 16:41:48.094: default/tiefighter:51860 (ID:9002) -> default/deathstar-689f66b57d-mm72v:80 (ID:391) http-request DROPPED (HTTP/1.1 PUT http://deathstar.default.svc.cluster.local/v1/exhaust-port)
# Oct 01 16:41:48.094: default/tiefighter:51860 (ID:9002) <- default/deathstar-689f66b57d-mm72v:80 (ID:391) http-response FORWARDED (HTTP/1.1 403 0ms (PUT http://deathstar.default.svc.cluster.local/v1/exhaust-port))
$ hubble observe --pod deathstar --protocol http
# => Oct 01 16:39:57.041: default/tiefighter:57616 (ID:9002) <- default/deathstar-689f66b57d-mm72v:80 (ID:391) http-response FORWARDED (HTTP/1.1 200 2ms (POST http://deathstar.default.svc.cluster.local/v1/request-landing))
# 삭제
$ kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/minikube/http-sw-app.yaml
# => service "deathstar" deleted
# deployment.apps "deathstar" deleted
# pod "tiefighter" deleted
# pod "xwing" deleted
$ kubectl delete cnp rule1
# => ciliumnetworkpolicy.cilium.io "rule1" deleted
Bandwidth Manager
Bandwidth Manager 소개
- Cilium은 Bandwidth(네트워크 대역폭)과 Latency optimization(지연 시간 최적화)를 지원합니다. - Link , Home , Youtube
https://cilium.io/use-cases/bandwidth-optimization/
- bandwidth manager는 TCP와 UDP 부하를 최적화 하고, 효율적으로 개별 파드의 접속률을 제한할 수 있습니다. - EDT(Earliest Departure Time) 와 eBPF 사용
-
kubernetes.io/egress-bandwidth
Pod annotation 은 egress 트래픽에 대해 호스트 네트워크 장치의 대역폭 제한을 설정합니다. -
annotation 은 지원되지 않습니다.kubernetes.io/ingress-bandwidth
- direct routing mode, tunneling mode 둘 다 지원합니다.
- Limitations : L7 정책과 함께 사용할 수 없습니다.
설정 및 확인
# 인터페이스 tc qdisc 확인
$ tc qdisc show dev enp0s8
# => qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
# qdisc clsact ffff: parent ffff:fff1
# 설정
$ helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values --set bandwidthManager.enabled=true
# => Release "cilium" has been upgraded. Happy Helming!
# ...
# 적용 확인
$ cilium config view | grep bandwidth
# => enable-bandwidth-manager true
# egress bandwidth limitation 동작하는 인터페이스 확인
$ c0 status | grep BandwidthManager
# => BandwidthManager: EDT with BPF [CUBIC] [enp0s3, enp0s8]
# 인터페이스 tc qdisc 확인 : 설정 전후 옵션값들이 상당히 추가된다
$ tc qdisc
$ tc qdisc show dev enp0s8
# => qdisc fq 8006: root refcnt 2 limit 10000p flow_limit 100p buckets 32768 orphan_mask 1023 quantum 3028b initial_quantum 15140b low_rate_threshold 550Kbit refill_delay 40ms timer_slack 10us horizon 2s horizon_cap
# qdisc clsact ffff: parent ffff:fff1
동작 및 확인
# 테스트를 위한 트래픽 발생 서버/클라이언트 파드 생성
$ cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: Pod
metadata:
annotations:
# Limits egress bandwidth to 10Mbit/s.
kubernetes.io/egress-bandwidth: "10M"
labels:
# This pod will act as server.
app.kubernetes.io/name: netperf-server
name: netperf-server
spec:
containers:
- name: netperf
image: cilium/netperf
ports:
- containerPort: 12865
---
apiVersion: v1
kind: Pod
metadata:
# This Pod will act as client.
name: netperf-client
spec:
affinity:
# Prevents the client from being scheduled to the
# same node as the server.
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- netperf-server
topologyKey: kubernetes.io/hostname
containers:
- name: netperf
args:
- sleep
- infinity
image: cilium/netperf
EOF
# => pod/netperf-server created
# pod/netperf-client created
# egress BW 제한 정보 확인
$ kubectl describe pod netperf-server | grep Annotations:
# => Annotations: kubernetes.io/egress-bandwidth: 10M
# egress BW 제한이 설정된 파드가 있는 cilium pod 에서 제한 정보 확인
$ c0 bpf bandwidth list
$ c1 bpf bandwidth list
$ c2 bpf bandwidth list
# => IDENTITY EGRESS BANDWIDTH (BitsPerSec)
# 1391 10M
$ c0 endpoint list
$ c1 endpoint list
$ c2 endpoint list
# => ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
# ENFORCEMENT ENFORCEMENT
# 1391 Disabled Disabled 8191 k8s:app.kubernetes.io/name=netperf-server 172.16.2.106 ready
# ...
# 트래픽 발생 >> Hubble UI 에서 확인
# egress traffic of the netperf-server Pod has been limited to 10Mbit per second.
$ NETPERF_SERVER_IP=$(kubectl get pod netperf-server -o jsonpath='{.status.podIP}')
$ kubectl exec netperf-client -- netperf -t TCP_MAERTS -H "${NETPERF_SERVER_IP}"
# => Recv Send Send
# Socket Socket Message Elapsed
# Size Size Size Time Throughput
# bytes bytes bytes secs. 10^6bits/sec
# 131072 16384 16384 10.01 9.13
# <span style="color: green;">👉 10Mbps 제한 확인!</span>
# 5M 제한 설정 후 테스트
$ kubectl get pod netperf-server -o json | sed -e 's|10M|5M|g' | kubectl apply -f -
# => pod/netperf-server configured
$ c0 bpf bandwidth list
$ c1 bpf bandwidth list
$ c2 bpf bandwidth list
# => IDENTITY EGRESS BANDWIDTH (BitsPerSec)
# 1391 5M
$ kubectl exec netperf-client -- netperf -t TCP_MAERTS -H "${NETPERF_SERVER_IP}"
# => Recv Send Send
# Socket Socket Message Elapsed
# Size Size Size Time Throughput
# bytes bytes bytes secs. 10^6bits/sec
#
# 131072 16384 16384 10.01 4.59
# <span style="color: green;">👉 4.5Mbps 제한 확인!</span>
# 20M 제한 설정 후 테스트
$ kubectl get pod netperf-server -o json | sed -e 's|5M|20M|g' | kubectl apply -f -
# => pod/netperf-server configured
$ kubectl exec netperf-client -- netperf -t TCP_MAERTS -H "${NETPERF_SERVER_IP}"
# => Recv Send Send
# Socket Socket Message Elapsed
# Size Size Size Time Throughput
# bytes bytes bytes secs. 10^6bits/sec
# 131072 16384 16384 10.00 18.40
# <span style="color: green;">👉 19Mbps 제한 확인!</span>
$ tc qdisc show dev enp0s8
# => qdisc fq 8006: root refcnt 2 limit 10000p flow_limit 100p buckets 32768 orphan_mask 1023 quantum 3028b initial_quantum 15140b low_rate_threshold 550Kbit refill_delay 40ms timer_slack 10us horizon 2s horizon_cap
# qdisc clsact ffff: parent ffff:fff1
# 삭제
$ kubectl delete pod netperf-client netperf-server
L2 Announcements / L2 Aware LB (Beta)
- 참고 링크 : Link , Blog
- L2 Announcements는 로컬 영역 네트워크에서 서비스를 표시하고 도달 가능하게 만드는 기능입니다. 이 기능은 주로 사무실 또는 캠퍼스 네트워크와 같이 BGP 기반 라우팅이 없는 네트워크 내에서 온프레미스 배포를 위해 고안되었습니다.
- 이 기능을 사용하면 ExternalIP 및 LoadBalancer IP에 대한 ARP 쿼리에 응답합니다. 이러한 IP는 여러 노드의 가상 IP(네트워크 장치에 설치되지 않음)이므로 각 서비스에 대해 한 번에 한 노드가 ARP 쿼리에 응답하고 MAC 주소로 응답합니다. 이 노드는 서비스 로드 밸런싱 기능으로 로드 밸런싱을 수행하여 북쪽/남쪽 로드 밸런서 역할을 합니다.
- NodePort 서비스에 비해 이 기능의 장점은 각 서비스가 고유한 IP를 사용할 수 있으므로 여러 서비스가 동일한 포트 번호를 사용할 수 있다는 것입니다. NodePort를 사용할 때 트래픽을 보낼 호스트를 결정하는 것은 클라이언트에게 달려 있으며 노드가 다운되면 IP+Port 콤보를 사용할 수 없게 됩니다. L2 공지를 사용하면 서비스 VIP가 다른 노드로 간단히 마이그레이션되고 계속 작동합니다.
- 이를 통해 MetalLB와 같은 외부 로드 밸런서를 Cilium으로 대체할 수 있습니다.
https://isovalent.com/blog/post/migrating-from-metallb-to-cilium/
설정 및 확인
#
$ helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set l2announcements.enabled=true --set externalIPs.enabled=true \
--set l2announcements.leaseDuration=3s --set l2announcements.leaseRenewDeadline=1s --set l2announcements.leaseRetryPeriod=200ms
# => Release "cilium" has been upgraded. Happy Helming!
# ...
#
$ c0 config --all | grep L2
# => EnableL2Announcements : true
# EnableL2NeighDiscovery : true
# CiliumL2AnnouncementPolicy 생성
$ cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
serviceSelector:
matchLabels:
color: blue
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
interfaces:
- ^enp0s[0-9]+
externalIPs: true
loadBalancerIPs: true
EOF
# => ciliuml2announcementpolicy.cilium.io/policy1 created
# 확인
$ kubectl get ciliuml2announcementpolicy
# => NAME AGE
# policy1 7s
$ kubectl describe l2announcement
#
$ cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: "cilium-pool"
spec:
allowFirstLastIPs: "No"
blocks:
- cidr: "10.10.200.0/29"
EOF
# => ciliumloadbalancerippool.cilium.io/cilium-pool created
# cilium ip pool 조회
$ kubectl get CiliumLoadBalancerIPPool
# => NAME DISABLED CONFLICTING IPS AVAILABLE AGE
# cilium-pool false False 6 9s
테스트용 파드, 서비스 생성
#
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: webpod1
labels:
app: webpod
spec:
nodeName: k3s-w1
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod2
labels:
app: webpod
spec:
nodeName: k3s-w2
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Service
metadata:
name: svc1
spec:
ports:
- name: svc1-webport
port: 80
targetPort: 80
selector:
app: webpod
type: LoadBalancer # 👉 서비스 타입이 LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: svc2
spec:
ports:
- name: svc2-webport
port: 80
targetPort: 80
selector:
app: webpod
type: LoadBalancer # 👉 서비스 타입이 LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: svc3
spec:
ports:
- name: svc3-webport
port: 80
targetPort: 80
selector:
app: webpod
type: LoadBalancer # 👉 서비스 타입이 LoadBalancer
EOF
# => pod/webpod1 created
# pod/webpod2 created
# service/svc1 created
# service/svc2 created
# service/svc3 created
접속 확인
#
$ kubectl get svc,ep
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kubernetes ClusterIP 10.10.200.1 <none> 443/TCP 10h
# service/svc1 LoadBalancer 10.10.200.214 10.10.200.1 80:30456/TCP 98s
# service/svc2 LoadBalancer 10.10.200.240 10.10.200.2 80:30367/TCP 98s
# service/svc3 LoadBalancer 10.10.200.155 10.10.200.3 80:31800/TCP 98s
#
# NAME ENDPOINTS AGE
# endpoints/kubernetes 192.168.10.10:6443 10h
# endpoints/svc1 172.16.1.32:80,172.16.2.115:80 98s
# endpoints/svc2 172.16.1.32:80,172.16.2.115:80 98s
# endpoints/svc3 172.16.1.32:80,172.16.2.115:80 98s
#
$ curl -s 10.10.200.1
# => Hostname: webpod1
# IP: 127.0.0.1
# IP: 172.16.1.32
# RemoteAddr: 192.168.10.10:58036
# GET / HTTP/1.1
# Host: 10.10.200.1
# User-Agent: curl/7.81.0
# Accept: */*
$ curl -s 10.10.200.2
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: 172.16.2.115
# RemoteAddr: 192.168.10.10:53496
# GET / HTTP/1.1
# Host: 10.10.200.2
# User-Agent: curl/7.81.0
# Accept: */*
$ curl -s 10.10.200.3
# => Hostname: webpod1
# IP: 127.0.0.1
# IP: 172.16.1.32
# RemoteAddr: 192.168.10.10:54098
# GET / HTTP/1.1
# Host: 10.10.200.3
# User-Agent: curl/7.81.0
# Accept: */*
이상과 같이 Cilium 만으로 Loadbalancer유형의 Service를 구성할 수 있었습니다.
마치며
이번 글에서는 Cilium CNI의 다양한 기능들을 살펴보았습니다. Cilium CNI은 알면 알수록 “끝판왕”, “It’s magic!”이라는 말이 계속 떠올랐습니다. 아래의 말과 참 맞닿는것 같습니다.
“충분히 발달한 과학 기술은 마법과 구별할 수 없다” - 아서 클라크
스터디 중에도 가시다 님이 “됩니다”를 연발하셔서 “다 되는 페이” KB 페이 광고가 생각났습니다. 정말 다 되는 CNI인것 같습니다. 물론 eBPF나 Envoy와 같은 기술들이 있었기에 Cilium이 가능한 것이지만, 참 대단합니다. 이번에 실습한것 외에도 더 다양한 기능들이 있어서, 더욱더 공부해야겠다는 생각이 듭니다.
어느덧 다음주가 마지막 주차입니다. 이런 저런 일들로 스터디 포기할까 하는 때도 있었는데 어찌저찌 잘 버텼습니다. 다음주에는 좀 더 일찍 과제를 제출할 수 있기를 바라며 이만 포스팅을 마치겠습니다.