본문 바로가기

카테고리 없음

KANS(Kubernetes Advanced Networking Study) - 8주차 정리

Cilium CNI

Cilium은 eBPF(Berkeley Packet Filter)를 기반으로 Pod Network 환경을 제공하는 CNI 플러그인으로 Kubernetes와 같은 리눅스 컨테이너 관리 플랫폼을 사용해 배포된 응용 프로그램 서비스 간의 네트워크 및 API 연결을 제공하는 오픈소스 소프트웨어 입니다. 

eBPF 기반으로 동작하기 때문에 iptables 기반으로 동작하는 다른 CNI를 사용하는 것보다 네트워크 성능 관점에서 우위를 점합니다. 이전 주차에서도 학습했지만, iptables는 Kubernetes에서 Pod와 Service가 생성될 때 마다 규칙도 추가되는데, 요청을 처리할 때, 모든 iptables 규칙을 평가하기 때문에 Pod와 Service 오브젝트가 많은 경우, 성능 저하가 발생할 수 밖에 없는 구조입니다. 또한, Cilium Agent에서 kube-proxy를 대체할 수 있기 때문에 관리 포인트를 덜 수 있다는 장점도 있습니다. 

 

아래 이미지는 eBPF의 트래픽 처리 플로우입니다.

  • 빨간 Box : Kernel Hook point
  • 녹색 Box : Network device components
  • 노란 Box : Cilium Components


위 그림의 Endpoint 간의 통신 시, 왼쪽부터 오른쪽 까지의 Datapath는 다음과 같습니다.

  • A Endpoint가 B Endpoint와 TCP 통신을 하는 경우, 기본적으로 TC egress Hook을 트리거
  • TC(Traffic Control) ingress Hook은 eBPF 프로그램을 실행 ("bpf_lxc")
  • (Optional) L7 Policy를 설정한 경우 :
    • Userspace의 Proxy로 전달을 위해 Network Stack으로 전달 (L7 Proxy를 사용하는 경우, iptables를 사용)
  • B Endpoint에 Packet이 도달하면 TC ingress Hook이 트리거
    • B Endpoint에 수신 되기 전에 TC Ingress Hook이 트리거 되어 eBPF 프로그램 실행 ("bpf_lxc")

 

실습 환경 구성

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/kans-8w.yaml

# 배포
aws cloudformation deploy --template-file kans-8w.yaml --stack-name mylab --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# 접속할 공인 IP 확인
aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2

 

Cilium CNI를 설치하기 위해 CNI가 설치되지 않은 상태로 Node 정보를 확인하면, NotReady 상태입니다. Pod 정보를 확인하면, Static Pod를 제외한 Pod(CoreDNS)도 Pending 상태입니다. 

 

Cilium에서 권장하는 최소 커널 버전은 5.8 이상 입니다. 

 

Helm을 활용해 Cilium을 배포합니다.

helm repo add cilium https://helm.cilium.io/
helm repo update

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

 

Cilium이 정상적으로 올라온 모습입니다.

 

테스트로 Pod를 생성하면 Pending으로 유지되지 않고 Running 상태로 잘 동작합니다.

 

Cilium은 iptables를 사용하지 않기 때문에 iptables 내 규칙도 매우 적은 상태인 걸 알 수 있습니다.

 

Cilium 관련 CRD 입니다. 

 

CILIUMINTERNALIP는 Cilium 생성 후 생성되는 cilium_host 인터페이스의 IP이고, INTERNALIP는 Node의 IP(아래 이미지의 ens5) 입니다. cilium_net과 cilium_host 인터페이스는 veth peer 관계이고, cilium_host 인터페이스는 Pod의 게이트웨이 IP 주소로 지정됩니다. 

 

기본으로 제공되는 endpoint와 유사하게 CiliumEndPoints를 제공하는데, 할당된 Pod IP와 상태를 확인할 수 있습니다.

 

Cilium CNI의 Config 입니다. (이미지 하나로 담을 수가 없어서 코드블럭으로 작성했습니다.)

