Kubernetes: Service の静的 IP 用レンジを分割する (ServiceIPStaticSubrange)

Service の静的 IP アドレス用のレンジを分割する [`ServiceIPStaticSubrange` というフィーチャーゲート](https://kubernetes.io/docs/concepts/services-networking/service/#service-ip-static-sub-range)が、[Kubernetes v1.24](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.24.md) でアルファ機能として導入されました。これは、Service の動的 IP アドレスと、静的 IP アドレスの衝突リスクを減らすための機能です。この記事では Kubernetes v1.24.0 時点で、この機能について調べた内容を記載しています。

:::note warn
Kubernetes v1.24 時点ではこの機能はアルファのため、本番環境での利用はベータ以上を待ったほうが良いでしょう。
:::

## 背景
Service の IP アドレス(Cluster IP) は、動的・静的どちらの割り当ても可能です。通常は、`.spec.clusterIP` を省略し、動的な割り当てを使うことが多いと思います。しかし、クラスタ DNS の Service [^kubedns]のように、[静的な割り当てが必要](https://kubernetes.io/docs/concepts/services-networking/service/#choosing-your-own-ip-address)なケースも稀ながら存在します。

[^kubedns]:  クラスタ DNS の IP アドレスは、kubelet の引数 `--cluster-dns` に事前に指定しておく必要があるため、静的な IP アドレスが必要です。

動的・静的どちらの割り当ても、API サーバーが管理する Cluster IP のレンジから IP アドレスを選択します。動的な IP アドレスはこのレンジ内からランダムに選択されます [^dynamic-allocation]。そのため、静的な Cluster IP は、動的に割り当てられた Cluster IP とアドレスが衝突する可能性がありました。

[^dynamic-allocation]: 現在の動的割り当ての実装は [Current Services ClusterIPs allocation model](https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/3070-reserved-service-ip-range#current-services-clusterips-allocation-model) が参考になります。

クラスタ DNS の場合、他の Service よりも早く作成しておくことで衝突を回避できます。しかし、汎用的な解決策ではなく、Service 再作成時など衝突の可能性が残っていました。

## 静的な Cluster IP 範囲の分割する (`ServiceIPStaticSubrange`)
この問題の解決のため実装されたのが、[`ServiceIPStaticSubrange` という機能](https://kubernetes.io/docs/concepts/services-networking/service/#service-ip-static-sub-range)です。Kubernetes v1.24 ではアルファ機能のため、利用するには[フィーチャーゲート](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/)で有効にする必要があります。

![service ip static subrange](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/f93062ac-9333-4b92-bb78-0e2864b01f2e.png)

この機能は、API サーバーの引数 `--service-cluster-ip-range` で指定された Cluster IP のレンジを、前半を静的割り当て用の優先レンジ、後半を動的割り当て用の優先レンジとして分割して管理する機能です。静的割り当て用のレンジは CIDR の大きさに応じて 16〜256 個の範囲で分割されます。

例えば、Cluster IP のレンジが `192.168.0.0/16` のとき、以下のように分割されます。

IP アドレス | IP 数 | 用途
--- | --- | ---
192.168.0.0 | 1 | ネットワークアドレス
192.168.0.1 - 192.168.1.0 | 256 | 静的割り当て優先レンジ
192.168.1.1 - 192.168.255.254 | 65278 | 動的割り当て優先レンジ
192.168.255.255 | 1 | ブロードキャストアドレス

このレンジはあくまで優先して使われるレンジであり、動的割り当て用レンジが枯渇した場合、静的割り当て用レンジからも動的に割り当てられる点にご注意ください。[KEP-3070](https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/3070-reserved-service-ip-range) によると、後方互換性を保つための仕様のようです。この仕様は[公式ドキュメント](https://kubernetes.io/docs/concepts/services-networking/service/#service-ip-static-sub-range) にも記載されています。

> Dynamic IP allocations will be preferentially chosen from the upper band, reducing risks of conflicts with the IPs assigned from the lower band.

:::note warn
Kubernetes v1.24.0 の実装では、[動的用レンジの枯渇時に IP アドレスが重複するバグ](https://github.com/kubernetes/kubernetes/issues/109927)があります。これは [Fix ServiceIPStaticSubrange assigns duplicate IP addresses #109928](https://github.com/kubernetes/kubernetes/pull/109928) で修正されています。
:::

## CIDR サイズによる静的割り当て用のサイズ
静的割り当て用の優先レンジは、CIDR の大きさに応じて 16〜256 個の範囲で分割されます。この値は以下の[計算式](https://kubernetes.io/docs/concepts/services-networking/service/#service-ip-static-sub-range)で求められます。

```
min(max(16, cidrSize / 16), 256)
```

動的割り当て用の優先レンジのサイズは以下で計算で求められます。(引いている 2 はネットワークアドレスとブロードキャストアドレス)

```
cidrSize - staticSize - 2
```

CIDR range | 全 IP 数 | static 用 | dynamic 用
--- | --- | --- | ---
/26 | 64 | 16 | 46
/25 | 128 | 16 | 110
/24 | 256 | 16 | 238
/23 | 512 | 32 | 478
/22 | 1,024 | 64 | 958
/21 | 2,048 | 128 | 1,918
/20 | 4,096 | 256 | 3,838
/19 | 8,192 | 256 | 7,934
/18 | 16,384 | 256 | 16,126
/17 | 32,768 | 256 | 32,510
/16 | 65,536 | 256 | 65,278

## minikube で `ServiceIPStaticSubrange` 試す
minikube を使うと、`ServiceIPStaticSubrange` を簡単に試すことができます。ここでは、意図的に小さい Cluster IP レンジ (`/26`) を指定しています。

```
minikube start \
  --kubernetes-version v1.24.0 \
  --extra-config=apiserver.service-cluster-ip-range=10.96.0.0/26 \
  --feature-gates=ServiceIPStaticSubrange=true
```

minikube 直後は、静的割り当てされた `kubernetes` [^kubernetes-svc] と、`kube-dns` の 2 Service が存在しています。

[^kubernetes-svc]: `default` ネームスペースの `kubernetes` は Kubernetes API サーバー用の特別な Service です。

```
$ kubectl get svc --all-namespaces
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  3m8s
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   3m7s
```

Service を作成する前にダミーの Pod を作成しておきます。

```
kubectl create deploy nginx --image nginx --port 80
```

動的割り当ての Service を作ります。動的割り当て優先レンジの個数ピッタリの 46 個作成します。

```
for i in $(seq 1 46); do
  kubectl expose --name "svc$i" deploy/nginx --port 80
done
```

作成された Service を IP アドレスでソートして見てみます。動的割り当て優先レンジである `10.96.0.17` 〜 `10.96.0.62` の範囲で Service (`svc*`) が作らていることが確認できます。:tada:

```
$ kubectl get svc --all-namespaces | sort -k4 -V
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  3m53s
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   3m52s
default       svc43        ClusterIP   10.96.0.17   <none>        80/TCP                   16s
default       svc40        ClusterIP   10.96.0.18   <none>        80/TCP                   16s
default       svc11        ClusterIP   10.96.0.19   <none>        80/TCP                   18s
default       svc10        ClusterIP   10.96.0.20   <none>        80/TCP                   18s
#....
default       svc7         ClusterIP   10.96.0.60   <none>        80/TCP                   18s
default       svc29        ClusterIP   10.96.0.61   <none>        80/TCP                   17s
default       svc28        ClusterIP   10.96.0.62   <none>        80/TCP                   17s
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
```

:::note warn
本来、この次に動的割り当ての Service 作成すると、静的割り当て優先レンジの範囲から IP アドレスが割り当てられます。しかし、前述の通り Kubernetes v1.24.0 の実装では、[動的用レンジの枯渇時に IP アドレスが重複するバグ](https://github.com/kubernetes/kubernetes/issues/109927) のため正しく動作しませんでした。
:::

#### `ServiceIPStaticSubrange` 無効の場合
前項の手順を `ServiceIPStaticSubrange` だけ無効にした場合、作成された Service は以下のようになりました。静的割り当ての Service (`kubernetes`, `kube-dns`) と動的割り当ての Service の IP アドレスが並んで割り当てられていることが確認できます。

```
$ kubectl get svc --all-namespaces | sort -k4 -V | head
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  6m40s
default       svc6         ClusterIP   10.96.0.2    <none>        80/TCP                   76s
default       svc25        ClusterIP   10.96.0.3    <none>        80/TCP                   75s
default       svc37        ClusterIP   10.96.0.4    <none>        80/TCP                   74s
default       svc39        ClusterIP   10.96.0.5    <none>        80/TCP                   74s
default       svc36        ClusterIP   10.96.0.6    <none>        80/TCP                   74s
default       svc41        ClusterIP   10.96.0.7    <none>        80/TCP                   74s
default       svc43        ClusterIP   10.96.0.8    <none>        80/TCP                   74s
default       svc46        ClusterIP   10.96.0.9    <none>        80/TCP                   74s
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   6m38s
```

## まとめ
`ServiceIPStaticSubrange` 機能を使って、Service の静的 IP アドレス用のレンジを分割して管理できることが確認できました。現在はアルファのため本番環境の使用には適しませんが、将来的に静的な IP アドレスの割り当て時の IP アドレスの衝突のリスクを大きく減らすことができそうです。

## 参考
- https://kubernetes.io/docs/concepts/services-networking/service/#service-ip-static-sub-range
- https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/3070-reserved-service-ip-range