kube-proxy 없는 Kubernetes

kube-proxy 없는 Kubernetes (Kubernetes Without kube-proxy)

kube-proxy 없이 Kubernetes 클러스터를 구성하고 Cilium으로 완전히 대체하는 방법을 설명해요. kubeadm으로 클러스터를 부트스트랩하고, eBPF 기반 kube-proxy replacement를 설정해 ClusterIP, NodePort, LoadBalancer 서비스 등을 처리해요.

출처: Kubernetes Without kube-proxy

본문

이 가이드는 kube-proxy 없이 Kubernetes 클러스터를 프로비저닝하고, Cilium으로 그것을 완전히 대체하는 방법을 설명해요. 단순하게 하기 위해 kubeadm을 사용해 클러스터를 부트스트랩할 거예요.

kubeadm 설치에 대한 도움과 더 많은 프로비저닝 옵션은 공식 Kubeadm 문서를 참고하세요.

Note Cilium의 kube-proxy replacement는 socket-LB 기능에 의존해요.

빠른 시작 (Quick-Start)

kubeadm init으로 control-plane 노드를 초기화하고 kube-proxy 애드온 설치를 건너뛰세요:

Note 사용 중인 CRI 구현에 따라 kubeadm init ... 명령에 --cri-socket 플래그를 사용해야 할 수도 있어요. 예를 들어 Docker CRI를 사용한다면 --cri-socket unix:///var/run/cri-dockerd.sock을 사용하면 돼요.

$ kubeadm init --skip-phases=addon/kube-proxy

그다음 control-plane 노드 IP 주소와 kubeadm init이 반환한 토큰을 지정해 워커 노드를 조인하세요 (이 튜토리얼에서는 클러스터에 워커 노드를 최소 하나 추가하는 것이 좋아요):

$ kubeadm join <..>

Note 인터페이스가 여러 개 있다면 각 워커에서 kubelet의 --node-ip이 올바르게 설정되어 있는지 확인하세요. 그렇지 않으면 Cilium의 kube-proxy replacement가 올바르게 동작하지 않을 수 있어요. kubectl get nodes -o wide를 실행해 각 노드가 각 노드에서 같은 이름의 디바이스에 할당된 InternalIP를 갖는지 확인해 이 사실을 검증할 수 있어요.

kube-proxy가 DaemonSet으로 실행 중인 기존 설치의 경우, 아래 명령으로 제거하세요.

Warning kube-proxy를 제거하면 기존 서비스 연결이 끊겨요. 또한 Cilium replacement가 설치될 때까지 서비스 관련 트래픽도 중단돼요.

Warning eBPF kube-proxy replacement를 시스템의 kube-proxy와 공존(c0o-existence)시키며 배포할 때는 두 메커니즘이 서로 독립적으로 동작한다는 점을 인지하세요. 즉, 이미 실행 중인 클러스터에서 eBPF kube-proxy replacement를 추가하거나 제거해 운영을 각각 kube-proxy로/로부터 위임하면, 예를 들어 두 NAT 테이블이 서로를 인지하지 못하므로 기존 연결이 끊어질 것으로 예상해야 해요. 아직 사용자 트래픽을 서빙하지 않는 새로 생성된 node/cluster에 공존 배포한다면 이는 문제가 되지 않아요.

$ kubectl -n kube-system delete ds kube-proxy
$ # Delete the configmap as well to avoid kube-proxy being reinstalled during a Kubeadm upgrade
$ kubectl -n kube-system delete cm kube-proxy
$ # Run on each node with root permissions:
$ iptables-save | grep -v KUBE | iptables-restore

Helm 저장소를 설정하세요:

Helm Repository OCI Registry

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

Cilium 차트는 OCI 레지스트리(Quay.io 및 Docker Hub)에서도 사용할 수 있어요. 설정이 필요 없으며 oci:// URL로 직접 설치할 수 있어요.

차트 서명 검증과 digest 기반 설치를 포함한 자세한 내용은 OCI Registry section을 참고하세요.

다음으로 필요한 YAML 파일을 생성하고 배포하세요.

Important 아래의 API_SERVER_IP와 API_SERVER_PORT를 kubeadm init이 보고한 control-plane 노드 IP 주소와 kube-apiserver 포트 번호로 올바르게 설정하세요 (Kubeadm은 기본적으로 6443 포트를 사용해요).

kubeadm init이 명시적으로 kube-proxy를 설정하지 않고 실행되기 때문에 이 지정이 필요해요. 결과적으로 kube-apiserver 서비스의 ClusterIP로 KUBERNETES_SERVICE_HOST와 KUBERNETES_SERVICE_PORT를 환경에 내보내지만, 그 서비스를 프로비저닝하는 kube-proxy가 우리 설정에는 없어요. 따라서 Cilium agent는 다음 구성으로 이 정보를 알게 해야 해요:

API_SERVER_IP=<your_api_server_ip>
# Kubeadm default is 6443
API_SERVER_PORT=<your_api_server_port>

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Note Cilium은 기본적으로 BPF cgroup 프로그램을 부착하는 데 필요한 cgroup v2 파일시스템을 /run/cilium/cgroupv2 경로에 자동으로 마운트해요. 그러려면 DaemonSet이 시작한 init 컨테이너 안에서 호스트 /proc을 일시적으로 마운트해야 해요. 자동 마운트를 비활성화하려면 --set cgroup.autoMount.enabled=false를 지정하고, cgroup v2 파일시스템이 이미 마운트된 호스트 마운트 지점을 --set cgroup.hostRoot로 설정하세요. 예를 들어 아직 마운트되지 않았다면 호스트에서 아래 명령을 실행해 cgroup v2 파일시스템을 마운트하고 --set cgroup.hostRoot=/sys/fs/cgroup을 지정하면 돼요.

mount -t cgroup2 none /sys/fs/cgroup

이렇게 하면 ClusterIP, NodePort, LoadBalancer 타입의 Kubernetes 서비스와 externalIPs가 있는 서비스의 처리를 구현하는 eBPF kube-proxy replacement와 함께 CNI 플러그인으로서 Cilium이 설치돼요. 또한 eBPF kube-proxy replacement는 컨테이너의 hostPort를 지원하므로 더 이상 portmap을 사용할 필요가 없어요.

마지막으로 모든 노드에서 Cilium이 올바르게 뜨고 운영할 준비가 됐는지 확인하세요:

$ kubectl -n kube-system get pods -l k8s-app=cilium
NAME                READY     STATUS    RESTARTS   AGE
cilium-fmh8d        1/1       Running   0          10m
cilium-mkcmb        1/1       Running   0          10m

위 Helm 구성에서 kubeProxyReplacement가 true 모드로 설정됐다는 점에 주의하세요. 이는 기본 Linux 커널 지원이 없으면 Cilium agent가 실행을 중단(bail out)한다는 것을 의미해요.

기본적으로 Helm은 kubeProxyReplacement=false로 설정하며, 이는 ClusterIP 서비스의 패킷별(packet-by-packet) 인클러스터 로드 밸런싱만 활성화해요.

Cilium의 eBPF kube-proxy replacement는 direct routing과 tunneling 모드 모두에서 지원돼요.

설정 검증 (Validate the Setup)

위 Quick-Start 가이드로 Cilium을 배포한 후, 먼저 Cilium agent가 원하는 모드로 실행 중인지 검증할 수 있어요:

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep KubeProxyReplacement
KubeProxyReplacement:   True        [eth0 (Direct Routing), eth1]

전체 세부사항은 --verbose를 사용하세요:

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status --verbose
[...]
KubeProxyReplacement Details:
  Status:                True
  Socket LB:             Enabled
  Protocols:             TCP, UDP
  Devices:               eth0 (Direct Routing), eth1
  Mode:                  SNAT
  Backend Selection:     Random
  Session Affinity:      Enabled
  Graceful Termination:  Enabled
  NAT46/64 Support:      Enabled
  XDP Acceleration:      Disabled
  Services:
  - ClusterIP:      Enabled
  - NodePort:       Enabled (Range: 30000-32767)
  - LoadBalancer:   Enabled
  - externalIPs:    Enabled
  - HostPort:       Enabled
[...]

선택적 다음 단계로 Nginx Deployment를 만들 거예요. 그런 다음 새 NodePort 서비스를 만들고 Cilium이 서비스를 올바르게 설치했는지 검증할 거예요.

백엔드 파드에 사용되는 YAML은 다음과 같아요:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

Nginx 파드가 실행 중인지 확인하세요:

$ kubectl get pods -l run=my-nginx -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP             NODE   NOMINATED NODE   READINESS GATES
my-nginx-756fb87568-gmp8c   1/1     Running   0          62m   10.217.0.149   apoc   <none>           <none>
my-nginx-756fb87568-n5scv   1/1     Running   0          62m   10.217.0.107   apoc   <none>           <none>

다음 단계에서 두 인스턴스에 대한 NodePort 서비스를 만듭니다:

$ kubectl expose deployment my-nginx --type=NodePort --port=80
service/my-nginx exposed

NodePort 서비스가 생성됐는지 확인하세요:

$ kubectl get svc my-nginx
NAME       TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
my-nginx   NodePort   10.104.239.135   <none>        80:31940/TCP   24m

cilium-dbg service list 명령의 도움으로 Cilium의 eBPF kube-proxy replacement가 새 NodePort 서비스를 만들었는지 검증할 수 있어요. 이 예제에서 포트 31940인 서비스가 생성됐어요 (디바이스 eth0과 eth1 각각 하나씩):

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg service list
ID   Frontend               Service Type   Backend
[...]
4    10.104.239.135:80/TCP      ClusterIP      1 => 10.217.0.107:80/TCP
                                               2 => 10.217.0.149:80/TCP
5    0.0.0.0:31940/TCP          NodePort       1 => 10.217.0.107:80/TCP
                                               2 => 10.217.0.149:80/TCP
6    192.168.178.29:31940/TCP   NodePort       1 => 10.217.0.107:80/TCP
                                               2 => 10.217.0.149:80/TCP
7    172.16.0.29:31940/TCP      NodePort       1 => 10.217.0.107:80/TCP
                                               2 => 10.217.0.149:80/TCP

테스트용 노드 포트 변수를 만드세요:

$ node_port=$(kubectl get svc my-nginx -o=jsonpath='{@.spec.ports[0].nodePort}')

동시에 호스트 네임스페이스에서 iptables로 서비스에 대한 iptables 규칙이 없는지 확인할 수 있어요:

$ iptables-save | grep KUBE-SVC
[ empty line ]

마지막으로 간단한 curl 테스트가 노출된 NodePort뿐 아니라 ClusterIP에 대한 연결성을 보여줘요:

$ curl 127.0.0.1:$node_port
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[....]
$ curl 192.168.178.29:$node_port
<!doctype html>
<html>
<head>
<title>welcome to nginx!</title>
[....]
$ curl 172.16.0.29:$node_port
<!doctype html>
<html>
<head>
<title>welcome to nginx!</title>
[....]
$ curl 10.104.239.135:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[....]

보시다시피 Cilium의 eBPF kube-proxy replacement가 올바르게 설정됐어요.

고급 구성 (Advanced Configuration)

이 섹션은 위 Quick-Start 가이드를 넘어서는 kube-proxy replacement의 몇 가지 고급 구성 모드를 다루며, 모두 선택사항이에요.

클라이언트 소스 IP 보존 (Client Source IP Preservation)

Cilium의 eBPF kube-proxy replacement는 서비스 엔드포인트로 가는 경로에서 클라이언트 소스 IP 주소가 손실될 노드 NodePort 요청에 SNAT을 수행하지 않기 위한 다양한 옵션을 구현해요.

  • externalTrafficPolicy=Local: Local 정책은 일반적으로 eBPF 구현을 통해 지원돼요. externalTrafficPolicy=Local인 서비스에 대한 인클러스터 연결이 가능하며, 로컬 백엔드가 없는 노드에서도 도달할 수 있어요. 즉 SNAT이 필요하지 않으므로 모든 서비스 엔드포인트가 인클러스터 측에서 로드 밸런싱을 위해 사용 가능해요.
  • externalTrafficPolicy=Cluster: 서비스 생성 시 기본값인 Cluster 정책의 경우 외부 트래픽에 대한 클라이언트 소스 IP 보존을 달성하기 위한 여러 옵션이 있어요. 즉 TCP 기반 서비스만 외부에 노출되는 경우 kube-proxy replacement를 DSR 또는 Hybrid 모드로 운영하는 것이에요.