(⎈|CiliumLab:default) root@k8s-s:~# kubectl get cm -n kube-system cilium-config -o json | jq
{
  "apiVersion": "v1",
  "data": {
    "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",
    "cgroup-root": "/run/cilium/cgroupv2",
    "cilium-endpoint-gc-interval": "5m0s",
    "cluster-id": "0",
    "cluster-name": "default",
    "clustermesh-enable-endpoint-sync": "false",
    "clustermesh-enable-mcs-api": "false",
    "cni-exclusive": "true",
    "cni-log-file": "/var/run/cilium/cilium-cni.log",
    "controller-group-metrics": "write-cni-file sync-host-ips sync-lb-maps-with-k8s-services",
    "custom-cni-conf": "false",
    "datapath-mode": "veth",
    "debug": "true",
    "debug-verbose": "",
    "direct-routing-skip-unreachable": "false",
    "dnsproxy-enable-transparent-mode": "true",
    "dnsproxy-socket-linger-timeout": "10",
    "egress-gateway-reconciliation-trigger-interval": "1s",
    "enable-auto-protect-node-port-range": "true",
    "enable-bpf-clock-probe": "false",
    "enable-bpf-masquerade": "true",
    "enable-endpoint-health-checking": "true",
    "enable-endpoint-routes": "true",
    "enable-health-check-loadbalancer-ip": "false",
    "enable-health-check-nodeport": "true",
    "enable-health-checking": "true",
    "enable-hubble": "true",
    "enable-hubble-open-metrics": "true",
    "enable-ipv4": "true",
    "enable-ipv4-big-tcp": "false",
    "enable-ipv4-masquerade": "true",
    "enable-ipv6": "false",
    "enable-ipv6-big-tcp": "false",
    "enable-ipv6-masquerade": "true",
    "enable-k8s-networkpolicy": "true",
    "enable-k8s-terminating-endpoint": "true",
    "enable-l2-neigh-discovery": "true",
    "enable-l7-proxy": "true",
    "enable-local-redirect-policy": "false",
    "enable-masquerade-to-route-source": "false",
    "enable-metrics": "true",
    "enable-node-selector-labels": "false",
    "enable-policy": "default",
    "enable-runtime-device-detection": "true",
    "enable-sctp": "false",
    "enable-svc-source-range-check": "true",
    "enable-tcx": "true",
    "enable-vtep": "false",
    "enable-well-known-identities": "false",
    "enable-xt-socket-fallback": "true",
    "envoy-base-id": "0",
    "envoy-keep-cap-netbindservice": "false",
    "external-envoy-proxy": "true",
    "hubble-disable-tls": "false",
    "hubble-export-file-max-backups": "5",
    "hubble-export-file-max-size-mb": "10",
    "hubble-listen-address": ":4244",
    "hubble-metrics": "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",
    "hubble-metrics-server": ":9965",
    "hubble-metrics-server-enable-tls": "false",
    "hubble-socket-path": "/var/run/cilium/hubble.sock",
    "hubble-tls-cert-file": "/var/lib/cilium/tls/hubble/server.crt",
    "hubble-tls-client-ca-files": "/var/lib/cilium/tls/hubble/client-ca.crt",
    "hubble-tls-key-file": "/var/lib/cilium/tls/hubble/server.key",
    "identity-allocation-mode": "crd",
    "identity-gc-interval": "15m0s",
    "identity-heartbeat-timeout": "30m0s",
    "install-no-conntrack-iptables-rules": "true",
    "ipam": "kubernetes",
    "ipam-cilium-node-update-rate": "15s",
    "ipv4-native-routing-cidr": "192.168.0.0/16",
    "k8s-client-burst": "20",
    "k8s-client-qps": "10",
    "k8s-require-ipv4-pod-cidr": "true",
    "k8s-require-ipv6-pod-cidr": "false",
    "kube-proxy-replacement": "true",
    "kube-proxy-replacement-healthz-bind-address": "",
    "max-connected-clusters": "255",
    "mesh-auth-enabled": "true",
    "mesh-auth-gc-interval": "5m0s",
    "mesh-auth-queue-size": "1024",
    "mesh-auth-rotated-identities-queue-size": "1024",
    "monitor-aggregation": "medium",
    "monitor-aggregation-flags": "all",
    "monitor-aggregation-interval": "5s",
    "nat-map-stats-entries": "32",
    "nat-map-stats-interval": "30s",
    "node-port-bind-protection": "true",
    "nodeport-addresses": "",
    "nodes-gc-interval": "5m0s",
    "operator-api-serve-addr": "127.0.0.1:9234",
    "operator-prometheus-serve-addr": ":9963",
    "policy-cidr-match-mode": "",
    "preallocate-bpf-maps": "false",
    "procfs": "/host/proc",
    "prometheus-serve-addr": ":9962",
    "proxy-connect-timeout": "2",
    "proxy-idle-timeout-seconds": "60",
    "proxy-max-connection-duration-seconds": "0",
    "proxy-max-requests-per-connection": "0",
    "proxy-xff-num-trusted-hops-egress": "0",
    "proxy-xff-num-trusted-hops-ingress": "0",
    "remove-cilium-node-taints": "true",
    "routing-mode": "native",
    "service-no-backend-response": "reject",
    "set-cilium-is-up-condition": "true",
    "set-cilium-node-taints": "true",
    "synchronize-k8s-nodes": "true",
    "tofqdns-dns-reject-response-code": "refused",
    "tofqdns-enable-dns-compression": "true",
    "tofqdns-endpoint-max-ip-per-hostname": "50",
    "tofqdns-idle-connection-grace-period": "0s",
    "tofqdns-max-deferred-connection-deletes": "10000",
    "tofqdns-proxy-response-max-delay": "100ms",
    "unmanaged-pod-watcher-interval": "15",
    "vtep-cidr": "",
    "vtep-endpoint": "",
    "vtep-mac": "",
    "vtep-mask": "",
    "write-cni-conf-when-ready": "/host/etc/cni/net.d/05-cilium.conflist"
  },
  "kind": "ConfigMap",
  "metadata": {
    "annotations": {
      "meta.helm.sh/release-name": "cilium",
      "meta.helm.sh/release-namespace": "kube-system"
    },
    "creationTimestamp": "2024-10-23T00:54:55Z",
    "labels": {
      "app.kubernetes.io/managed-by": "Helm"
    },
    "name": "cilium-config",
    "namespace": "kube-system",
    "resourceVersion": "3681",
    "uid": "5b6d14eb-0474-4c37-aa9c-a471f93057cd"
  }
}

 

