はじめに
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 <<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.loadBalancerClassでio.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.externalTrafficPolicyをLocal
にすると、トラフィックが到達したノード内の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.externalTrafficPolicyをLocal
にしてみます。
$ 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.annotationsでio.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.externalTrafficPolicyはCluster になっているため、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-L1131でsvc.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つあり、それぞれの特徴をまとめた表が以下になります。
特徴 | BGP | L2 Announcement | Node 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 Releasehttps://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 Proxieshttps://kubernetes.io/docs/reference/networking/virtual-ips/#external-traffic-policy
[4] kubernetes Documentation Create an External Load Balancer [5] kubernetes Documentation ラベル(Labels)とセレクター(Selectors)https://kubernetes.io/ja/docs/concepts/overview/working-with-objects/labels/#label-selectors