Cilium Node IPAM LBによるロードバランシング

Sreake事業部

2024.10.28

はじめに

Sreake事業部でインターンをしている小林です。

本記事では、Cilium v1.16で追加されたCilium Node IPAM LBを検証しました。

Ciliumのロードバランシング方法

CiliumでLoadBalancerタイプのServiceを作成する方法として、BGPとL2 Announcementがあります。

Cilium v1.13でLoadBalancer IP Address Management(LB-IPAM)機能が追加され、CiliumがLoadBalancerタイプのServiceにEXTERNAL-IPを割り当て、そのIPアドレスをBGPでアドバタイズできるようになりました。

さらに、Cilium v1.14でL2 Announcementが追加され、LB-IPAMが仮想IPアドレス(VIP)をETERNAL-IPとして割り当て、オフィスや家のネットワークからアクセスできるようになりました。

L2 Announcementについては、以前書いたCilium L2 Announcement を使ってみるをご覧ください。

しかし、BGPでアドバタイズするにはBGPルータが別途必要であったり、L2 Announcementでもネットワークが自分の管理下にあることが求められます。以前のブログを見てもらえばわかりますが、L2 Announcementでは、どのノードのIPアドレスとも異なる”172.18.0.100”という新しいIPアドレスをEXTERNAL-IPとしてServiceを作成しており、”172.18.0.100”を割り当てる権限がない場合には使うことができません。

Node IPAM LB

そこで、VPCリソースを作成したり変更したりする権限がなく、ネットワークを自由に制御できないといった場合にもLoadBalancer Serviceを作成する方法として、Node IPAM LBがCilium v1.16で追加されました[1]。

Node IPAM LB は、k3sのServiceLB[2]にヒントを得た機能で、ノードのIPアドレスを直接アドバタイズします。そのため、ネットワークを管理する権限がなく、BGPもL2 Announcementも使用できないといった場合でも、LoadBalancerタイプのServiceを作成することができます。

検証

実際にKubernetesクラスタを作成して、Node IPAM LBを試してみます。今回は、kube-proxyを使っていますが、without kube-proxyでもNode IPAM LBは動作します。

検証では、PodmanとKindを使用しています。

環境構築

今回は、マスターノード1台、ワーカーノード3台の4台構成でデフォルトのCNIを無効にしてクラスタを作成します。

<!-- wp:paragraph -->
<p>$ cat &lt;&lt;EOF | kind create cluster --config -<br>kind: Cluster<br>apiVersion: kind.x-k8s.io/v1alpha4<br>nodes:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>role: control-plane</li><li>role: worker</li><li>role: worker</li><li>role: worker<br>networking:<br>disableDefaultCNI: true<br>EOF</li></ul>
<!-- /wp:list -->

Helmを使って、Ciliumをインストールします。ここで、nodeIPAM.enabledをtrueにする必要があります。

$ helm install cilium cilium/cilium --version 1.16.1 \
  --namespace kube-system \
  --set nodeIPAM.enabled=true

Ciliumが正常にインストールされると、全てのPodが動いていることが確認できます。

$ kubectl get pods -A
NAMESPACE            NAME                                         READY   STATUS    RESTARTS   AGE
kube-system          cilium-4m4m4                                 1/1     Running   0          79s
kube-system          cilium-67cvq                                 1/1     Running   0          79s
kube-system          cilium-8h8bk                                 1/1     Running   0          79s
kube-system          cilium-envoy-8fdq8                           1/1     Running   0          79s
kube-system          cilium-envoy-hm5k4                           1/1     Running   0          79s
kube-system          cilium-envoy-qcd25                           1/1     Running   0          79s
kube-system          cilium-envoy-vf9lt                           1/1     Running   0          79s
kube-system          cilium-operator-64767f6566-bfmkf             1/1     Running   0          79s
kube-system          cilium-operator-64767f6566-zjfbh             1/1     Running   0          79s
kube-system          cilium-r56tg                                 1/1     Running   0          79s
kube-system          coredns-7db6d8ff4d-8djpn                     1/1     Running   0          2m2s
kube-system          coredns-7db6d8ff4d-d66hg                     1/1     Running   0          2m2s
kube-system          etcd-kind-control-plane                      1/1     Running   0          2m18s
kube-system          kube-apiserver-kind-control-plane            1/1     Running   0          2m18s
kube-system          kube-controller-manager-kind-control-plane   1/1     Running   0          2m18s
kube-system          kube-proxy-fjnmr                             1/1     Running   0          2m2s
kube-system          kube-proxy-jppnh                             1/1     Running   0          119s
kube-system          kube-proxy-lvmns                             1/1     Running   0          119s
kube-system          kube-proxy-zwq7z                             1/1     Running   0          119s
kube-system          kube-scheduler-kind-control-plane            1/1     Running   0          2m18s
local-path-storage   local-path-provisioner-988d74bc-n68nk        1/1     Running   0          2m2s