Cilium은 Calico와 같이 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 DaemonSet Pod에서 Cilium 명령어를 통해 상태를 확인할 수도 있습니다.

 

Cilium CLI를 통해 Cilium의 상태와 설정을 확인할 수 있습니다. 

 

Cilium에서 Observerability 툴로 기본 제공하는 Hubble 입니다. 모니터링을 위해 애플리케이션에서 별도로 해줘야 하는 작업 없이 쉽고 빠르게 사용할 수 있습니다.

kubectl patch -n kube-system svc hubble-ui -p '{"spec": {"type": "NodePort"}}'
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에서도 CLI를 제공합니다. 

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
rm hubble-linux-${HUBBLE_ARCH}.tar.gz{,.sha256sum}

 

Node 간 Pod 통신을 확인하기 위해 아래 코드를 적용합니다.

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: netpod
  labels:
    app: netpod
spec:
  nodeName: k8s-s
  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: k8s-w1
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod2
  labels:
    app: webpod
spec:
  nodeName: k8s-w2
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
EOF

 

정상 배포되었습니다.

 

각 Pod의 상태정보를 통해 같은 Node에 할당된 Pod를 확인할 수 있습니다.

 

Pod1 안에서 동작하는 앱이 connect() 시스템 콜을 이용하여 소켓을 연결할 때 목적지 주소가 서비스 주소(10.10.8.55)이면 소켓의 목적지 주소를 바로 백엔드 주소(10.0.0.31)로 설정한다. 이후 앱에서 해당 소켓을 통해 보내는 모든 패킷의 목적지 주소는 이미 백엔드 주소(10.0.0.31)로 설정되어 있기 때문에 중간에 DNAT 변환 및 역변환 과정이 필요없어진다.

 

Service 생성 후 통신을 해봅니다.

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가 정상적으로 생겼지만, iptables는 생성된 게 없습니다. (eBPF를 사용하기 때문입니다.) 

 

Pod에서 Service를 호출하는데, Service의 IP인 10 대역을 거치지 않고, 172 대역인 Pod IP로 바로 통신합니다. 위 그림의 설명과 같이 DNAT를 수행하지 않는 것을 알 수 있습니다.

 

 

Prometheus & Grafana 배포

Prometheus와 Grafana를 설치해 Cilium 관련 메트릭을 수집하고 모니터링 합니다.

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/kubernetes/addons/prometheus/monitoring-example.yaml
kubectl get all -n cilium-monitoring

# 파드와 서비스 확인
kubectl get pod,svc,ep -o wide -n cilium-monitoring

# 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"

# 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"

 

Network Policy

이전 주차에 작성한 VPC CNI의 Network Policy와 동일하게 Cilium CNI에서도 CiliumNetworkPolicy 커스텀 리소스를 통해 Network Policy를 사용할 수 있습니다. 테스트를 위해 배포되는 서비스 구성은 아래와 같습니다.

 

kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/minikube/http-sw-app.yaml
kubectl get all

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

 

정책 확인

kubectl get cnp
kc describe cnp rule1
c0 policy get

 

NetworkPolicy 설정으로 xwing에서 deathster로 통신이 불가능한 것을 알 수 있습니다. Hubble을 통해서도 Drop 되는 정보를 확인할 수 있습니다.

 

 

 

참고문서

https://ebpf.io/