Kubernetes クラスターの CNI Migration

CNI Migration と題しまして、今回は、FlannelがインストールされているクラスターのCNIをCiliumに移行させてみたいと思います。思ったより大したことがなかったのでエントリにするのは躊躇われたのだけれども、他にアドベントカレンダーに書くこともなかったのでまあいいか。

ちなみにこの記事は、[Kubernetes Advent Calender](https://qiita.com/advent-calendar/2022/kubernetes) 21 日目の記事となります。


## :rotating_light: 注意

- クラウドのKubernetesクラスターを使ってるなど、クラスターの移行が簡単な場合は、クラスターの移行によるCNIの変更がオススメです。
- [K8s@Home#2](https://k8shome.connpass.com/event/266118/) にて同内容のLTをしました。
    - [YouTube](https://youtu.be/GQu3rBz5lLE?t=4856)
    - [スライド](https://speakerdeck.com/yuanying/kubernetes-cni-migration)

## :earth_asia: 環境

テストした環境は以下の通り。

- Kubernetes: v1.24
- Flannel
    - Version
        - flannelcni/flannel: v0.20.2
        - flannelcni/flannel-cni-plugin: v1.2.0
    - Tunnel: VXLAN
    - IPAM: Kubernetes
    - Pod CIDR: `10.243.0.0/16`
- Cilium
    - Version
        - Chart: 1.12.2
        - CLI: v0.12.10
    - Tunnel: geneve
    - IPAM: cluster-pool
    - Pod CIDR: `172.20.0.0/16`

## :spider_web: Flannel to Cilium

### 手順

1. Cilium Install
2. Flannel Uninstall

### 移行

実のところ、こんな "CNI Migration" というタイトルでこのエントリを書いていたりはするのですが、CNIの、特にCiliumへの移行は大体の場合においてそんな難しい話ではなかったりします。というのも単に `cilium install` を実行するだけで古いCNIからCiliumへの移行が大体完了するからです。

例えば事前に、以下のようなFlannelによりIPアドレスがアサインされたPodがあるとします。

```bash
$ k get pod -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP           NODE                                       NOMINATED NODE   READINESS GATES
testapp-0   1/1     Running   0          3m32s   10.243.4.3   node-01.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-1   1/1     Running   0          3m1s    10.243.6.4   node-02.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-2   1/1     Running   0          2m34s   10.243.0.4   node-03.staging.vms.svc.fraction.cluster   <none>           <none>
```

そこに、以下のような設定で Cilium をインストールします。

```bash
$ cat <<EOF > values.yaml
tunnel: geneve

ipam:
  mode: cluster-pool
  operator:
    clusterPoolIPv4PodCIDRList:
    - "172.20.0.0/16"

hubble:
  enabled: false
EOF

$ cilium install --helm-values values.yaml
ℹ️  Using Ciliumversion  1.12.2
🔮 Auto-detected cluster name: unstable
🔮 Auto-detected datapath mode: tunnel
🔮 Auto-detected kube-proxy has been installed
ℹ️  helm temlate --namespace kube-system ciliumr cilium/cilium --version 1.12.2 --set cluster.id=0,cluster.name=unstable,encryption.nodeEncryption=false,hubble.enabled=false,ipam.mode=cluster-pool,ipam.operator.clusterPoolIPv4PodCIDRList[0]=172.20.0.0/16,kubeProxyReplacement=disabled,operator.replicas=1,serviceAccounts.cilium.name=cilium,serviceAccounts.operator.name=cilium-operator,tunnel=geneve
ℹ️  Storng hvelm values file in kube-system/cilium-cli-helm-values Secret
🔑 Created CA in secret cilium-ca
🔑 Generating certificates for Hubble...
🚀 Creating Service accounts...
🚀 Creating Cluster roles...
🚀 Creating ConfigMap for Cilium version 1.12.2...
🚀 Creating Agent DaemonSet...
🚀 Creating Operator Deployment...
⌛ Waiting for Cilium to be installed and ready...
♻️  Restrting unmanaged  pods...
♻️  Restrtned unmanaged pod default/testapp-0
♻️  Restrtned unmanaged pod default/testapp-1
♻️  Restarted unmanagedpod  default/testapp-2
...
(snip)
...
♻️  Restarted unmanaged podkube-system/trigger-76c66678c4-8funbr
 Cilium was su78ccessfully installed! Run 'cilium status' to view installation health
```

そうすると、なんと不思議、コマンドによっていい感じにCiliumがインストールされて、Flannelによってネットワークが設定されていたPodが一旦削除されて再起動されます。

コマンド実行後に再起動された Pod の状態を確認すると、CiliumによってIPアドレスが付与されていることが確認されます。

```bash
$ k get pod -o wide
NAME        READY   STATUS    RESTARTS   AGE   IP             NODE                                       NOMINATED NODE   READINESS GATES
testapp-0   1/1     Running   0          12h   172.20.3.216   node-03.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-1   1/1     Running   0          19s   172.20.2.7     node-02.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-2   1/1     Running   0          18s   172.20.1.189   node-01.staging.vms.svc.fraction.cluster   <none>           <none>
```

いや、待て、まだFlannelがインストールされているぞ？それはどうなっているんだ？と思う方がいるかもしれません。しかしCiliumはそもそもこのようなCNIの移行を最初から考慮しているのか、ノードの `/etc/cni/net.d` を確認すると Flannel の設定ファイルが無効化されていることがわかります。

```bash
$ ssh ubuntu@node-01.staging.vms.svc.fraction.cluster sudo ls /etc/cni/net.d
05-cilium.conf
10-flannel.conflist.cilium_bak
```

素晴らしい。

と、いうことで、Ciliumをインストールした後にやることは古いCNIを削除するだけ、ということになります。

```bash
$ kubectl delete ds -n kube-system kube-flannel-ds
daemonset.apps "kube-flannel-ds" deleted
```

### ネットワーク接続性

さて、CNIの移行が簡単にできることはわかったものの、Podが再起動している最中、CNIのMigrationが行われている最中はどうなっているんだろう？というのが気になりますよね！願わくば上品で気品がある感じで移行されているといいですよね。

と、いうわけで途中の過程を見るために、また一度、Flannel CNI がインストールされているクラスターにロールバックしまして、Ciliumを手作業(Helm)でインストールしてみます。(コマンドでインストールすると勝手にPodが再起動されてしまうので)

```bash
$ helm template --namespace kube-system cilium cilium/cilium --version 1.12.2 -f values.yaml | k apply -f -
```

はい、上記のコマンドで Cilium をインストールした状態のクラスタです。PodはまだFlannelが割り当てたIPアドレスを持っていますね。

```bash
$ k get pod -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP           NODE                                       NOMINATED NODE   READINESS GATES
testapp-0   1/1     Running   0          7m54s   10.243.4.3   node-01.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-1   1/1     Running   0          7m23s   10.243.6.4   node-02.staging.vms.svc.fraction.cluster   <none>           <none>
testapp-2   1/1     Running   0          6m56s   10.243.0.4   node-03.staging.vms.svc.fraction.cluster   <none>           <none>
```

しかしこの状態はすでにノードの `/etc/cni/net.d/10-flannel.conflist` は無効化されているため、Flannelは動作しません。この状態で、testapp-0のみ `kubectl delete pod testapp-0` で削除して再起動させてみると、再起動したPodにFlannelの代わりにCiliumが`172.20.0.0/16` のアドレス帯からIPアドレスを割り当ててくれているのがわかります。

```bash
$ k get pod -o wide testapp-0
NAME        READY   STATUS    RESTARTS   AGE   IP             NODE                                       NOMINATED NODE   READINESS GATES
testapp-0   1/1     Running   0          7s    172.20.2.137   node-01.staging.vms.svc.fraction.cluster   <none>           <none>
```

CiliumにIPアドレスを割り当ててもらったPodとFlannelにIPアドレスを割り当ててもらったPod間のネットワーク疎通を確認してみましょう。

```bash
# testapp-0(Cilium:172.20.2.137) -> testapp-1(Flannel:10.243.6.4)
$ k exec -ti testapp-0 -- ping 10.243.6.4
PING 10.243.6.4 (10.243.6.4) 56(84) bytes of data.
64 bytes from 10.243.6.4: icmp_seq=1 ttl=61 time=1.28 ms
64 bytes from 10.243.6.4: icmp_seq=2 ttl=61 time=1.79 ms

# testapp-1(Flannel:10.243.6.4) -> testapp-0(Cilium:172.20.2.137)
$ k exec -ti testapp-1 -- ping 172.20.2.137
PING 172.20.2.137 (172.20.2.137) 56(84) bytes of data.
64 bytes from 172.20.2.137: icmp_seq=1 ttl=62 time=0.662 ms
64 bytes from 172.20.2.137: icmp_seq=2 ttl=62 time=1.21 ms
```

ping がちゃんと通ってることがわかります。この感じだとCiliumをインストールした後、クラスターが壊れないようにPodのローリングアップデートをしながらグレースフルにCNIの移行を進めることができそうですね！

### ネットワーク接続性のための条件 

実は、このように新旧のCNIによりIPアドレスが割り当てられたPod同士が通信できるようになるためにはいくつか条件があります。上記の"環境"セクションで挙げられていた環境は適当な感じで無邪気に決めたテスト条件ではなかったんですね。

以下がその条件となります。

1. IPAM が異なること
2. 割り当てられるネットワークアドレスが異なること
3. ノードがゲートウェイになりそれぞれのPodに接続可能であること
4. VXLAN を使うならば、どちらか一方の CNI に限ること

#### IPAM が異なること

これはまあ、仕方がないですね。FlannelでKubernetesのIPAMを利用しているのにCiliumでもKubernetesのIPAMを利用したら、割り当てるネットワークアドレスが被ってしまいます。

幸運なことに、Ciliumは[KubernetesのIPAM以外にもいくつか別の方式をサポート](https://docs.cilium.io/en/v1.12/concepts/networking/ipam/)しています。

これが Cilium(IPAM: kubernetes)からFlannelへの移行だったらちょっと困ったことになっていましたね。

#### 割り当てられるネットワークアドレスが異なること

これもまあ、仕方がない。ルーティングどうするんだって話ですし、IPアドレスが重複してしまうかもしれないですし。

#### ノードがゲートウェイになりそれぞれのPodに接続可能であること

自分は基本的に、Node -> Pod が NAT なしに疎通可能な CNI しか使ったことがないので良くわからないのだけど、少なくともこれができないと今回の構成では異なるCNI間で接続はできないです。

これもまた幸運なことに Flannel も Cilium もホストのNICをゲートウェイとして別のネットワークと通信するモデルでした。

#### VXLAN を使うならば、どちらか一方の CNI に限ること

これは知らなかったんですが、VXLANってひとつのホストに二つ以上設定することができなかったんですね。

- https://github.com/cilium/cilium/issues/8990

さらに幸運なことに Flannel も Cilium もノード間通信の方式で VXLAN 以外の方式もサポートしているCNIでしたので、今回は Cilium で `geneve` を利用することにしました。

### ネットワーク構成

さて、いくつかの幸運により、Flannel -> Cilium の移行は比較的簡単にグレースフルに行えることができることがわかりました。
ところで一体全体、これらのPodがどうやってやり取りしてるのか気になりますよね？

うまい具合にここにデプロイしている[testappはSinatra製のアプリ](https://github.com/yuanying/testapp/blob/main/index.rb)で、以下の情報を表示してくれるようになっています。

- HttpのHostヘッダ (Requested Host)
- 自身に割り当てられているIPアドレス (Host IP Addresses)
- クライアントのIPアドレス (Client IP Address)

ping と同じように testapp-0 <-> testapp-1 間で curl してみます。

```bash
# testapp-0(Cilium:172.20.2.137) -> testapp-1(Flannel:10.243.6.4)
$ k exec -ti testapp-0 -- curl 10.243.6.4:4567
## Requested Host
- 10.243.6.4

## Host IP Addresses
- 10.243.6.4

## Client IP Address
- 10.243.4.0

# testapp-1(Flannel:10.243.6.4) -> testapp-0(Cilium:172.20.2.137)
$ k exec -ti testapp-1 -- curl 172.20.2.137:4567
## Requested Host
- 172.20.2.137

## Host IP Addresses
- 172.20.2.137

## Client IP Address
- 172.20.1.84
```

おや、クライアントのIPアドレスが自身のアドレスと異なるものが使われていますね。どうやらホストのIPアドレスでSNATされているようです。

ノードに潜って調べたところ、Flannel/Cilium の混在環境下においては大体こんな感じのネットワーク構成になっていることがわかりました。

![cni-flannel-cilium.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9958/08bba193-22fa-8566-afec-e0ec563a1eed.png)


#### testapp-0(Cilium:172.20.2.137) -> testapp-1(Flannel:10.243.6.4)

testapp-0 から testapp-1 への通信の際には、ノード内の flannel.1 を通る際に SNAT され、ソースのIPアドレスが flannel.1 のものに変更。

#### testapp-1(Flannel:10.243.6.4) -> testapp-0(Cilium:172.20.2.137)

testapp-1 から testapp-0 への通信の際には、ノード内の cilium_host を通る際に SNAT され、ソースのIPアドレスが cilium_host のものに変更。

ということのようです。

まあ、特段SNATされることで問題になるようなアプリケーションを使っていなければ問題はないのだけれども、一応、Kubernetesのネットワークモデルは以下のように説明されているようです。

- https://kubernetes.io/docs/concepts/services-networking/

> - pods can communicate with all other pods on any other node without NAT
> - agents on a node (e.g. system daemons, kubelet) can communicate with all pods on that node
> 
> Note: For those platforms that support Pods running in the host network (e.g. Linux), when pods are attached to the host network of a node they can still communicate with all pods on all nodes without NAT.

- **Podは全ての他のノードにいるPod含めてNAT無しで接続できなければならない。**

回避策はいくつか思いつくのだけれども、CNIの移行中の一時的な状態におけるネットワークの問題なので、無視できる人は無視しても良かろうと思う。その方法については時間があったら別途書きたいと思う。

#### これらのNATはどこからやってきたのか？

標準的なCNIプラグインは、自分が割り当てられたPodネットワーク以外へのパケット送信時にマスカレードを行う設定が含まれている。Podネットワークは大抵プライベートアドレスを持っているので、外側のネットワークに行く際にSNATしないとパケットが戻って来れなくなるからです。

##### Flannel

[通常のFlannelのインストーレション時](https://github.com/flannel-io/flannel/blob/402276f8b334537e5bcd6348589777862c193c2a/Documentation/kube-flannel.yml#L158)には以下のオプションが有効化されているはずです。

- `--ip-masq`

その際に、PodCIDRはマスカレードから除外されるようです。

##### Cilium

Helmインストール時で下記の設定で有効化します。デフォルト値は `true` のようです。

- `enableIPv4Masquerade`

また、`clusterPoolIPv4PodCIDRList` の値は自動的にマスカレードから除外されるようです。それとは別に、`ipv4NativeRoutingCIDR` の値でも追加でネットワークを指定できるようですが未検証です。

## :notebook_with_decorative_cover: その他注意点

- Service間通信で Pod Network に属するIPアドレスをNATしたくない場合は、kube-proxy の `--cluster-cidr` を事前に調整しておいた方が良いでしょう。

## :pencil: まとめ

Ciliumへのマイグレーションは簡単だった。