nginxのDeploymentを作成します。Deploymentでは、nginxのPodを2台動作するように設定しています。この後、.spec.externalTrafficPolicy を検証するためにここでは3台のノードで2つのPodを動かしています。

$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx-pod
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx-pod
    spec:
      containers:
      - name: nginx-container
        image: nginx:1.27.0
        ports:
        - containerPort: 80
EOF

kubectlでdeploymentとpodを確認すると、2つのPodが動いていることがわかります。

$ kubectl get deployment nginx-deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   2/2     2            2           67s

$ kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,IP:.status.podIP,NODE:.spec.nodeName
NAME                                STATUS    IP           NODE
nginx-deployment-7b9576f465-s78n8   Running   10.0.2.130   kind-worker
nginx-deployment-7b9576f465-zr7xc   Running   10.0.3.100   kind-worker2    

Serviceの作成

LoadBalancerタイプのServiceを作成します。Node IPAM LBを使うには、spec.loadBalancerClassio.cilium/node を指定する必要があります。

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-pod
  type: LoadBalancer
  loadBalancerClass: io.cilium/node
EOF  

serviceを確認すると、nginx-serviceのEXTERNAL-IPが 10.89.0.6,10.89.0.7,10.89.0.9 になっていることがわかります。

$ kubectl get service nginx-service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP                     PORT(S)        AGE
nginx-service   LoadBalancer   10.96.174.161   10.89.0.6,10.89.0.7,10.89.0.9   80:31817/TCP   13s 

その後、ノードのIPアドレスを見てみると、ワーカーノードのIPアドレスとnginx-serviceのEXTERNAL-IPが一致することがわかります。

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,IP:.status.addresses[0].address
NAME                 IP
kind-control-plane   10.89.0.8
kind-worker          10.89.0.6
kind-worker2         10.89.0.7
kind-worker3         10.89.0.9

Podmanでテスト用のコンテナ(test-container)をkindネットワークに作成して、クラスタ外からアクセスできるか確認します。test-containerを作成した後のネットワーク構成は、下の図のようになります。

serviceのEXTERNAL-IPに対して、クラスタ外からもアクセスできていることが確認できます。

$ podman run -it --name test-container --net kind ubuntu:24.04 /bin/bash
root@ae95151e571b:/# curl 10.89.0.6 -o /dev/null -w '%{http_code}\n' -s
200

externalTrafficPolicy

Serviceの.spec.externalTrafficPolicyを設定すると、Kubernetesの機能でトラフィックをどのPodにルーティングするのか制御することができます[3,4]。

.spec.externalTrafficPolicyのデフォルト値はClusterになっており、トラフィックがノードに到達した後に、異なるノードのPodにも負荷分散します。別のノードへのホップが発生する可能性がありますが、トラフィックは全体に分散します。

一方で、.spec.externalTrafficPolicyLocalにすると、トラフィックが到達したノード内のPodにのみ負荷分散します。別のノードへのホップはなくなりますが、特定のPodにトラフィックが集中してしまう可能性があります。