내부 트래픽 정책 (Internal Traffic Policy)

위에서 설명한 externalTrafficPolicy와 유사하게, Cilium의 eBPF kube-proxy replacement는 internalTrafficPolicy를 지원하며, 이는 위 의미를 인클러스터 트래픽으로 변환해요.

  • internalTrafficPolicy=Local인 서비스의 경우 현재 클러스터의 파드에서 시작된 트래픽은 트래픽이 시작된 노드와 같은 노드 안의 엔드포인트로만 라우팅돼요.
  • internalTrafficPolicy=Cluster가 기본값이며, 내부(인클러스터) 트래픽을 처리할 수 있는 엔드포인트를 제한하지 않아요.

다음 표는 외부 및 내부 트래픽 정책에 따라 서비스에 대한 연결을 제공하는 데 어떤 백엔드가 사용되는지에 대한 아이디어를 줘요:

Traffic policy Service backends used
Internal External for North-South traffic for East-West traffic
Cluster Cluster All (default) All (default)
Cluster Local Node-local only All (default)
Local Cluster All (default) Node-local only
Local Local Node-local only Node-local only

선택적 서비스 타입 노출 (Selective Service Type Exposure)

기본적으로 LoadBalancer 서비스에 대해 Cilium은 해당 NodePort와 ClusterIP 서비스를 노출해요. 마찬가지로 새 NodePort 서비스에 대해 Cilium은 해당 ClusterIP 서비스를 노출해요.

이 동작이 바람직하지 않다면 service.cilium.io/type 어노테이션으로 서비스 생성을 특정 서비스 타입에만 고정할 수 있어요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/type: LoadBalancer
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
  allocateLoadBalancerNodePorts: false

위 예제에서는 LoadBalancer 서비스만 생성되고 해당 NodePort와 ClusterIP 서비스는 생성되지 않아요. 어노테이션이 예를 들어 service.cilium.io/type: NodePort로 설정되면 NodePort 서비스만 설치돼요.

호스트 프록시 위임 (Host Proxy Delegation)

주어진 서비스에 대해 선택된 서비스 백엔드 IP가 로컬 노드 IP와 일치하면 service.cilium.io/proxy-delegation: delegate-if-local 어노테이션이 수신된 패킷을 수정하지 않고 upper stack으로 전달해서, Envoy(존재한다면) 같은 L7 프록시가 호스트 네임스페이스에서 요청을 처리할 수 있게 해요. 이 메커니즘은 주로 north/south 트래픽을 위한 것이에요.

선택된 서비스 백엔드가 원격 IP라면 수신된 패킷은 upper stack으로 밀어지지 않고, 대신 BPF 코드가 구성된 포워딩 방법으로 패킷을 네이티브하게 원격 IP로 전달해요.

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/proxy-delegation: delegate-if-local
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer

externalTrafficPolicy=Local과 결합하면 이 메커니즘은 모든 트래픽을 upper proxy로 밀어내는 것도 허용해요.

East/west 트래픽의 경우 서비스 변환이 건너뛰어지고 패킷은 DNAT 없이 노드 밖으로 나가요.

service.cilium.io/proxy-delegation 어노테이션이 없으면 모든 포워딩을 BPF에 네이티브하게 맡기며, 이는 kube-proxy replacement의 기본값이기도 해요.

선택적 서비스 노드 노출 (Selective Service Node Exposure)

기본적으로 Cilium은 클러스터의 모든 노드에 Kubernetes 서비스를 노출해요. 서비스를 하위 집합의 노드에만 노출하려면 관련 노드에 service.cilium.io/node 라벨을 사용하세요. 예를 들어 노드를 다음과 같이 라벨링합니다:

$ kubectl label node node_name service.cilium.io/node=beefy

service.cilium.io/node=beefy 라벨이 있는 노드에만 노출되어야 하는 새 서비스를 추가하려면 서비스를 다음과 같이 설치하세요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/node: beefy
spec:
  selector:
    app: example
  ports:
    - port: 8765
      targetPort: 9376
  type: LoadBalancer

service.cilium.io/node-selector 어노테이션으로 서비스 노드 노출을 제어할 수도 있어요. 어노테이션 값에 라벨 셀렉터가 포함돼요. 이렇게 하면 서비스는 노드 라벨 셀렉터와 매칭되는 노드에서만 노출돼요. service.cilium.io/node-selector 어노테이션은 같은 서비스에 둘 다 존재하면 항상 service.cilium.io/node보다 우선해요.

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/node-selector: "service.cilium.io/node in ( beefy , slow )"
spec:
  selector:
    app: example
  ports:
    - port: 8765
      targetPort: 9376
  type: LoadBalancer

서비스가 해당 라벨과 매칭되면서 노출된 후 노드 라벨을 변경해도 서비스가 노출된 노드 목록이 자동으로 업데이트되지는 않아요. 노드 라벨 변경 후 서비스 노출을 업데이트하려면 Cilium agent를 재시작하세요. 일반적으로 Kubernetes 클러스터에 조인할 때 노드 라벨을 고정하고 노드의 수명 동안 유지하는 것이 좋아요.

Maglev 일관 해싱 (Maglev Consistent Hashing)

Cilium의 eBPF kube-proxy replacement는 백엔드 선택을 위한 로드 밸런서에 Maglev hashing의 변형을 구현해 일관 해싱을 지원해요. 이는 실패 시 탄력성을 개선해요. 또한 클러스터에 추가된 노드들이 다른 노드와 상태를 동기화할 필요 없이 주어진 5-tuple에 대해 클러스터 전체에서 일관된 백엔드 선택을 하게 되므로 더 나은 로드 밸런싱 특성을 제공해요. 마찬가지로 백엔드 제거 시 백엔드 조회 테이블은 관련 없는 백엔드에 대한 중단을 최소화하며(재배정에서 최대 1% 차이) 주어진 서비스에 대해 재프로그램돼요.

서비스 로드 밸런싱을 위한 Maglev 해싱은 loadBalancer.algorithm=maglev로 설정해 활성화할 수 있어요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set loadBalancer.algorithm=maglev \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set loadBalancer.algorithm=maglev \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Maglev 해싱은 외부(N-S) 트래픽에만 적용된다는 점에 주의하세요. 인클러스터 서비스 연결(E-W)의 경우 소켓은 중간 홉 없이 서비스 백엔드에 직접 할당되며(예: TCP connect 시점), 따라서 Maglev의 적용을 받지 않아요. Maglev 해싱은 Cilium의 XDP 가속화에서도 지원돼요.

Maglev 관련 구성 설정이 두 가지 더 있어요: maglev.tableSize와 maglev.hashSeed.

maglev.tableSize는 각 단일 서비스에 대한 Maglev 조회 테이블의 크기를 지정해요. Maglev는 테이블 크기(M)가 예상 최대 백엔드 수(N)보다 훨씬 크기를 권장해요. 실제로 이는 백엔드 변경 시 재배정에서 최대 1% 차이의 특성을 보장하기 위해 M이 100 * N보다 커야 함을 의미해요. M은 소수여야 해요. Cilium은 M의 기본 크기로 16381을 사용해요. maglev.tableSize Helm 옵션으로 다음 크기의 M이 지원돼요:

maglev.tableSize value
251
509
1021
2039
4093
8191
16381
32749
65521
131071

예를 들어 maglev.tableSize 16381은 서비스당 최대 ~160 백엔드에 적합해요. 이 설정에서 더 많은 수의 백엔드가 프로비저닝되면 백엔드 변경 시 재배정의 차이가 증가할 거예요. 테이블 크기(M)를 변경하면 조회 테이블의 재계산이 트리거되고, 모든 노드가 수렴하고 agent 재시작을 완료할 때까지 새 트래픽에 대한 백엔드 선택이 일시적으로 일관되지 않을 수 있다는 점에 주의하세요.

maglev.hashSeed 옵션은 Cilium이 고정된 내장 시드를 사용하지 않도록 설정하는 것이 권장돼요. 시드는 base64로 인코딩된 12바이트 난수이며, 예를 들어 head -c12 /dev/urandom | base64 -w0로 한 번 생성할 수 있어요. Maglev가 동작하려면 클러스터의 모든 Cilium agent가 같은 hash seed를 사용해야 해요.

아래 배포 예시는 그런 시드를 생성해 Helm에 전달하고, 주어진 서비스에 대해 ~650 최대 백엔드(백엔드 재배정 시 최대 1% 차이 특성 포함)를 허용하도록 Maglev 테이블 크기를 65521로 설정해요:

SEED=$(head -c12 /dev/urandom | base64 -w0)

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set loadBalancer.algorithm=maglev \
   --set maglev.tableSize=65521 \
   --set maglev.hashSeed=$SEED \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set loadBalancer.algorithm=maglev \
   --set maglev.tableSize=65521 \
   --set maglev.hashSeed=$SEED \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Maglev를 활성화하면 기본 loadBalancer.algorithm=random에 비해 각 Cilium 관리 Node에서 더 높은 메모리 소비가 발생한다는 점에 주의하세요. random은 추가 조회 테이블이 필요 없기 때문이에요. 그러나 random은 일관된 백엔드 선택을 하지 않아요.

DSR (Direct Server Return)

기본적으로 Cilium의 eBPF NodePort 구현은 SNAT 모드로 동작해요. 즉 노드 외부 트래픽이 도착하고 노드가 LoadBalancer, NodePort 또는 externalIPs가 있는 서비스의 백엔드가 원격 노드에 있다고 판단하면, 노드는 SNAT을 수행해 자신을 대신해 요청을 원격 백엔드로 리다이렉트해요. 이는 추가 MTU 변경이 필요 없어요. 비용은 백엔드의 응답이 그 노드로 한 번 더 홉을 해서 역방향 SNAT 변환을 수행한 후 패킷을 외부 클라이언트에 직접 반환해야 한다는 것이에요.

이 설정은 loadBalancer.mode Helm 옵션을 dsr로 바꿔 Cilium의 eBPF NodePort 구현이 DSR 모드로 동작하게 할 수 있어요. 이 모드에서 백엔드는 추가 홉을 거치지 않고 외부 클라이언트에 직접 응답해요. 즉 백엔드는 서비스 IP/포트를 소스로 사용해 응답해요.

DSR 모드의 또 다른 장점은 클라이언트의 소스 IP가 보존되어 백엔드 노드에서 정책이 그것과 매칭할 수 있다는 것이에요. SNAT 모드에서는 이것이 불가능해요. 특정 백엔드는 여러 서비스에서 사용될 수 있으므로, 백엔드는 자신이 응답해야 하는 서비스 IP/포트를 인지해야 해요. Cilium은 이 정보를 패킷에 인코딩하고(아래 설명된 디스패치 메커니즘 중 하나 사용), 더 낮은 MTU를 광고하는 비용이 들어요. TCP 서비스의 경우 Cilium은 SYN 패킷에만 서비스 IP/포트를 인코딩하고 이후 패킷에는 인코딩하지 않아요. 이 최적화는 또한 이후 하위 섹션에서 자세히 설명하는 하이브리드 모드로 Cilium을 운영할 수 있게 해주는데, 그 모드에서는 불필요한 MTU 감소를 피하기 위해 TCP에는 DSR을, UDP에는 SNAT을 사용해요.

일부 소스/목적지 IP 주소 검사를 구현하는 공용 클라우드 제공자 환경(예: AWS)에서는 DSR 모드가 동작하도록 이 검사를 비활성화해야 해요.

기본적으로 Cilium은 CVE-2020-8554 MITM 취약점에 대한 특별한 ExternalIP 완화(mitigation)를 사용해요. 이는 같은 클러스터에서 ExternalIP로 향하는 연결성에 영향을 줄 수 있어요. 이 완화는 bpf.disableExternalIPMitigation을 true로 설정해 비활성화할 수 있어요.

