はじめに
以下の記事で、オンプレミスのKubernetesクラスタを構築するまでの流れについてまとめを行いました。
ところで、Kubernetesクラスタを構築する際において、自らでCNI Pluginを選定し導入する必要があります。
今回は、CNIとしてセットアップが容易なFlannelを使用しましたが、Kubernetes CNIには他にもCalicoやCiliumなどの選択肢があります。
そこで、本記事ではFlannelだけでなく、CNIにCiliumを実際に導入し機能の比較を行ってみたいと思います。また、簡単に動作原理についても追ってみようと思います。
なお、本記事はKubernetes CNIを体系的にまとめたり比較するような記事ではなく、Flannelを中心に仕組みを調査し、Ciliumとの比較をNetworkPolicyベースで行っている記事になります。Calicoの説明やパフォーマンス比較等は今回のスコープ外です。
動作環境は以下の通りです:
- Kubernetes v1.31
- Flannel v0.27.4
- Cilium v1.18.0
Flannel
動作原理
Flannelは、VXLANというトンネリング技術を利用して、異なるノードに存在するPod間の通信を可能にするオーバーレイネットワークを構築します。
Flannelの主な役割は以下の2点です。
-
各ノードへのPod用IPアドレス範囲の割り当て
- クラスタ内の各ノードに、重複しないサブネット (podCIDR) を割り当てる。
-
ノード間をVXLANトンネルで接続
- 各ノードの物理IPアドレス間で仮想的なトンネルを張り、Podからのパケットをカプセル化して転送する。
図に表すと以下のようになります。
より細かく、動作確認を行いながら仕組みを見ていきます。
-
VXLANトンネルの確認
- flanneld は flannel.1 という名前の仮想ネットワークインターフェースを作成します。これがVXLANトンネルの出入り口です。
$ ip addr show flannel.1
5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 26:1f:d2:c5:65:4e brd ff:ff:ff:ff:ff:ff
inet 10.244.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
-
ルーティングテーブルの確認
- ルーティングテーブルを見ると、自分以外のノードのPod CIDR(例: 10.244.1.0/24)宛の通信は、すべて先ほどの flannel.1 デバイスに送られるように設定されていることがわかります。
$ ip route
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink # master
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 # worker-1
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink # worker-2
10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink # worker-3
-
VXLAN VTEPの対応確認 (宛先物理IPの特定)
- パケットが flannel.1 に到達した後、どの物理ノードにカプセル化して送ればよいかを判断する必要があります。その対応表が
bridge fdb(Forwarding Database) に記録されています。 - 以下の出力から、例えば
worker-2のflannel.1インターフェースのMACアドレス (5e:83:73:4b:98:06) 宛の通信は、物理IP192.168.80.105に送ればよい、というマッピングがわかります。
- パケットが flannel.1 に到達した後、どの物理ノードにカプセル化して送ればよいかを判断する必要があります。その対応表が
$ bridge fdb show dev flannel.1
5e:83:73:4b:98:06 dst 192.168.80.105 self permanent # worker-2
5a:73:ea:c3:4f:e3 dst 192.168.80.101 self permanent # master
8e:92:8d:9c:2e:f4 dst 192.168.80.107 self permanent # worker-3
-
Pod間通信のキャプチャ
- 実際に、tcpdumpを使って、worker-1 (10.244.1.0) のPodからworker-2 (10.244.2.0) のPodへ通信した際のパケットをキャプチャしてみます。
- キャプチャ結果から、Pod間のTCPパケット (10.244.1.0 > 10.244.2.0) が、ノード間のUDPパケット (192.168.80.103 > 192.168.80.105 のポート 8472) にカプセル化されて送られていることがわかります。
$ sudo tcpdump -i enp0s8 -n "dst host 192.168.80.105 and udp port 8472"
06:17:36.164107 IP 192.168.80.103.55383 > 192.168.80.105.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.244.1.0.43566 > 10.244.2.0.80: Flags [S], ...
06:17:36.166066 IP 192.168.80.103.55383 > 192.168.80.105.8472: OTV, ...
IP 10.244.1.0.43566 > 10.244.2.0.80: Flags [P.], ... HTTP: GET / HTTP/1.1
...
つまり、あるPodから別のノードのPodへ通信が発生すると、まず送信元ノードのカーネルがそのパケットを丸ごとカプセル化し、同時に元のPod IPヘッダーは内側に隠蔽され、外側にはノードの物理IPを宛先とする新しいヘッダーが付与されます。
このように、カプセル化されたパケットが物理ネットワークを介して宛先ノードに到着すると、今度は受信側ノードのカーネルがカプセル化を解除(デカプセル化)し、中の元のパケットを取り出して宛先のPodに届けます。
このようにして、物理ネットワークからは直接見えない、仮想的なPodネットワーク上(オーバーレイネットワーク)での通信が実現されています。
Cilium
Flannelからの移行
ここまでで、Flannelに関する説明を行いました。Ciliumと比較を行うために、既存のFlannel環境からCiliumへ移行したいと思います。
手順は以下の通りです。
Flannelの削除
まず、インストールしていたFlannelのDaemonSetや関連リソースを削除します。
kubectl delete -f https://raw.githubusercontent.com/flannel-io/flannel/v0.27.4/Documentation/kube-flannel.yml
次に、各ワーカーノードで、Flannelが作成したネットワークインターフェースや設定ファイルを削除します。
# 各ワーカーノードで実行
sudo rm -rf /etc/cni/net.d/* sudo ip link delete cni0 2>/dev/null
sudo ip link delete flannel.1 2>/dev/null
sudo systemctl restart kubelet
Ciliumインストール
CLIインストール(Linux)
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インストール
cilium install
cilium upgrade \
--set routingMode=tunnel \
--set kubeProxyReplacement=false \
--set bpf.masquerade=false
インストールの確認(CNIにCiliumが設定されていることを確認します)
$ cilium status --wait
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: OK
\__/¯¯\__/ Hubble Relay: disabled
\__/ ClusterMesh: disabled
DaemonSet cilium Desired: 4, Ready: 4/4, Available: 4/4
...
Cluster Pods: 4/4 managed by Cilium
...
NetworkPolicyの比較
目的
NetworkPolicyを作成し、FlannelとCiliumで違いを確認します。目的については、以下を確認することをゴールとします。
- FlannelはNetworkPolicyを作成してもそれが機能しないこと。
- Ciliumは機能し、かつL7でも動作することを確認すること。
Flannel
まず、通信のテストに使う簡単なクライアントとサーバーのPodをデプロイします。
apiVersion: v1
kind: Pod
metadata:
name: backend-pod
labels:
app: backend
spec:
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: client-pod
labels:
app: client
spec:
containers:
- name: busybox-container
image: busybox:1.28
command: ['sh', '-c', 'sleep 3600']
次に、clientからbackendに通信できることを確認します。
NetworkPolicyは設定しておらず、CNIが正しく設定されていれば問題なくNginxのウェルカムページが表示されるはずです。これは、FlannelによりPod間の基本的な通信を許可していることを示します。
BACKEND_IP=$(kubectl get pod backend-pod -o jsonpath='{.status.podIP}')
# client-podからcurlを実行
kubectl exec client-pod -- curl --connect-timeout 2 $BACKEND_IP
次に、backend-podへの通信を拒否するネットワークポリシーを作成します。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
これを適用し、再度テストしてみます。
kubectl exec client-pod -- wget -q -O - --timeout=2 $BACKEND_IP
Flannelの場合、このポリシーを適用しても通信は成功したままであることが確認されます。 (welcome to nginxが引き続き出る状態) これは、FlannelがNetworkPolicyリソースを解釈・実行できないためです。
Cilium
同じようにclientとbackendをデプロイし、NetworkPolicyもデプロイすると、通信が失敗することが確認できます。
$ kubectl exec client-pod -- wget -q -O - --timeout=2 $BACKEND_IP
wget: download timed out
command terminated with exit code 1
これはCiliumが先ほど適用したdefault-deny-ingressポリシーを正しく解釈し、通信をブロックしているためです。
次に、CiliumがL7のNetworkPolicyをサポートしていることを確認するために、以下を作成し反映します。
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l7-policy-example"
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: client
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/allowed"
/allowedというパスをbackendに追加します。
kubectl exec -it backend-pod -- /bin/sh -c 'echo "200" > /usr/share/nginx/html/allowed'
/allowed へのアクセスは200となることが確認できます。
$ kubectl exec client-pod -- wget -q -O - --timeout=2 http://$BACKEND_IP/allowed
200
しかし、/deniedへのアクセスは403になる(NetworkPolicyにより破棄)と、異なる挙動をしていることがわかります。これは、CiliumがL7レベルでアクセスを遮断しているということを示しています。
$ kubectl exec client-pod -- wget -q -O - --timeout=2 http://$BACKEND_IP/denied
wget: server returned error: HTTP/1.1 403 Forbidden
command terminated with exit code 1
まとめ
本記事では、KubernetesのCNIであるFlannelとCiliumを比較し、その動作原理とNetworkPolicyの挙動の違いを検証しました。
FlannelはVXLANによるシンプルなオーバーレイネットワークを構築することを確認しました。一方、Ciliumも同様に仕組みですが、eBPFを用いることでiptablesを介さずにカーネル内で直接パケット処理を高速に実行し、高いパフォーマンスと豊富な機能を実現している点が違いとなります。
また、NetworkPolicyについて、Flannelはポリシーを解釈する機能を持たないため、ルールを適用しても通信はブロックされないことを確認しました。一方でCiliumは、eBPFの能力を活かしてL3/L4の通信を正しくブロックし、さらにHTTPパスのようなL7情報に基づいた高度な通信制御も可能であることを確認できました。
CNIは普段あまり意識することはないのですが、このように原理を調べたり違いを比較してみるのは面白かったです。
Calicoについても、また別のタイミングで検証できればと思います。