先ほどは、.spec.externalTrafficPolicyを指定していなかったため、Cluster が指定されており、nginx-serviceのEXTERNAL-IPでnginxのPodが動作していない10.89.0.9 でも待ち受けていました。実際には、IPアドレスが10.89.0.9 のkind-worker3にトラフィックが到達した後に、iptablesによって、nginxのPodが動作しているkind-workerかkind-worker2に転送されます。

そこで、.spec.externalTrafficPolicyLocalにしてみます

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-pod
  type: LoadBalancer
  loadBalancerClass: io.cilium/node
  externalTrafficPolicy: Local
EOF

すると、nginxのPodが動作していない10.89.0.9 はEXTERNAL-IPに含まれなくなり、トラフィックが10.89.0.6,10.89.0.7に到達した後に、ノードを跨いだ負荷分散を行わなくなります。

$ kubectl get service nginx-service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP           PORT(S)        AGE
nginx-service   LoadBalancer   10.96.174.161   10.89.0.6,10.89.0.7   80:31817/TCP   14m

ノードの制限

serviceにio.cilium.nodeipam/match-node-labels のアノテーションを付けることでトラフィックを受け付けるノードを制限することができます。

ここでは、kind-worker2でのみトラフィックを受け付けるようにしてみます。

.metadata.annotationsio.cilium.nodeipam/match-node-labels: "kubernetes.io/hostname=kind-worker2" を追加しています。

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  annotations:
    io.cilium.nodeipam/match-node-labels: "kubernetes.io/hostname=kind-worker2"
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-pod
  type: LoadBalancer
  loadBalancerClass: io.cilium/node
EOF

結果を確認すると、EXTERNAL-IPが10.89.0.7となっており、kind-worker2でのみトラフィックを受け付けていることがわかります。ただ、.spec.externalTrafficPolicyCluster になっているため、kind-worker2に到達した後に、kind-workerのPodにルーティングされる可能性はあります。

$ kubectl get svc nginx-service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx-service   LoadBalancer   10.96.174.161   10.89.0.7     80:31817/TCP   17m

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,IP:.status.addresses[0].address
NAME                 IP
kind-control-plane   10.89.0.8
kind-worker          10.89.0.6
kind-worker2         10.89.0.7
kind-worker3         10.89.0.9

Kubernetesでは、ラベルセレクターとして等価ベースと集合ベースがあり、上記では等価ベースでラベルを記述しましたが、集合ベースで記述することで複数ノードの指定ということも可能です[5]。

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  annotations:
    io.cilium.nodeipam/match-node-labels: "kubernetes.io/hostname in (kind-worker,kind-worker2)"
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx-pod
  type: LoadBalancer
  loadBalancerClass: io.cilium/node
EOF

結果を確認すると、EXTERNAL-IPが10.89.0.6,10.89.0.7となっており、kind-workerとkind-worker2の2つのノードでトラフィックを受け付けていることがわかります。

$ kubectl get service nginx-service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP           PORT(S)        AGE
nginx-service   LoadBalancer   10.96.174.161   10.89.0.6,10.89.0.7   80:31817/TCP   45m

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,IP:.status.addresses[0].address
NAME                 IP
kind-control-plane   10.89.0.8
kind-worker          10.89.0.6
kind-worker2         10.89.0.7
kind-worker3         10.89.0.9

実装

Node IPAM LBの実装は、#30038のPull requestsで確認することができます。

SetupWithManager() を見ると、Service, EndpointSlice, Nodeの状態を監視しておき、これらのリソースに変更があった場合にReconciliation Loop処理が発生します。

このときに行う処理は Reconcile() にあり、getRelevantNodes() で対象となるノードを選択して、getNodeLoadBalancerIngresses() でノードのIPアドレスを取得し、svc.Status.LoadBalancer.Ingressにセットしています。その後、Update() を呼び出してServiceの更新を行うことで、設定が反映されます。

上記の getRelevantNodes() を見ると、アノテーションのio.cilium.nodeipam/match-node-labelsを読み取って、ラベルセレクタをパースし、それを基にノードをフィルタリングしていることが確認できます。ここで、ノードの制限で紹介した、指定された条件に一致するノードのみを対象とする処理を行っています。