Dispatch 선택을 돕기 위해 다음 표는 라우팅 모드(Native/Tunnel)와 터널 프로토콜에 따라 지원되는 DSR Dispatch Mode를 지정해요.

DSR Dispatch Mode Native Tunnel (Geneve) Tunnel (VXLAN)
Option (OPT) ✅ ❌ ❌
Geneve ✅ ✅ ❌

IPv4 option / IPv6 확장 헤더를 사용한 DSR (Direct Server Return with IPv4 option / IPv6 extension Header)

이 DSR 디스패치 모드에서 서비스 IP/포트 정보는 Cilium 특유의 IPv4 Option 또는 IPv6 Destination Option 확장 헤더를 통해 백엔드로 전달돼요. Cilium이 Native-Routing으로 배포되어야 하며, 즉 Encapsulation 모드에서는 동작하지 않아요.

이 DSR 모드는 Cilium 특유의 IP 옵션이 기본 네트워크 패브릭에 의해 드롭될 수 있어 일부 공용 클라우드 제공자 환경에서 동작하지 않을 수 있어요. 주어진 NodePort 요청을 처리하는 노드에서 원격 노드에 백엔드가 있는 서비스에 연결 문제가 발생하면, 먼저 NodePort 요청이 실제로 백엔드를 포함한 노드에 도착했는지 확인하세요. 그렇지 않았다면 Geneve를 사용한 DSR(아래 설명)로 전환하거나 기본 SNAT 모드로 돌아가는 것을 고려하세요.

DSR-only 모드가 활성화된 kube-proxy-free 환경의 위 Helm 예제 구성은 다음과 같아요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=opt \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=opt \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Geneve를 사용한 DSR (Direct Server Return with Geneve)

기본적으로 DSR 모드의 Cilium은 서비스 IP/포트를 Cilium 특유의 IPv4 option 또는 IPv6 Destination Option 확장에 인코딩해 백엔드가 응답해야 할 서비스 IP/포트를 알게 해요.

그러나 일부 데이터센터 라우터는 알 수 없는 IP 옵션이 있는 패킷을 "Layer 2 slow path"라는 소프트웨어 처리로 전달해요. 그 라우터들은 IP 옵션이 있는 패킷의 양이 주어진 임계값을 초과하면 패킷을 드롭하며, 이는 네트워크 성능에 상당한 영향을 줄 수 있어요.

Cilium은 이 문제를 피하기 위해 DSR with Geneve라는 또 다른 디스패치 모드를 제공해요. DSR with Geneve에서 Cilium은 Geneve 헤더에 서비스 IP/포트를 포함해 Loadbalancer로 가는 패킷을 캡슐화하고 이를 백엔드로 리다이렉트해요.

라우팅 모드를 Geneve Encapsulation을 사용해 트래픽을 라우팅하도록 구성했든 Native-Routing을 사용했든, LoadBalancer 트래픽을 캡슐화하는 데 DSR with Geneve를 사용할 수 있어요.

Native (Routing) Tunnel (encapsulation) DSR Geneve dispatch와 Native (routing) Mode로 helm install을 통해 Cilium 설치

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set tunnelProtocol=geneve \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=geneve \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set tunnelProtocol=geneve \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=geneve \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

DSR Geneve dispatch와 tunneling (encapsulation) mode로 helm install을 통해 Cilium 설치

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=tunnel \
   --set tunnelProtocol=geneve \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=geneve \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=tunnel \
   --set tunnelProtocol=geneve \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=dsr \
   --set loadBalancer.dsrDispatch=geneve \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

IPIP를 사용한 DSR (Direct Server Return with IPIP)

Cilium은 세 번째 DSR 디스패치 모드인 DSR with IPIP를 제공해요. 이는 백엔드로 가는 LoadBalancer 트래픽을 일반 IPIP(IPv4-in-IPv4) 또는 IP6IP6(IPv6-in-IPv6) 터널로 캡슐화해요. 로드 밸런싱 노드에서 원본 클라이언트 패킷(목적지가 여전히 서비스 IP/포트로 설정된)이 내부 페이로드가 돼요. Cilium은 그런 다음 소스가 로드 밸런싱 노드이고 목적지가 선택된 백엔드 Pod의 IP 주소인 외부 IP 헤더를 추가해요. 캡슐화된 패킷은 그 Pod를 호스팅하는 노드로 라우팅되는데, 그곳에서 Cilium이 터널을 종료하고 외부 헤더를 제거한 후 DNAT을 수행해 외부 목적지 주소가 식별한 백엔드 Pod에 내부 패킷을 전달해요.

Geneve를 사용한 DSR과 비교해 IPIP dispatch는 서비스 IP/포트를 백엔드로 전달하기 위해 Geneve option에 의존하지 않으며, IPv4 option / IPv6 확장 헤더 dispatch와 비교해 일부 네트워크 패브릭이 드롭하는 Cilium 특유의 IP 옵션을 피해요 (또한 IP 옵션은 GRO와 잘 작동하지 않아요). 이는 다른 디스패치 방법이 연결 문제를 겪는 환경에서 DSR with IPIP를 견고한 선택으로 만들어요.

Cilium이 Native-Routing으로 배포되어야 하며, Encapsulation 모드에서는 동작하지 않아요.

Note IPIP dispatch를 사용할 때는 Geneve와 달리 IPIP가 포트 정보를 운반하지 않으므로 서비스 프론트엔드 포트가 백엔드(target) 포트와 같아야 해요. 아래 서비스 예제에서 port와 targetPort가 둘 다 80으로 설정된 이유가 이것이에요. 프론트엔드와 백엔드 포트 일치는 일반적으로 DSR에 IPIP를 사용할 때 전제 조건이에요.

아래 Helm 예제 구성은 전역 기본 포워딩 모드를 SNAT(loadBalancer.mode=snat)로 유지하고, service.cilium.io/forwarding-mode: dsr 어노테이션을 통해 서비스별로 DSR IPIP를 선택해요. Annotation-based DSR and SNAT Mode에 설명된 대로, 이는 서비스별 DSR 패킷이 백엔드로 어떻게 디스패치되는지 정의하기 위해 bpf.lbModeAnnotation=true와 함께 명시적인 loadBalancer.dsrDispatch=ipip가 필요해요. 예제는 또한 어노테이션 기반 로드 밸런싱 알고리즘 선택(bpf.lbAlgorithmAnnotation=true)을 선택해서 아래에서 보여주는 service.cilium.io/lb-algorithm 어노테이션이 효과를 발휘하게 해요. loadBalancer.dsrDispatch=ipip를 선택하면 백엔드 노드에서 인바운드 IPIP 종료(외부 헤더 제거 및 내부 패킷을 로컬 백엔드 Pod에 전달)가 자동으로 활성화되므로 추가 설정이 필요 없어요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=snat \
   --set loadBalancer.dsrDispatch=ipip \
   --set bpf.lbModeAnnotation=true \
   --set bpf.lbAlgorithmAnnotation=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=snat \
   --set loadBalancer.dsrDispatch=ipip \
   --set bpf.lbModeAnnotation=true \
   --set bpf.lbAlgorithmAnnotation=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

다음 예제는 nginx 웹 서버를 백엔드로 배포하고 그것을 LoadBalancer 서비스를 통해 노출해요. 이 예제에서는 서비스를 특정 로드 밸런싱 노드에만 노출해요 (Selective Service Node Exposure도 참고):

$ kubectl label node node_name service.cilium.io/node=beefy

nginx 백엔드를 배포하세요:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.1
          ports:
            - containerPort: 80

그런 다음 DSR 포워딩 모드, Maglev 로드 밸런싱 알고리즘, 어노테이션을 통한 노드 노출을 선택하는 LoadBalancer 서비스로 노출하세요:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    service.cilium.io/type: LoadBalancer
    service.cilium.io/forwarding-mode: dsr
    service.cilium.io/lb-algorithm: maglev
    service.cilium.io/node: beefy
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer

이렇게 하면 서비스는 service.cilium.io/node=beefy로 라벨링된 노드에만 LoadBalancer 타입으로 설치되고, 요청은 Maglev 일관 해싱을 사용해 nginx 백엔드로 분산되며, 패킷은 IPIP 캡슐화로 백엔드 노드에 디스패치돼요. 백엔드는 로드 밸런싱 노드를 다시 거치지 않고 클라이언트에 직접 응답해요.

Note DSR에서는 백엔드 노드가 서비스 처리의 일부가 돼요: 인바운드 IPIP 트래픽을 종료하고 응답을 재작성해 서비스 주소에서 온 것으로 소싱해 클라이언트에 직접 반환해요. 이를 위해서는 백엔드 노드에 서비스가 존재해야 해요. 따라서 DSR IPIP를 Selective Service Node Exposure와 결합할 때는 백엔드 Pod가 노출 집합의 일부인 노드(여기서는 service.cilium.io/node=beefy로 라벨링된 노드)에서 실행되어야 해요. LoadBalancer 프론트엔드도 백엔드도 호스팅하지 않는 노드만 제외될 수 있어요.

이는 트래픽을 완전히 변환하고 응답이 그 노드를 통해 다시 흐르는 SNAT 포워딩에는 적용되지 않아요. 그 경우 백엔드 노드는 서비스 처리에 참여하지 않으며 서비스(또는 라벨)를 설치할 필요가 없어요.

service.cilium.io/forwarding-mode와 service.cilium.io/lb-algorithm 어노테이션은 서비스 생성 시점에 설정해야 하며 해당 서비스의 수명 동안 변경해서는 안 된다는 점에 주의하세요. 서비스가 설치된 상태에서 어노테이션 값을 변경하거나 제거하면 연결이 끊겨요.

하이브리드 DSR 및 SNAT 모드 (Hybrid DSR and SNAT Mode)

Cilium은 하이브리드 DSR 및 SNAT 모드도 지원해요. 즉 TCP 연결에는 DSR, UDP 연결에는 SNAT이 수행돼요.

이를 통해 응답의 추가 홉을 제거한 지연 시간 개선의 이점을 누리면서 네트워크에서 수동 MTU 변경의 필요성을 없애요. 특히 TCP가 워크로드의 주요 전송 계층일 때 그렇습니다.

loadBalancer.mode 모드 설정은 dsr, snat, annotation, hybrid 옵션으로 동작을 제어할 수 있게 해줘요. 기본적으로 agent에서는 snat 모드가 사용돼요.

kube-proxy-free 환경에서 hybrid 모드로 DSR을 활성화한 Helm 예제 구성은 다음과 같아요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=hybrid \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=hybrid \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

어노테이션 기반 DSR 및 SNAT 모드 (Annotation-based DSR and SNAT Mode)

Cilium은 어노테이션 기반 DSR 및 SNAT 모드도 지원해요. 즉 서비스를 기본적으로 SNAT로, 온디맨드로 DSR로(또는 그 반대로) 노출할 수 있어요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/type: LoadBalancer
    service.cilium.io/forwarding-mode: dsr
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer

forwarding-mode 어노테이션은 서비스 생성 시점에 설정해야 하며 해당 서비스의 수명 동안 변경해서는 안 된다는 점에 주의하세요. 서비스가 설치된 상태에서 어노테이션 값을 변경하거나 어노테이션을 제거하면 연결이 끊겨요.

위 예제는 Kubernetes 서비스를 LoadBalancer 타입으로만 설치해요. 즉 해당 NodePort와 ClusterIP 서비스 없이, 기본 SNAT 대신 구성된 DSR 방법을 사용해 패킷을 전달해요. 이 예제에서 Helm 설정 loadBalancer.mode=snat이 기본을 SNAT로 정의해요. loadBalancer.mode=dsr이면 기본이 DSR로 바뀌고, service.cilium.io/forwarding-mode: snat 어노테이션을 사용해 SNAT으로 전환할 수 있어요.

kube-proxy-free 환경에서 SNAT 기본값과 함께 annotation 모드로 DSR을 활성화한 Helm 예제 구성은 다음과 같아요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=snat \
   --set loadBalancer.dsrDispatch=geneve \
   --set bpf.lbModeAnnotation=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.mode=snat \
   --set loadBalancer.dsrDispatch=geneve \
   --set bpf.lbModeAnnotation=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Note 이전 예제처럼 어노테이션 기반 DSR 모드(bpf.lbModeAnnotation=true)를 사용할 때는 DSR 패킷이 백엔드로 어떻게 디스패치되는지 정의하기 위해 loadBalancer.dsrDispatch 파라미터를 명시적으로 지정해야 해요. 유효한 옵션은 opt, ipip, geneve예요.

어노테이션 기반 로드 밸런싱 알고리즘 선택 (Annotation-based Load Balancing Algorithm Selection)

Cilium은 service.cilium.io/lb-algorithm 어노테이션을 통해 서비스별로 로드 밸런싱 알고리즘을 지정할 수 있어요. bpf.lbAlgorithmAnnotation=true를 설정하면 BPF 및 해당 agent 코드에서 이 기능을 선택해요. 전형적인 사용 사례는 Maglev가 각 서비스에 대해 큰 조회 테이블을 요구하기 때문에 그에 따른 메모리 풋프린트를 줄이는 것이에요. 따라서 모든 서비스가 일관 해싱을 필요로 하지 않는다면 랜덤 선택으로 대체할 수 있어요.

기본적으로 서비스 어노테이션이 제공되지 않으면 로직은 loadBalancer.algorithm으로 전역적으로 지정된 방법을 사용하는 것으로 대체돼요. 후자는 현재 random 또는 maglev 값을 지원하며, loadBalancer.algorithm이 Helm으로 명시적으로 설정되지 않은 경우 random이 기본값이에요.

로드 밸런싱 알고리즘으로 random을 사용해야 하는 새 서비스를 추가하려면:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/lb-algorithm: random
spec:
  selector:
    app: example
  ports:
    - port: 8765
      targetPort: 9376
  type: LoadBalancer

마찬가지로 maglev를 선택하려면 다음을 사용하세요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/lb-algorithm: maglev
spec:
  selector:
    app: example
  ports:
    - port: 8765
      targetPort: 9376
  type: LoadBalancer

이제 모든 north-south 트래픽은 후자 예제의 경우 maglev 기반 로드 밸런싱을 받아요.

service.cilium.io/lb-algorithm은 초기 서비스 생성 시에만 적용되며 주어진 Kubernetes 서비스의 수명 동안 변경할 수 없다는 점에 주의하세요. 로드 밸런싱 알고리즘 사이를 전환하려면 서비스를 재생성해야 해요.

어노테이션 기반 로드 밸런싱 가중치 (Annotation-based Load Balancing Weight)

백엔드 가중치는 service.cilium.io/weight 어노테이션을 통해 EndpointSlice별로 할당할 수 있어요.

가중치는 Maglev가 전역적으로 구성됐든 service.cilium.io/lb-algorithm을 통해 서비스별로 선택됐든 해당 EndpointSlice가 참조하는 Service가 Maglev 로드 밸런싱 알고리즘을 사용할 때만 존중돼요. 서비스별 알고리즘 선택에는 bpf.lbAlgorithmAnnotation=true가 필요해요. 이는 주로 수동으로 관리되는 여러 EndpointSlice 객체가 백업하는 selectorless Service를 위한 것이며, 각 EndpointSlice가 그 Service에 기여하는 백엔드에 서로 다른 가중치를 할당할 수 있어요.

예를 들어 다음 selectorless Service는 Maglev 로드 밸런싱 알고리즘을 사용하도록 명시적으로 표시되고, 가중치 70과 30을 가진 두 EndpointSlice 객체가 백업해요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/lb-algorithm: maglev
spec:
  ports:
    - name: http
      port: 8080
      protocol: TCP
      targetPort: 8080
  type: ClusterIP
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: example-service-1
  labels:
    kubernetes.io/service-name: example-service
  annotations:
    service.cilium.io/weight: "70"
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses:
      - 10.0.0.11
  - addresses:
      - 10.0.0.12
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: example-service-2
  labels:
    kubernetes.io/service-name: example-service
  annotations:
    service.cilium.io/weight: "30"
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses:
      - 10.0.0.21
  - addresses:
      - 10.0.0.22

구성된 가중치는 주어진 EndpointSlice의 모든 백엔드에 적용돼요. 유효한 값의 범위는 0부터 65535까지예요. 가중치 0은 해당 백엔드를 유지보수 중으로 표시해서 기존 연결은 계속되고 새 트래픽은 다른 백엔드로 향하게 해요. 유효하지 않은 값은 무시돼요. 가중치는 상대적이며 합이 100이 될 필요는 없어요.

위 예제에서 새 트래픽은 두 EndpointSlice 객체의 상대적 가중치에 따라 분산돼요. 즉 대략 70%%가 example-service-1의 백엔드로, 30%%가 example-service-2의 백엔드로 가요.

두 번째 EndpointSlice가 나중에 가중치 30에서 0으로 변경되면 그 백엔드가 모두 유지보수 상태에 들어가요. 기존 연결은 계속 사용할 수 있지만, 새 트래픽은 전적으로 example-service-1의 나머지 비-유지보수 백엔드로 향하게 돼요. 즉 첫 번째 slice가 이제 새 트래픽 점유율의 100%%를 받아요.

파드 네임스페이스의 Socket LoadBalancer 우회 (Socket LoadBalancer Bypass in Pod Namespace)

소켓 수준 로드밸런서는 Cilium의 하위 레이어 데이터 경로에 투명하게 작동해요. connect(TCP, 연결된 UDP), sendmsg(UDP), 또는 recvmsg(UDP) 시스템 콜 시 목적지 IP가 기존 서비스 IP인지 확인하고 서비스 백엔드 중 하나가 대상으로 선택돼요. 이는 애플리케이션이 서비스 주소에 연결됐다고 가정하지만 실제로는 해당 커널 소켓이 백엔드 주소에 연결되어 추가 하위 레이어 NAT가 필요 없음을 의미해요.

Cilium은 커스텀 리다이렉션/작업이 파드 네임스페이스 안의 원래 ClusterIP에 의존할 때(예: Istio sidecar) 또는 Pod의 특성상 소켓 수준 로드밸런서가 비효율적일 때(예: KubeVirt, Kata Containers, gVisor), 소켓 수준 로드밸런서를 우회하고 veth 인터페이스에서 tc 로드밸런서로 대체하는 내장 지원을 갖고 있어요.

socketLB.hostNamespaceOnly=true를 설정하면 이 우회 모드를 활성화해요. 활성화되면 connect()와 sendmsg() syscall bpf hook에서 소켓 재작성을 우회하고 원본 패킷을 다음 동작 단계(예: per-endpoint-routing 모드의 스택)로 전달하며 tc bpf 프로그램에서 서비스 조회를 다시 활성화해요.

socket LB bypass가 있는 kube-proxy-free 환경의 Helm 예제 구성은 다음과 같아요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set socketLB.hostNamespaceOnly=true
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set socketLB.hostNamespaceOnly=true

LoadBalancer & NodePort XDP 가속화 (LoadBalancer & NodePort XDP Acceleration)

Cilium은 도착한 요청을 전달해야 하고 백엔드가 원격 노드에 있는 경우 NodePort, LoadBalancer 서비스 및 externalIPs가 있는 서비스를 가속화하는 내장 지원을 갖고 있어요. 이 기능은 eBPF가 상위 레이어 대신 네트워킹 드라이버에서 직접 동작하는 XDP(eXpress Data Path) 레이어에서 Cilium 버전 1.8에 도입됐어요.

loadBalancer.acceleration을 옵션 native로 설정하면 이 가속화가 활성화돼요. 옵션 disabled가 기본값이며 가속화를 비활성화해요. 10G 이상 속도를 지원하는 대부분의 드라이버는 최신 커널에서 native XDP를 지원해요. 클라우드 기반 배포의 경우 대부분의 이러한 드라이버에 native XDP를 지원하는 SR-IOV 변형이 있어요. 온프레미스 배포의 경우 Cilium XDP 가속화는 Kubernetes용 MetalLB 같은 LoadBalancer 서비스 구현과 함께 사용할 수 있어요. 가속화는 direct routing에 사용되는 단일 디바이스에서만 활성화할 수 있어요.

고규모 환경에서는 예를 들어 더 높은 config.bpfMapDynamicSizeRatio를 설정해 기본 맵 크기를 더 큰 항목 수로 조정하는 것도 고려하세요. 자세한 내용은 eBPF Maps를 참고하세요.

loadBalancer.acceleration 설정은 DSR, SNAT, hybrid 모드에서 지원되며, 이 예제에서 loadBalancer.mode=hybrid에 대해 다음과 같이 활성화할 수 있어요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=hybrid \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set routingMode=native \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=hybrid \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

Cilium의 디바이스 자동 감지가 NodePort를 노출할 단일 디바이스보다 많은 것을 선택하거나 사용자가 devices로 여러 디바이스를 지정하는 multi-device 환경의 경우, XDP 가속화는 모든 디바이스에서 활성화돼요. 즉 각 기본 디바이스의 드라이버가 모든 Cilium 관리 노드에서 native XDP를 지원해야 해요. 일부 디바이스는 XDP를 지원하지만 다른 디바이스는 지원하지 않는 환경이 있다면 loadBalancer.acceleration을 best-effort로 설정해 지원되는 디바이스에서 XDP를 활성화할 수 있어요.

XDP를 지원하는 드라이버 목록은 the XDP documentation에서 찾을 수 있어요.

현재 Cilium kube-proxy XDP 가속화 모드는 cilium-dbg status CLI 명령으로도 들여다볼 수 있어요. 성공적으로 활성화되면 Native가 표시돼요:

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status --verbose | grep XDP
  XDP Acceleration:    Native

NodePort 처리를 위해 바로 XDP 레이어에서 디바이스 밖으로 다시 밀어진 패킷은, 패킷 탭이 네트워킹 스택에서 훨씬 늦은 단계에 오기 때문에 tcpdump에서 보이지 않는다는 점에 주의하세요. 가시성을 얻으려면 Cilium의 monitor 명령이나 메트릭 카운터를 사용할 수 있어요.

AWS에서 NodePort XDP (NodePort XDP on AWS)

AWS에서 NodePort XDP로 실행하려면 Cilium Quick Installation 가이드의 지침을 따라 EKS 클러스터를 설정하거나, 선호하는 다른 방법으로 Kubernetes 클러스터를 설정하세요.

EKS 가이드를 따른다면 추가 설정 단계가 필요하고 Elastic Network Adapter (ena)를 지원하는 더 큰 인스턴스 타입도 만들어야 하므로, SSH 접근이 있는 노드 그룹을 만들어야 해요. 인스턴스 예시로 nodegroup-config.yaml 구성에서 m5n.xlarge를 사용해요:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: test-cluster
  region: us-west-2

nodeGroups:
  - name: ng-1
    instanceType: m5n.xlarge
    desiredCapacity: 2
    ssh:
      allow: true
    ## taint nodes so that application pods are
    ## not scheduled/executed until Cilium is deployed.
    ## Alternatively, see the note below.
    taints:
      - key: "node.cilium.io/agent-not-ready"
        value: "true"
        effect: "NoExecute"

Note taint effects and unmanaged pods 문서 페이지를 읽고 이해하세요.

노드 그룹은 다음으로 생성돼요:

$ eksctl create nodegroup -f nodegroup-config.yaml

각 노드에는 kernel-ng와 ethtool 패키지를 설치해야 해요. 전자는 일반적으로 eBPF와 ena 드라이버의 native XDP 지원에 충분히 최신 커널을 실행하는 데 필요하고, 후자는 NIC의 채널 파라미터를 구성하는 데 필요해요.

$ IPS=$(kubectl get no -o jsonpath='{$.items[*].status.addresses[?(@.type=="ExternalIP")].address }{"\\n"}' | tr ' ' '\\n')

$ for ip in $IPS ; do ssh ec2-user@$ip "sudo amazon-linux-extras install -y kernel-ng && sudo yum install -y ethtool && sudo reboot"; done