また、上記の getNodeLoadBalancerIngresses() を見ると、各ノードのIPアドレス(node.Status.Addressを参照)を取得し、ソートして返していることがわかります。そのため、serviceを確認すると、EXTERNAL-IPはソートされて表示されるようになっています。

今回は、Cilium kube-proxy Replacementを有効にしていないため、kube-proxyが操作するiptablesによって、ノードからPodまでルーティングされます。kube-proxyの実装はkubernetesリポジトリのpkg/proxyにあり、その中でもiptablesの設定はpkg/proxy/iptables/proxier.goで行っています。Serviceに変更があった場合にはsyncProxyRules()を呼び出してiptablesのルールを同期するようになっており、L1110-L1131svc.Status.LoadBalancer.Ingressから宛先IPアドレス、svc.Spec.Ports.Portから宛先ポート番号を取得して”KUBE-EXT-XXX”にジャンプするルールを追加しています。NodePort作成時のiptablesにおけるNATルールには宛先IPアドレスの指定はなく宛先ポート番号はspec.ports.nodePortから取得してiptablesのルールを作成しています。そのため、NodePortでは30000-32767のポート番号でアクセスすることになりますが、LoadBalancerでは任意のポート番号でアクセスすることができます。

Node IPAM LBの処理自体は各ノードのIPアドレスを取得してEXTERNAL-IPにセットするだけなので、理解しやすいプログラムとなっています。

まとめ

Cilium v1.16 時点でLoadBalancerタイプのServiceを作成する方法は3つあり、それぞれの特徴をまとめた表が以下になります。

特徴BGPL2 AnnouncementNode IPAM LB
ユースケースエンタープライズ会社や学内、家のネットワークネットワーク管理権限がない
スケーラビリティ
設定が必要なリソースCiliumBGPPeeringPolicy
CiliumLoadBalancerIPPool
CiliumL2AnnouncementPolicy
CiliumLoadBalancerIPPool
なし
設定の複雑さ
外部依存性BGPルータ
ネットワーク管理権限
ネットワーク管理権限なし
フェイルオーバー

Node IPAM LBではネットワーク管理権限がない場合にもLoadBalancerタイプのServiceを作ることができ、BGPやL2 Announcementに比べて設定も非常に簡単ですが、EXTERNAL-IPはワーカーノードの数に依存するためスケーラビリティは低く、またフェイルオーバー機能もありません。

NodePortと異なる点としては、ワーカーノードの80番や443番といったポートでサービスを待ち受けられることが挙げられます。NodePortでは、デフォルトでマスターノードが30000-32767からポートを選択して割り当てるため、どのポートで待ち受けているか確認してからそのポートにアクセスしなければなりません。一方で、Node IPAM LBでは、 Serviceマニフェストのspec.ports.portで指定したポート番号で待ち受けるため、ポート番号を確認する必要がなく、また、ウェルノウンポートを含む全てのポート番号で待ち受けることができます。

これらの特徴から、基本的にはBGPまたはL2 AnnouncementとLB-IPAMの組み合わせを利用するのがいいですが、ネットワーク管理権限がないけどLoadBalancerタイプのServiceを作成したいといった場合にはCiliumのNode IPAM LBを使ってみるのはどうでしょうか。

参照

[1] Cilium 1.16 Release

https://isovalent.com/blog/post/cilium-1-16/#h-node-ipam-service-lb

[2] K3s ネットワーキングサービス サービスロードバランサー

https://docs.k3s.io/ja/networking/networking-services#サービスロードバランサー

[3] kubernetes Documentation Virtual IPs and Service Proxies

https://kubernetes.io/docs/reference/networking/virtual-ips/#external-traffic-policy

[4] kubernetes Documentation Create an External Load Balancer

https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip

[5] kubernetes Documentation ラベル(Labels)とセレクター(Selectors)

https://kubernetes.io/ja/docs/concepts/overview/working-with-objects/labels/#label-selectors

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