노드가 다시 뜨면 커널 버전이 uname -r로 5.4.58-27.104.amzn2.x86_64 또는 유사해야 해요. ena에서 XDP를 실행하려면 드라이버 버전이 최소 2.2.8인지 확인하세요. 드라이버 버전은 ethtool -i eth0으로 검사할 수 있어요. 주어진 커널 버전에 대해 드라이버 버전은 2.2.10g로 보고되어야 해요.

Cilium의 XDP 가속화를 배포하기 전에 네트워크 어댑터 쪽에 두 가지 설정이 필요해요. 즉 MTU를 XDP로 운영할 수 있게 낮춰야 하고, combined channels 수를 조정해야 해요.

ena 드라이버의 기본 MTU는 9001로 설정돼요. XDP 버퍼는 선형이므로 단일 페이지에서 동작해요. 드라이버는 일반적으로 XDP용 headroom도 일부 예약하므로(예: 캡슐화 목적), XDP에 가능한 가장 높은 MTU는 3498이에요.

ena 채널 측면에서 설정은 ethtool -l eth0으로 수집할 수 있어요. m5n.xlarge 인스턴스의 기본 출력은 다음과 같아야 해요:

Channel parameters for eth0:
Pre-set maximums:
RX:             0
TX:             0
Other:          0
Combined:       4
Current hardware settings:
RX:             0
TX:             0
Other:          0
Combined:       4

XDP를 사용하려면 채널을 위 Combined 값의 최대 1/2로 설정해야 해요. MTU와 채널 변경은 모두 다음과 같이 적용돼요:

$ for ip in $IPS ; do ssh ec2-user@$ip "sudo ip link set dev eth0 mtu 3498"; done
$ for ip in $IPS ; do ssh ec2-user@$ip "sudo ethtool -L eth0 combined 2"; done

Cilium을 배포하려면 Kubernetes API server IP와 포트가 필요해요:

$ export API_SERVER_IP=$(kubectl get ep kubernetes -o jsonpath='{$.subsets[0].addresses[0].ip}')
$ export API_SERVER_PORT=443

마지막으로 Cilium에서 XDP를 활성화하려면 loadBalancer.acceleration=native 설정으로 배포를 업그레이드하고 나중에 롤아웃할 수 있어요:

Helm Repository OCI Registry

helm upgrade cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --reuse-values \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=snat \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm upgrade cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --reuse-values \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=snat \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
Azure에서 NodePort XDP (NodePort XDP on Azure)

Azure AKS 또는 Azure에서 실행되는 자체 관리 Kubernetes에서 NodePort XDP를 활성화하려면 Kubernetes를 실행하는 가상 머신에 Accelerated Networking이 활성화되어 있어야 해요. 또한 노드의 Linux 커널이 hv_netvsc 드라이버에서 native XDP를 지원해야 하며, 이는 커널 >= 5.6에서 사용 가능하고 5.4.0-1022의 Azure Linux 커널로 백포트됐어요.

AKS에서는 Kubernetes 버전 v1.26인 AKS Ubuntu 22.04 노드 이미지를 사용해야 해요. 이는 hv_netvsc 드라이버에 필요한 백포트를 갖춘 Linux 커널을 제공해요. AKS 클러스터 구성 방법 문서를 참고하세요.

가상 머신이나 가상 머신 스케일 세트를 만들 때 가속화 네트워킹을 활성화하려면 Azure CLI에 --accelerated-networking 옵션을 전달하세요. Azure CLI로 Accelerated Networking이 있는 Linux 가상 머신 만들기 가이드를 참고하세요.

Accelerated Networking이 활성화되면 lspci가 Mellanox ConnectX NIC를 보여줘요:

$ lspci | grep Ethernet
2846:00:02.0 Ethernet controller: Mellanox Technologies MT27710 Family [ConnectX-4 Lx Virtual Function] (rev 80)

XDP 가속화는 ConnectX-4 Lx 이상의 NIC에서만 활성화할 수 있어요.

XDP를 실행하려면 hv_netvsc 디바이스에서 large receive offload(LRO)를 비활성화해야 해요. 아직 아니라면 다음으로 달성할 수 있어요:

$ ethtool -K eth0 lro off

파드 IP 주소 할당에는 Azure IPAM을 사용하는 것이 권장돼요. 이는 파드 트래픽을 올바르게 라우팅하도록 가상 네트워크를 자동으로 구성해요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set ipam.mode=azure \
   --set azure.enabled=true \
   --set azure.resourceGroup=$AZURE_NODE_RESOURCE_GROUP \
   --set azure.subscriptionID=$AZURE_SUBSCRIPTION_ID \
   --set azure.tenantID=$AZURE_TENANT_ID \
   --set azure.clientID=$AZURE_CLIENT_ID \
   --set azure.clientSecret=$AZURE_CLIENT_SECRET \
   --set routingMode=native \
   --set enableIPv4Masquerade=false \
   --set devices=eth0 \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=snat \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set ipam.mode=azure \
   --set azure.enabled=true \
   --set azure.resourceGroup=$AZURE_NODE_RESOURCE_GROUP \
   --set azure.subscriptionID=$AZURE_SUBSCRIPTION_ID \
   --set azure.tenantID=$AZURE_TENANT_ID \
   --set azure.clientID=$AZURE_CLIENT_ID \
   --set azure.clientSecret=$AZURE_CLIENT_SECRET \
   --set routingMode=native \
   --set enableIPv4Masquerade=false \
   --set devices=eth0 \
   --set kubeProxyReplacement=true \
   --set loadBalancer.acceleration=native \
   --set loadBalancer.mode=snat \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

자체 관리 Kubernetes 클러스터에서 Azure IPAM을 실행할 때는 각 v1.Node가 spec.providerID 필드에 VM의 리소스 ID를 가져야 해요. 자세한 내용은 Azure IPAM 참조를 참고하세요.

GCP에서 NodePort XDP (NodePort XDP on GCP)

Google Cloud Platform에서의 NodePort XDP는 현재 지원되지 않아요. Google Compute Engine에서 사용 가능한 두 가상 네트워크 인터페이스(이전 virtIO 기반 인터페이스와 최신 gVNIC) 모두 현재 native XDP 지원이 부족해요.

NodePort 디바이스, 포트 및 바인드 설정 (NodePort Devices, Port and Bind settings)

Cilium의 eBPF kube-proxy replacement를 실행할 때 기본적으로 NodePort 또는 LoadBalancer 서비스 또는 externalIPs가 있는 서비스는 호스트에 기본 라우트가 있거나 Kubernetes InternalIP 또는 ExternalIP가 할당된 네이티브 디바이스의 IP 주소를 통해 접근 가능해요. 둘 다 존재하면 InternalIP가 ExternalIP보다 우선해요. 디바이스를 변경하려면 devices Helm 옵션에 이름을 설정하세요. 예: devices='eth0,eth1,eth2'. 나열된 각 디바이스는 모든 Cilium 관리 노드에서 같은 이름이어야 해요. 또는 디바이스가 서로 다른 노드에 걸쳐 일치하지 않으면 와일드카드 옵션을 사용할 수 있어요. 예: devices=eth+는 eth 접두사로 시작하는 모든 디바이스와 매칭돼요. 매칭되는 디바이스가 없으면 Cilium agent가 자동 감지를 시도해요.

여러 디바이스를 사용할 때는 하나의 디바이스만 Cilium 노드 간 direct routing에 사용할 수 있어요. 기본적으로 단일 디바이스가 감지되거나 devices로 지정되면 Cilium은 direct routing에 그 디바이스를 사용해요. 그렇지 않으면 Cilium은 Kubernetes InternalIP 또는 ExternalIP가 설정된 디바이스를 사용해요. 둘 다 존재하면 InternalIP가 ExternalIP보다 우선해요. direct routing 디바이스를 변경하려면 nodePort.directRoutingDevice Helm 옵션을 설정하세요. 예: nodePort.directRoutingDevice=eth1. devices 옵션과 마찬가지로 와일드카드 옵션도 사용할 수 있어요. 예: directRoutingDevice=eth+. 와일드카드 옵션과 하나 이상의 디바이스가 매칭되면 Cilium은 이를 영숫자 오름차순으로 정렬해 첫 번째를 선택해요. direct routing 디바이스가 devices 안에 없으면 Cilium이 디바이스를 후자 목록에 추가해요. direct routing 디바이스는 NodePort XDP 가속화에도 사용돼요 (활성화된 경우).

또한 socket-LB 기능 덕분에 NodePort 서비스는 기본적으로 클러스터 내의 호스트나 파드에서 공용 주소, 모든 로컬(docker* 접두사 이름 제외) 또는 루프백 주소(예: 127.0.0.1:NODE_PORT)를 통해 접근할 수 있어요.

kube-apiserver가 기본이 아닌 NodePort 포트 범위를 사용하도록 구성된 경우, 같은 범위를 nodePort.range 옵션으로 Cilium에 전달해야 해요. 예를 들어 10000-32767 범위는 nodePort.range="10000\,32767"로 전달해요. 기본 Kubernetes NodePort 범위는 30000-32767이에요.

NodePort 포트 범위가 임시 포트 범위(net.ipv4.ip_local_port_range)와 겹치면, Cilium은 NodePort 범위를 예약 포트(net.ipv4.ip_local_reserved_ports)에 추가해요. 이는 소스 포트가 서비스 포트와 일치하는 호스트 로컬 애플리케이션의 트래픽을 NodePort 서비스가 가로채는 것을 방지하는 데 필요해요. 예약 포트 수정을 비활성화하려면 nodePort.autoProtectPortRanges를 false로 설정하세요.

기본적으로 NodePort 구현은 NodePort 서비스 포트에 대한 애플리케이션 bind(2) 요청을 방지해요. 그런 경우 애플리케이션은 보통 bind: Operation not permitted 오류를 볼 거예요. 기본적으로 이것은 호스트 네임스페이스에서만 발생하므로 애플리케이션 파드의 bind(2) 요청에는 영향을 주지 않아요. 일반적으로 이 동작에서 제외하려면 전문가 사용자가 nodePort.bindProtection을 false로 전환해 이 설정을 변경할 수 있어요.

BPF 맵 크기 구성 (Configuring BPF Map Sizes)

고규모 환경의 경우 Cilium의 BPF 맵을 항목 수에 대한 더 높은 한도로 구성할 수 있어요. Helm 옵션을 재정의해 이 한도를 조정할 수 있어요.

Cilium의 BPF LB 서비스, 백엔드 및 affinity 맵의 항목 수를 늘리려면 bpf.lbMapMax Helm 옵션 재정의를 고려하세요. 이 LB 맵 크기의 기본값은 65536이에요.

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set bpf.lbMapMax=131072
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set bpf.lbMapMax=131072

컨테이너 HostPort 지원 (Container HostPort Support)

비록 kube-proxy의 일부는 아니지만, Cilium의 eBPF kube-proxy replacement는 Helm CNI chaining 옵션 cni.chainingMode=portmap을 사용하지 않고도 hostPort 서비스 매핑을 네이티브로 지원해요.

kubeProxyReplacement=true를 지정하면 네이티브 hostPort 지원이 자동으로 활성화되므로 추가 조치가 필요 없어요.

추가 hostIP 없이 hostPort가 지정되면 Pod는 NodePort 서비스를 노출하는 데 감지되고 사용된 것과 같은 로컬 주소(예: 설정된 경우 Kubernetes InternalIP 또는 ExternalIP)로 외부 세계에 노출돼요.

또한 Pod는 노드의 루프백 주소(예: 127.0.0.1:hostPort)로도 접근할 수 있어요. hostPort에 더해 Pod에 hostIP도 지정되면 Pod는 주어진 hostIP에서만 노출돼요. hostIP가 0.0.0.0인 것은 hostIP를 지정하지 않은 것과 같은 동작이에요.

hostPort는 충돌을 피하기 위해 구성된 NodePort 포트 범위에 있으면 안 돼요.

hostPort 지원은 Cilium의 eBPF kube-proxy replacement에 의존하며 백그라운드에서 트래픽을 로컬 호스트 포트 백엔드로 향하게 하는 서비스 항목을 연결해요. host port는 Kubernetes 서비스 객체를 통해 구성되지 않으므로 Kubernetes 서비스의 전체 기능 집합(커스텀 Cilium 서비스 어노테이션 같은)을 사용할 수 없어요. 대신 host port는 서비스 처리 동작의 사용자 구성 기본값에 편승해요.

따라서 kube-proxy-free 환경의 예제 배포는 이전 getting started 배포와 같아요:

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

또한 각 노드 IP가 INTERNAL-IP 또는 EXTERNAL-IP로 알려져 있는지 확인하세요. 예를 들어:

$ kubectl get nodes -o wide
NAME   STATUS   ROLES    AGE     VERSION   INTERNAL-IP      EXTERNAL-IP   [...]
apoc   Ready    master   6h15m   v1.17.3   192.168.178.29   <none>        [...]
tank   Ready    <none>   6h13m   v1.17.3   192.168.178.28   <none>        [...]

그렇지 않으면 KUBELET_EXTRA_ARGS를 통해 --node-ip을 지정해 kubelet이 이를 알게 해야 해요. eth0이 공용 facing 인터페이스라고 가정하면 다음으로 달성할 수 있어요:

$ echo KUBELET_EXTRA_ARGS=\"--node-ip=$(ip -4 -o a show eth0 | awk '{print $4}' | cut -d/ -f1)\" | tee -a /etc/default/kubelet

/etc/default/kubelet을 업데이트한 후 kubelet을 재시작해야 해요.

Cilium에서 HostPort 기능이 활성화됐는지 확인하려면 cilium-dbg status CLI 명령이 KubeProxyReplacement 정보 줄을 통해 가시성을 제공해요. 성공적으로 활성화되면 HostPort가 Enabled로 표시돼요. 예를 들어:

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status --verbose | grep HostPort
  - HostPort:       Enabled

추가 hostPort: 8080 파라미터가 있는 아래 수정된 설정 검증 예제 yaml을 사용해 매핑을 검증할 수 있어요:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
          hostPort: 8080

배포 후 Cilium의 eBPF kube-proxy replacement가 지정된 포트 8080에서 컨테이너를 HostPort로 노출했는지 검증할 수 있어요:

$ kubectl exec -it -n kube-system cilium-fmh8d -- cilium-dbg service list
ID   Frontend               Service Type   Backend
[...]
5    192.168.178.29:8080    HostPort       1 => 10.29.207.199:80

마찬가지로 호스트 네임스페이스에서 iptables로 HostPort 서비스에 대한 iptables 규칙이 없는지 검사할 수 있어요:

$ iptables-save | grep HOSTPORT
[ empty line ]

마지막으로 간단한 curl 테스트가 노드의 IP에서 노출된 HostPort 컨테이너에 대한 연결성을 보여줘요:

$ curl 192.168.178.29:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[....]

deployment를 제거하면 cilium-dbg service list 덤프에서 해당 HostPort도 제거돼요:

$ kubectl delete deployment my-nginx

그레이스풀 종료 (Graceful Termination)

Cilium의 eBPF kube-proxy replacement는 서비스 엔드포인트 파드의 그레이스풀 종료를 지원해요. Cilium agent는 그러한 terminating Pod 이벤트를 감지하고 k8s_terminating_endpoints_events_total 메트릭을 증가시켜요.

Cilium agent가 엔드포인트를 terminating으로 표시하는 Kubernetes 업데이트 이벤트를 받으면 Cilium은 기존 연결에 필요한 데이터 경로 상태를 유지해요. terminating 엔드포인트는 1) 서비스에 대해 활성 엔드포인트가 존재하지 않고, 2) terminating 엔드포인트가 serving 조건을 가질 때(예: Pod가 여전히 readinessProbes를 통과하는 경우)에만 새 연결의 fallback으로 사용돼요.

Service에 publishNotReadyAddresses가 설정되면 Cilium이 받는 엔드포인트는 ready와 terminating 조건을 모두 가질 수 있어요. 이 경우 Cilium은 kube-proxy를 따라 이들을 새 연결에 사용하며 terminating 조건은 무시해요.

엔드포인트 상태는 agent가 엔드포인트에 대한 Kubernetes 삭제 이벤트를 받으면 완전히 제거돼요. Kubernetes pod termination 문서에는 terminationGracePeriodSeconds를 사용한 동작과 구성에 대한 더 많은 배경이 있어요. 롤링 업데이트 중 중단 없음 같은 특별한 경우가 몇 가지 있는데, Terminating 기간 동안 여전히 트래픽을 서빙하는 Terminating Pod로 트래픽을 보낼 수 있어야 하는데, Kubernetes 블로그 Advancements in Kubernetes Traffic Engineering이 이를 자세히 설명해요.

Video Cilium의 그레이스풀 종료 지원에 대해 더 알아보려면 eCHO Episode 49: Graceful Termination Support with Cilium 1.11을 확인하세요.

세션 어피니티 (Session Affinity)

Cilium의 eBPF kube-proxy replacement는 Kubernetes 서비스 세션 어피니티를 지원해요. sessionAffinity: ClientIP로 구성된 서비스에 대한 같은 파드 또는 호스트의 각 연결은 항상 같은 서비스 엔드포인트를 선택해요. 어피니티의 기본 타임아웃은 3시간(서비스에 대한 각 요청으로 갱신됨)이지만, 필요하면 Kubernetes의 sessionAffinityConfig로 구성할 수 있어요.

어피니티의 소스는 요청의 출처에 따라 달라져요. 요청이 클러스터 외부에서 서비스로 보내지면 엔드포인트 어피니티를 결정하는 데 요청의 소스 IP 주소가 사용돼요. 요청이 클러스터 내부에서 보내지면 소스는 ClusterIP 서비스를 로드 밸런싱하는 데 socket-LB 기능이 사용되는지 여부에 따라 달라져요. 사용된다면 소스로 클라이언트의 네트워크 네임스페이스 쿠키가 사용돼요. 이를 통해 socket-LB가 동작하는 소켓 레이어에서 어피니티를 구현할 수 있어요 (엔드포인트 선택이 커널이 네트워크 패킷을 만들기 전에 발생하므로 거기에는 소스 IP를 사용할 수 없어요). socket-LB가 사용되지 않으면(즉 로드 밸런싱이 파드 네트워크 인터페이스에서 패킷별로 이루어지면) 요청의 소스 IP 주소가 소스로 사용돼요.

여러 포트가 있는 서비스의 세션 어피니티는 서비스 IP 및 포트별이에요. 즉 같은 소스에서 보내지고 같은 서비스 포트로 향하는 주어진 서비스에 대한 모든 요청은 같은 서비스 엔드포인트로 라우팅되지만, 같은 소스에서 보내졌지만 다른 서비스 포트로 향하는 같은 서비스에 대한 두 요청은 서로 다른 서비스 엔드포인트로 라우팅될 수 있어요.

세션 어피니티 기능을 Maglev 일관 해싱과 함께 사용해 백엔드를 선택한다면, 사용자의 ClientIP 선택을 존중하기 위해 Maglev가 해싱 입력으로 소스 포트를 사용하지 않는다는 점에 주의하세요 (자세한 내용은 GH#26709 참고).

kube-proxy Replacement Health Check server

kube-proxy replacement에 대한 health check server를 활성화하려면 kubeProxyReplacementHealthzBindAddr 옵션을 설정해야 해요 (기본적으로 비활성화). 이 옵션은 health check server가 서빙할 IP 주소와 포트를 받아요. 예: IPv4 인터페이스용으로는 kubeProxyReplacementHealthzBindAddr='0.0.0.0:10256', IPv6용으로는 kubeProxyReplacementHealthzBindAddr='[::]:10256'로 설정하세요. health check server는 HTTP /healthz 엔드포인트로 접근할 수 있어요.

LoadBalancer Source Ranges 검사 (LoadBalancer Source Ranges Checks)

LoadBalancer 서비스가 spec.loadBalancerSourceRanges로 구성되면 Cilium의 eBPF kube-proxy replacement는 외부(예: 외부 세계 트래픽)에서 서비스로의 접근을 그 필드에 지정된 화이트리스트 CIDR로 제한해요. 필드가 비어 있으면 접근에 대한 제한이 적용되지 않아요.

클러스터 내부에서 서비스에 접근할 때 kube-proxy replacement는 설정 여부와 무관하게 그 필드를 무시해요. 즉 클러스터의 모든 파드 또는 모든 호스트 프로세스가 내부적으로 LoadBalancer 서비스에 접근할 수 있어요.

기본적으로 spec.loadBalancerSourceRanges에 지정된 화이트리스트 CIDR은 LoadBalancer 서비스에만 적용되며, LoadBalancer 서비스와 함께 설치되는 해당 NodePort 또는 ClusterIP 서비스에는 적용되지 않아요.

이 동작이 바람직하지 않다면 두 가지 옵션이 있어요. 한 가지 가능성은 service.cilium.io/type 어노테이션으로 해당 NodePort와 ClusterIP 서비스의 생성을 피하는 것이에요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/type: LoadBalancer
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
  loadBalancerSourceRanges:
  - 192.168.1.0/24

다른 가능성은 화이트리스트 CIDR을 모든 외부 노출 서비스 타입으로 전파하는 것이에요. 즉 NodePort뿐 아니라 ClusterIP(외부에서 접근 가능한 경우, External Access To ClusterIP Services 섹션 참고)도 소스 IP 주소에 기반해 트래픽을 필터링해요. 이 옵션은 Helm에서 bpf.lbSourceRangeAllTypes=true로 활성화할 수 있어요.

loadBalancerSourceRanges는 기본적으로 CIDR의 허용 목록을 지정해요. 즉 그 CIDR에서 발생하지 않은 트래픽은 자동으로 드롭돼요.

Cilium은 또한 이 목록을 거부 목록으로 바꿔 특정 CIDR의 트래픽을 차단하면서 나머지는 모두 허용하는 옵션도 지원해요. 이 동작은 allow 또는 deny 값을 받는 service.cilium.io/src-ranges-policy 어노테이션으로 달성할 수 있어요.

기본 loadBalancerSourceRanges 동작은 service.cilium.io/src-ranges-policy: allow와 같아요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/type: LoadBalancer
    service.cilium.io/src-ranges-policy: allow
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
  loadBalancerSourceRanges:
  - 192.168.1.0/24

CIDR 목록을 거부 목록으로 바꿔 이 집합에서 발생하지 않은 트래픽을 허용하려면 service.cilium.io/src-ranges-policy: deny로 변경할 수 있어요:

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/type: LoadBalancer
    service.cilium.io/src-ranges-policy: deny
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
  loadBalancerSourceRanges:
  - 192.168.1.0/24

서비스 프록시 이름 구성 (Service Proxy Name Configuration)

kube-proxy와 마찬가지로 Cilium도 service.kubernetes.io/service-proxy-name 서비스 어노테이션을 존중하며, 일치하는 service-proxy-name 라벨을 포함한 서비스만 관리해요. 이 이름은 k8s.serviceProxyName 옵션을 설정해 구성할 수 있으며 동작은 kube-proxy와 동일해요. 서비스 프록시 이름은 기본적으로 빈 문자열이며, 이는 Cilium이 service.kubernetes.io/service-proxy-name 라벨이 없는 서비스만 관리하도록 지시해요.

service.kubernetes.io/service-proxy-name 라벨의 사용과 동작에 대한 자세한 내용은 이 KEP를 참고하세요.

Note 비어 있지 않은 서비스 프록시 이름을 가진 Cilium이 kube-proxy free 모드에서 모든 서비스를 관리하려면, kube-dns와 kubernetes 같은 기본 Kubernetes 서비스가 필요한 라벨 값을 갖도록 확인하세요.

트래픽 분산 및 토폴로지 인식 힌트 (Traffic Distribution and Topology Aware Hints)

kube-proxy replacement는 Kubernetes Topology Aware Routing과 더 최신의 Traffic Distribution 기능을 모두 구현해요.

이 두 기능 모두 Cilium이 같은 영역(zone)에 있는 엔드포인트로 라우팅할 수 있게 하는 EndpointSlices의 hints를 설정해 동작해요. 기능을 활성화하려면 loadBalancer.serviceTopology=true를 설정하세요.

이웃 탐색 (Neighbor Discovery)

kube-proxy replacement와 XDP 가속화가 활성화되면 Cilium은 클러스터의 노드와 서비스 백엔드의 L2 이웃 탐색을 수행해요. 이는 fast-path에서 요청 시 이웃을 동적으로 해석할 수 없기 때문에 서비스 로드 밸런싱이 백엔드용 L2 주소를 채우는 데 필요해요.

L2 이웃 탐색은 agent가 XDP가 사용 중임을 감지하면 자동으로 활성화되지만, --enable-l2-neigh-discovery=true 플래그나 l2NeighDiscovery.enabled=true Helm 옵션으로 수동으로 켤 수도 있어요.

agent는 같은 L2 네트워크의 게이트웨이나 호스트를 발견하는 것을 전적으로 Linux 커널에 의존해요. Cilium agent에서는 IPv4와 IPv6 이웃 탐색이 모두 지원돼요. Plumbers에서 발표한 커널 작업에 따라 "managed" 이웃 항목이 업스트림되었고 Linux 커널 v5.16 이후에서 사용 가능하며, Cilium agent가 이를 감지해 투명하게 사용해요. 이 경우 agent는 클러스터에 조인하는 새 노드의 L3 주소를 외부에서 학습된 "managed" 이웃 항목으로 밀어 넣어요. 들여다보기 위해 iproute2는 이를 "managed extern_learn"으로 표시해요. extern_learn 속성은 커널의 이웃 하위 시스템에 의한 항목들의 가비지 컬렉션을 방지해요. 그런 "managed" 이웃 항목은 일정 기간 동안 활성 트래픽이 없으면 Linux 커널 자체가 동적으로 해석하고 주기적으로 새로고침해요. 즉 커널은 항상 이를 REACHABLE 상태로 유지하려 시도해요. "managed" 이웃 항목이 없는 Linux 커널 v5.15 이하에서는 Cilium agent가 유사하게 새 노드의 L3 주소를 커널에 밀어 넣어 동적 해석을 해요. 들여다보기 위해 iproute2는 이 경우 extern_learn으로만 표시해요. 일정 기간 동안 활성 트래픽이 없고 항목이 상태가 되면 Cilium agent는 이를 REACHABLE 상태로 유지하려 시도하기 위해 Linux 커널 기반 재해석을 트리거해요.

Cilium agent는 디바이스, 라우트, 이웃을 적극적으로 모니터링하고 커널의 이웃 항목을 조정(reconcile)해요. 예를 들어 디바이스가 추가되면 그 디바이스에 대한 새 이웃 항목이 추가돼요. next-hop 변경 같은 라우트 변경이 있으면 Cilium agent는 이웃 항목을 그에 맞게 업데이트해요. 그리고 carrier-down 이벤트 같은 것으로 이웃 항목이 플러시되면 Cilium agent는 가능한 한 빨리 이웃 항목을 복원해요.

이웃 탐색은 각 노드가 다른 노드로의 여러 디바이스와 여러 next-hop을 갖는 multi-device 환경을 지원해요. Cilium agent는 direct routing 디바이스를 포함해 모든 대상 디바이스에 대한 이웃 항목을 밀어 넣어요. 현재 디바이스당 하나의 next-hop을 지원해요. 다음 예제는 multi-device 환경에서 이웃 탐색이 어떻게 동작하는지 보여줘요. 각 노드는 서로 다른 L3 네트워크(10.69.0.64/26과 10.69.0.128/26)에 연결된 두 디바이스와 전역 범위 주소(각각 10.69.0.1/26과 10.69.0.2/26)를 갖고 있어요. node1에서 node2로의 next-hop은 10.69.0.66 dev eno1 또는 10.69.0.130 dev eno2예요. 이 경우 Cilium agent는 10.69.0.66 dev eno1과 10.69.0.130 dev eno2 모두에 대한 이웃 항목을 밀어 넣어요.

+---------------+     +---------------+
|    node1      |     |    node2      |
| 10.69.0.1/26  |     | 10.69.0.2/26  |
|           eno1+-----+eno1           |
|           |   |     |   |           |
| 10.69.0.65/26 |     |10.69.0.66/26  |
|               |     |               |
|           eno2+-----+eno2           |
|           |   |     | |             |
| 10.69.0.129/26|     | 10.69.0.130/26|
+---------------+     +---------------+

node1에서:

$ ip route show
10.69.0.2
        nexthop via 10.69.0.66 dev eno1 weight 1
        nexthop via 10.69.0.130 dev eno2 weight 1

$ ip neigh show
10.69.0.66 dev eno1 lladdr 96:eb:75:fd:89:fd extern_learn  REACHABLE
10.69.0.130 dev eno2 lladdr 52:54:00:a6:62:56 extern_learn  REACHABLE

ClusterIP 서비스에 대한 외부 접근 (External Access To ClusterIP Services)

k8s Service에 따라 Cilium의 eBPF kube-proxy replacement는 기본적으로 클러스터 외부에서 ClusterIP 서비스로의 접근을 허용하지 않아요. 이는 bpf.lbExternalClusterIP=true로 허용할 수 있어요.

Kubernetes API server 고가용성 (Kubernetes API server high availability)

클러스터에서 여러 인스턴스의 Kubernetes API server를 실행 중이라면 k8s-api-server-urls 플래그를 설정해 Cilium이 활성 인스턴스로 장애 조치(fail over)할 수 있게 할 수 있어요. Cilium은 kubernetes 서비스 주소로 전환해 런타임 중 API 요청이 API server 엔드포인트로 로드 밸런싱되도록 해요. 그러나 agent가 다운된 동안 초기에 구성된 API server가 교체된다면, k8s-api-server-urls 플래그를 업데이트된 API server로 갱신할 수 있어요.

Helm Repository OCI Registry

helm install cilium cilium/cilium --version 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8s.apiServerURLs="https://172.21.0.4:6443 \
   --set https://172.21.0.5:6443 \
   --set https://172.21.0.6:6443"
helm install cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
   --namespace kube-system \
   --set kubeProxyReplacement=true \
   --set k8s.apiServerURLs="https://172.21.0.4:6443 \
   --set https://172.21.0.5:6443 \
   --set https://172.21.0.6:6443"

관찰 가능성 (Observability)

Hubble과 cilium monitor를 사용해 socket LB 관련 데이터 경로 이벤트를 추적할 수 있어요.

다음 pod와 서비스를 적용하세요:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: proxy
spec:
  containers:
  - name: nginx
    image: nginx:stable
    ports:
      - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: proxy
  ports:
  - port: 80

트래픽을 시작할 클라이언트 파드를 배포하세요.

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.20.2/examples/kubernetes-dns/dns-sw-app.yaml
$ kubectl get svc | grep nginx
  nginx-service   ClusterIP   10.96.128.44   <none>        80/TCP    140m

$ kubectl exec -it mediabot -- curl -v --connect-timeout 5 10.96.128.44

Hubble Inspecting Network Flows with the CLI 가이드를 따라 네트워크 흐름을 보세요. Hubble 출력은 서비스와 선택된 서비스 엔드포인트 사이의 socket LB 변환 전후 데이터 경로 이벤트를 출력해요.

$ hubble observe --all | grep mediabot
Jan 13 13:47:20.932: default/mediabot (ID:5618) <> default/nginx-service:80 (world) pre-xlate-fwd TRACED (TCP)
Jan 13 13:47:20.932: default/mediabot (ID:5618) <> default/nginx:80 (ID:35772) post-xlate-fwd TRANSLATED (TCP)
Jan 13 13:47:20.932: default/nginx:80 (ID:35772) <> default/mediabot (ID:5618) pre-xlate-rev TRACED (TCP)
Jan 13 13:47:20.932: default/nginx-service:80 (world) <> default/mediabot (ID:5618) post-xlate-rev TRANSLATED (TCP)
Jan 13 13:47:20.932: default/mediabot:38750 (ID:5618) <> default/nginx (ID:35772) pre-xlate-rev TRACED (TCP)

Hubble로 socket LB 추적하려면 cilium agent가 파드 cgroup 경로를 감지해야 해요. cilium agent에서 Failed to setup socket load-balancing tracing with Hubble. 메시지가 보이면 대신 cilium-dbg monitor를 사용해 패킷을 추적할 수 있어요.

Note 로그에서 socket load-balancing 설정 실패에 대한 메시지를 관찰하면, 클러스터의 Kubernetes 노드에서 다음 명령을 실행해 얻은 아무 파드의 cgroup 경로와 함께 GitHub issue를 제출해 주세요: sudo crictl inspectp -o=json $POD_ID | grep cgroup.

$ kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE     IP             NODE          NOMINATED NODE   READINESS GATES
mediabot   1/1     Running   0          54m     10.244.1.237   kind-worker   <none>           <none>
nginx      1/1     Running   0          3h25m   10.244.1.246   kind-worker   <none>           <none>

$ kubectl exec -n kube-system cilium-rt2jh -- cilium-dbg monitor -v -t trace-sock
CPU 11: [pre-xlate-fwd] cgroup_id: 479586 sock_cookie: 7123674, dst [10.96.128.44]:80 tcp
CPU 11: [post-xlate-fwd] cgroup_id: 479586 sock_cookie: 7123674, dst [10.244.1.246]:80 tcp
CPU 11: [pre-xlate-rev] cgroup_id: 479586 sock_cookie: 7123674, dst [10.244.1.246]:80 tcp
CPU 11: [post-xlate-rev] cgroup_id: 479586 sock_cookie: 7123674, dst [10.96.128.44]:80 tcp

출력된 cgroup id 메타데이터로 클라이언트 파드를 식별할 수 있어요. cgroup id에 해당하는 파드 cgroup path에는 그 UUID가 있어요. socket cookie는 Linux 커널에서 할당된 고유 소켓 식별자예요. socket cookie 메타데이터를 사용해 한 소켓의 모든 추적 이벤트를 식별할 수 있어요.

$ kubectl get pods -o custom-columns=PodName:.metadata.name,PodUID:.metadata.uid
PodName    PodUID
mediabot   b620703c-c446-49c7-84c8-e23f4ba5626b
nginx      73b9938b-7e4b-4cbd-8c4c-67d4f253ccf4

$ kubectl exec -n kube-system cilium-rt2jh -- find /run/cilium/cgroupv2/ -inum 479586
Defaulted container "cilium-agent" out of: cilium-agent, mount-cgroup (init), apply-sysctl-overwrites (init), clean-cilium-state (init)
/run/cilium/cgroupv2/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-besteffort.slice/kubelet-kubepods-besteffort-podb620703c_c446_49c7_84c8_e23f4ba5626b.slice/cri-containerd-4e7fc71c8bef8c05c9fb76d93a186736fca266e668722e1239fe64503b3e80d3.scope

트러블슈팅 (Troubleshooting)

BPF cgroup 프로그램 부착 검증 (Validate BPF cgroup programs attachment)

Cilium은 소켓 기반 로드 밸런싱(일명 host-reachable 서비스)을 활성화하기 위해 BPF cgroup 프로그램을 부착해요. clusterIP 서비스에 대한 연결 문제가 보이면 프로그램이 호스트 cgroup root에 부착됐는지 확인하세요. 기본 cgroup root는 /run/cilium/cgroupv2로 설정돼요. Cilium agent 파드와 그 파드가 실행 중인 기본 kubernetes 노드에서 다음 명령을 실행하세요. 클러스터의 컨테이너 런타임이 cgroup 네임스페이스 모드로 실행 중이면 Cilium agent 파드는 BPF cgroup 프로그램을 virtualized cgroup root에 부착할 수 있어요. 그런 경우 Cilium kube-proxy replacement 기반 로드 밸런싱이 효과적이지 않아 연결 문제로 이어질 수 있어요. 자세한 내용은 수정 사항 Pull Request을 확인하세요.

$ mount | grep cgroup2
none on /run/cilium/cgroupv2 type cgroup2 (rw,relatime)

$ bpftool cgroup tree /run/cilium/cgroupv2/
CgroupPath
ID       AttachType      AttachFlags     Name
/run/cilium/cgroupv2
10613    device          multi
48497    connect4
48493    connect6
48499    sendmsg4
48495    sendmsg6
48500    recvmsg4
48496    recvmsg6
48498    getpeername4
48494    getpeername6

알려진 문제 (Known Issues)

하나의 서비스 엔드포인트가 여러 VIP로 접근될 때의 연결 충돌 (Connection Collisions When a Service Endpoint Is Accessed via Multiple VIPs)

주어진 백엔드 엔드포인트가 여러 서비스(즉, 다른 VIP 또는 NodePort)를 통해 도달 가능하면, 클라이언트가 다른 VIP 또는 NodePort로 가는 새 연결이 다른 VIP 또는 NodePort로 가는 연결의 기존 커넥션 트래킹 상태를 재사용할 수 있어요. 이는 클라이언트가 같은 소스 포트를 선택하면 발생할 수 있어요. 그런 경우 연결이 드롭될 수 있어요.

다음 시나리오가 이 문제에 취약해요:

  • DSR 사용: 클러스터 외부에서 실행되는 클라이언트가 중간 K8s 노드(들)를 통해 CLIENT_IP:SRC_PORT -> LB1_IP:LB1_PORT와 CLIENT_IP:SRC_PORT -> LB2_IP:LB2_PORT 요청을 보내요. 중간 노드는 각 요청에 대해 BACKEND_IP:BACKEND_PORT를 선택하고 이를 백엔드 엔드포인트로 전달해요. 각 요청은 CLIENT_IP:SRC_PORT -> BACKEND_IP:BACKEND_PORT로 동일하게 보이므로 백엔드가 이를 구분할 수 없어요.
  • DSR 유무와 무관하게: 클러스터 외부에서 실행되는 클라이언트가 선택된 백엔드 엔드포인트를 실행하는 K8s 노드에 CLIENT_IP:SRC_PORT -> LB1_IP:LB1_PORT와 CLIENT_IP:SRC_PORT -> LB2_IP:LB2_PORT 요청을 보내요. 다시 각 요청은 CLIENT_IP:SRC_PORT -> BACKEND_IP:BACKEND_PORT로 동일하게 보여요.
  • Socket LB 없이: Pod에서 실행되는 클라이언트가 CLIENT_IP:SRC_PORT -> LB1_IP:LB1_PORT와 CLIENT_IP:SRC_PORT -> LB2_IP:LB2_PORT 요청을 보내요. 패킷별 로드밸런서가 각 요청을 DNAT해서 백엔드로 보내고, 결과는 CLIENT_IP:SRC_PORT -> BACKEND_IP:BACKEND_PORT예요.

따라서 백엔드 엔드포인트를 여러 VIP로 노출하지 않는 것이 강력히 권장돼요 GitHub issue 11810 GitHub issue 18632.

역방향 SK 맵 고갈로 인한 소켓 종료 불안정성 (Socket Termination Unreliable Due to Reverse SK Map Exhaustion)

socket-LB가 활성화되면 Cilium은 삭제된 서비스 백엔드에 연결된 애플리케이션 소켓을 강제로 종료해서, 애플리케이션이 활성 백엔드로 다시 로드 밸런싱될 수 있게 해요 (Limitations 참고). 소켓을 종료하기 전에 Cilium은 cilium_lb4_reverse_sk와 cilium_lb6_reverse_sk BPF 맵에서 소켓의 쿠키와 목적지를 조회해 소켓이 socket-LB로 실제로 로드 밸런싱됐는지 검증해요. 이는 관련 없는 소켓이 종료되는 것을 방지해요. 이 맵들이 가득 차면 LRU 퇴거가 활성 연결에 속한 항목을 제거할 수 있고, 그 연결이 검증에 실패해 소켓 종료 중 건너뛰어질 수 있어요.

소켓이 닫힐 때 이 맵들에서 항목을 제거하는 정리 메커니즘이 있지만, 정리가 올바르게 동작하지 않는 경우가 있어요: 연결되지 않은 UDP 소켓(즉 create → sendto() → close 패턴에서 connect()를 호출하지 않는 경우)과 abort로 종료된 연결된 UDP 소켓이에요.

이런 경우를 트리거하는 워크로드가 있는 노드에서 맵이 점차 채워져 노드를 재시작할 때까지 소켓 종료가 불안정해질 수 있어요. 문제가 된다면 socketLB.hostNamespaceOnly=true를 설정해 파드 네임스페이스에서 socket-LB를 비활성화하는 것을 고려하세요. 자세한 내용은 GitHub issue 42649와 GitHub issue 28820을 참고하세요.

이 맵들을 BPF socket storage로 교체하면(GitHub issue 42658) 이 문제가 해결될 거예요.

서비스 백엔드 종료 중 연결 드롭 (Connection drops during service backend termination)

Kubernetes 환경에서 Service 백엔드는 보통 Pod 객체에 해당하는 EndpointSlice 객체로 표현돼요. Pod가 삭제되면(HPA scale-down, 롤링 업데이트 등) 두 가지 작업이 동시에 트리거돼요:

  • EndpointSlice 엔드포인트가 Terminating으로 업데이트되어 CNI가 그쪽으로 트래픽을 보내지 않도록 지시해요.
  • Kubelet이 pod의 컨테이너에 SIGTERM을 보내요.

그 때문에 애플리케이션이 Cilium이 Pod로 트래픽을 보내는 것을 멈추기 전에 연결을 거부하기 시작할 수 있어요. 이는 연결 드롭과 클라이언트 오류로 이어질 수 있어요. Kubernetes 1.36 기준으로 Kubelet은 아키텍처 제한으로 EndpointSlice 업데이트와 조정할 수 없어요. 동시에 애플리케이션 측 작업 방법을 구현하는 것도 이상적이지 않아요. 예를 들어 웹 애플리케이션 프레임워크는 의미론적으로 애플리케이션이 Kubernetes 환경에서 실행 중임을 알지 못해야 하므로, TERM 신호를 받으면 즉시 연결을 거부해야 합니다.

Pod spec에서 preStop을 정의해 이를 완화할 수 있어요. 다음 예제는 TERM 신호의 애플리케이션 컨테이너 전달을 2초 지연시켜 Cilium이 EndpointSlice 상태 변경에 대응할 더 많은 시간을 줘요:

lifecycle:
  preStop: # Kubernetes 1.29+
    sleep:
      seconds: 2

필요한 지연은 종료되는 Service의 유형에 따라 달라져요. LoadBalancer Service의 경우 외부 로드 밸런서가 노드로 트래픽 보내기를 멈출 충분한 시간을 주기 위해 더 높은 값(예: 10초)이 필요할 수 있어요. ClusterIP Service의 경우 더 낮은 값(예: 1초)이 충분할 수 있어요. 애플리케이션에 가장 좋은 설정을 찾으려면 서로 다른 값으로 실험해 보세요.

여기서 더 읽을 수 있어요:

Remember preStop은 terminationGracePeriodSeconds보다 낮은 숫자여야 해요. 그 시간이 바로 이 유예 기간 안에서 계산되기 때문이에요. 애플리케이션 요구 사항에 맞게 조정하세요. 그렇지 않으면 애플리케이션이 graceful shutdown을 처리할 시간을 너무 적게 줄 수 있어요.

제한 사항 (Limitations)

  • Cilium의 eBPF kube-proxy replacement는 서비스 변환을 구현하기 위해 eBPF cgroup hook을 사용하는 socket-LB 기능에 의존해요. libceph 배포와 함께 사용하려면 현재 eBPF에서 getpeername(2) hook 주소 변환 지원이 필요해요.

  • socket-LB를 사용하면서 NFS와 SMB 마운트를 Service 클러스터 IP에 마운트하면 깨질 수 있어요. 이 문제는 Longhorn, Portworx, Robin에 영향을 주는 것으로 알려져 있지만, 이 패턴을 사용해 ReadWriteMany 볼륨을 구현하는 다른 스토리지 시스템에도 영향을 줄 수 있어요. 이 문제를 피하려면 다음 커밋들이 기본 커널의 일부인지 확인하세요:

    • 0bdf399342c5 ("net: Avoid address overwrite in kernel_connect")
    • 86a7e0b69bd5 ("net: prevent rewrite of msg_name in sock_sendmsg()")
    • 01b2885d9415 ("net: Save and restore msg_namelen in sock_sendmsg")
    • cedc019b9f26 ("smb: use kernel_connect() and kernel_bind()") (SMB only)

    이 패치들은 모든 안정 커널과 일부 배포판 특유 커널에 백포트됐어요:

    • Ubuntu: 5.4.0-187-generic, 5.15.0-113-generic, 6.5.0-41-generic 이상.
    • RHEL 8: 4.18.0-553.8.1.el8_10.x86_64 이상 (RHEL 8.10+).
    • RHEL 9: kernel-5.14.0-427.31.1.el9_4 이상 (RHEL 9.4+).

    더 자세한 논의는 GitHub issue 21541을 참고하세요.

  • Cilium의 DSR NodePort 모드는 현재 TCP Fast Open(TFO)이 활성화된 환경에서 잘 동작하지 않아요. 이 상황에서는 snat 모드로 전환하는 것이 권장돼요.

  • Cilium의 eBPF kube-proxy replacement는 몇 가지 기본적인 경우를 제외하고 SCTP 전송 프로토콜을 지원하지 않아요. 자세한 내용은 SCTP support (beta)를 참고하세요. 현재 서비스의 전송으로는 TCP와 UDP만 완전히 지원돼요.

  • Cilium의 eBPF kube-proxy replacement는 구성된 NodePort 범위와 겹치는 Pod의 hostPort 포트 구성을 허용하지 않아요. 그런 경우 hostPort 설정은 무시되고 Cilium agent 로그에 경고가 출력돼요. 마찬가지로 호스트 네임스페이스에서 hostIP를 루프백 주소에 명시적으로 바인딩하는 것은 현재 지원되지 않으며 Cilium agent 로그에 경고를 기록해요.

  • multi-device 환경의 이웃 탐색은 런타임 디바이스 감지와 함께 동작하지 않아서, 이웃 탐색의 대상 디바이스가 디바이스 변경을 따르지 않아요.

  • socket-LB 기능이 활성화되면 서비스로 (연결된) UDP 및 TCP 트래픽을 보내는 파드는 백엔드가 삭제된 후에도 계속 그 서비스 백엔드로 트래픽을 보낼 수 있어요. Cilium agent는 삭제된 백엔드에 연결된 애플리케이션 소켓을 강제로 종료해 이런 시나리오를 처리하며, 그래서 애플리케이션이 활성 백엔드로 로드 밸런싱될 수 있어요. 이 기능은 다음 커널 구성이 활성화되어야 해요: CONFIG_INET_DIAG, CONFIG_INET_UDP_DIAG, CONFIG_INET_DIAG_DESTROY. lb-sock-terminate-all-protos가 활성화되면 기능이 추가로 커널 구성 CONFIG_INET_TCP_DIAG를 요구해요.

  • BPF 기반 NodePort를 사용할 때는 Cilium의 BPF 기반 마스커레이딩이 iptables보다 권장돼요. 그렇지 않으면 BPF와 iptables SNAT 사이에 포트 충돌 위험이 있어 NodePort 연결이 드롭될 수 있어요 GitHub issue 23604.

추가 자료 (Further Readings)

다음 발표들이 eBPF에서 kube-proxy replacement의 내부 동작을 아주 자세히 설명해요:

  • "Liberating Kubernetes from kube-proxy and iptables" (KubeCon North America 2019, slides, video)
  • "Kubernetes service load-balancing at scale with BPF & XDP" (Linux Plumbers 2020, slides, video)
  • "eBPF as a revolutionary technology for the container landscape" (Fosdem 2020, slides, video)
  • "Kernel improvements for Cilium socket LB" (LSF/MM/BPF 2020, slides)

더 알아보기 (Learn more)