Kubernetes のサービスとは (3) kube-proxy とネットワーク設定

# この記事について
[Kubernetes のサービスとは (2) リソース、API Server、コントローラー](https://qiita.com/nozmiz/items/715913f3026d9ca699bb)の続き。

Kubernetes の Service/Endpoints/EndpointSlice リソースに基づき、通信に必要なネットワーク設定を行う kube-proxy の話。
内容は大体こんな感じ：
- kube-proxy はノードのネットワーク設定を実際に行うコンポーネント
- Service の IP とネットワーク、ロードバランサーは実体はなく、単なる iptables 上のルールである

# kube-proxy って何？

kube-proxy は Kubernetes のコンポーネントで、Service/Endpoints/EndpointSlice リソースを監視し、それらの変更(作成・更新・削除)を検知した場合にリソースに登録されている情報をもとにノードのネットワーク設定を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/bbfeff07-c659-005c-95c7-b67f79aacb85.png)

kube-proxy は DaemonSet としてデプロイされているので各ノードで1つずつ Pod が動作している。

```bash
# DaemonSet として動作
$ kubectl get daemonset -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-proxy   2         2         2       2            2           kubernetes.io/os=linux   32h

# DaemonSet なので、kube-proxy の Pod は各ノードで実行される
$ kubectl get pod -n kube-system -o wide
NAME                             READY   STATUS    RESTARTS      AGE   IP               NODE  
coredns-565d847f94-p79gc         1/1     Running   1 (37m ago)   32h   192.168.219.71   master
coredns-565d847f94-r794g         1/1     Running   1 (37m ago)   32h   192.168.219.73   master
etcd-master                      1/1     Running   1 (37m ago)   32h   10.0.0.5         master
kube-apiserver-master            1/1     Running   1 (37m ago)   32h   10.0.0.5         master
kube-controller-manager-master   1/1     Running   1 (37m ago)   32h   10.0.0.5         master
kube-proxy-fkpfb                 1/1     Running   1 (37m ago)   32h   10.0.0.4         node01 ★これ
kube-proxy-mm4rc                 1/1     Running   1 (37m ago)   32h   10.0.0.5         master ★これ
kube-scheduler-master            1/1     Running   1 (37m ago)   32h   10.0.0.5         master
```
# kube-proxy が行うネットワーク設定とは？

kube-proxy には3つの動作モードがあり、デフォルトは iptables を構成する `iptables` モードになる。つまり、iptables の設定により Service の IP やロードバランスを実現する。
他にも user-space proxy モードと IPVS モードがあるがここでは割愛する。[仮想IPとサービスプロキシー](https://kubernetes.io/ja/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies) にこれらのモードの説明があるので必要に応じて参照いただければ。


# kube-proxy による iptables の設定

では、kube-proxy が実際にどんな設定を行うか見てみる。nginx の Deployment(レプリカ数=2)と Service(Type=ClusterIP) が以下のようにデプロイされているとする。Service の IP は `10.110.64.222` で、Pod の IP は `192.168.219.77`, `192.168.196.131`。

```bash
# Deployment に含まれる Pod
$ kubectl get pod -l app=nginx -o wide
NAME                    READY   STATUS    RESTARTS   AGE   IP                NODE
nginx-b8c585cf6-67wkb   1/1     Running   0          21m   192.168.219.77    master
nginx-b8c585cf6-stsmr   1/1     Running   0          22m   192.168.196.131   node01

# Service
kubectl get svc nginx
NAME    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
nginx   ClusterIP  10.110.64.222   <none>        8080/TCP   32s
```

kube-proxy は主に NAT テーブルを操作して Service やロードバランスを実現する。上記のリソースをデプロイした後の `sudo iptables -n -t nat -L` を実行して関係するところを抜粋したのが以下。

```diff
# 入口となる PREROUTING チェイン
Chain PREROUTING (policy ACCEPT)
target         prot opt source      destination   
KUBE-SERVICES  all  --  0.0.0.0/0   0.0.0.0/0     /* kubernetes service portals */

# 全 Service を含むチェイン
Chain KUBE-SERVICES (2 references)
target                     prot opt source      destination   
KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0   10.96.0.10     /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0   10.96.0.10     /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-JD5MR3NA4I4DYORP  tcp  --  0.0.0.0/0   10.96.0.10     /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
+ KUBE-SVC-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0   10.110.64.222  /* default/nginx:http cluster IP */ tcp dpt:8080
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0   10.96.0.1      /* default/kubernetes:https cluster IP */ tcp dpt:443

# 個々の Service に該当するチェイン
+ Chain KUBE-SVC-P4Q3KNUAWJVP4ILH (1 references)
+ target                     prot opt source           destination    
+ KUBE-MARK-MASQ             tcp  -- !192.168.0.0/16   10.110.64.222  /* default/nginx:http cluster IP */ tcp dpt:8080
+ KUBE-SEP-5V2UTJDVBQ73MZHD  all  --  0.0.0.0/0        0.0.0.0/0      /* default/nginx:http -> 192.168.196.131:80 */ statistic mode random probability 0.50000000000
+ KUBE-SEP-6KKQPHGRLUODOJRX  all  --  0.0.0.0/0        0.0.0.0/0      /* default/nginx:http -> 192.168.219.77:80 */

# そのサービスに紐づく Endpoint(Pod の IP:Port) に該当するチェイン ： 1個目
+ Chain KUBE-SEP-5V2UTJDVBQ73MZHD (1 references)
+ target          prot opt source            destination   
+ KUBE-MARK-MASQ  all  --  192.168.196.131   0.0.0.0/0     /* default/nginx:http */
+ DNAT            tcp  --  0.0.0.0/0         0.0.0.0/0     /* default/nginx:http */ tcp to:192.168.196.131:80

# そのサービスに紐づく Endpoint(Pod の IP:Port) に該当するチェイン ： 2個目
+ Chain KUBE-SEP-6KKQPHGRLUODOJRX (1 references)
+ target          prot opt source           destination
+ KUBE-MARK-MASQ  all  --  192.168.219.77   0.0.0.0/0      /* default/nginx:http */
+ DNAT            tcp  --  0.0.0.0/0        0.0.0.0/0      /* default/nginx:http */ tcp to:192.168.219.77:80

```

ややこしく見えるかもしれないが、図にすると結構シンプルに見えると思う。Service 一覧 → Service → Endpoint と段階的に iptables のルールを追って、最後にパケットの宛先を Pod に変換している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/6d18d35a-4e64-756a-75b1-5e48f626473b.png)

なお、ロードバランスは以下の部分で実現している。この場合は Pod が2つなので50%ずつ振り分けるようになっている。
```
Chain KUBE-SVC-P4Q3KNUAWJVP4ILH (1 references)
target                    prot opt source     destination    
...
KUBE-SEP-5V2UTJDVBQ73MZHD all  --  0.0.0.0/0  0.0.0.0/0  statistic mode random probability 0.50000000000
KUBE-SEP-6KKQPHGRLUODOJRX all  --  0.0.0.0/0  0.0.0.0/0  
```

statistic の設定については、https://linuxjm.osdn.jp/html/iptables/man8/iptables-extensions.8.html の説明を引用しておく。
> statistic
> このモジュールは統計的な条件に基づいたパケットのマッチングを行う。 二つのモードがサポートされており、 --mode オプションで設定できる。
> サポートされているオプション:
> --mode mode
> &nbsp;&nbsp;&nbsp; マッチングルールのマッチングモードを設定する。 サポートされているモードは random と nth である。
> [!] --probability p
> &nbsp;&nbsp;&nbsp; ランダムにパケットがマッチする確率を設定する。 random モードでのみ機能する。 p は 0.0 と 1.0 の範囲でなければならない。 サポートされている粒度は 1/2147483648 である。

# NodePort の場合

## NodePort とは？
ここまでは ClsuterIP という種類の Service の場合で、この IP は Kubernetes クラスターの中に閉じていて外部から直接アクセスできない。クラスターの外からアクセスするには NodePort という種類の Service が必要になる。すごく概念的な図だがこんなイメージ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/fdbb7e4b-41b9-93df-821d-ae3201c86820.png)

NodePort は実際にはクラスターを構成する各ノード上の IP:Port(上図の例では `172.16.0.1:30037`)で、イメージ的にはここから NodePort → Servcie → Pod という風にパケットが流れる感じである。

## NodePort はどういう構成になる？
では NodePort の場合はどう構成されるかというと、実は大きくは変わらない。これまた iptables 上の定義に過ぎず、パケットの宛先ポートが NodePort にマッチすると結局該当の Service(ClusterIP) に行く。 Service(ClusterIP)の前段に NodePort がいるという形になる。

Service(NodePort) を作成すると自動で Service(ClusterIP) も作成されるが、それはこのような通信経路になるためと言える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/9e325438-8eba-4edb-2620-a19b2740a507.png)

いちおう、iptables の nat テーブルに追加された部分を一部抜粋して記載しておく。
```diff
# Service 一覧のチェインに NodePort へ行くルールが追加される
Chain KUBE-SERVICES (2 references)
target                     prot opt source     destination   
...
KUBE-SVC-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0  10.110.64.222  /* default/nginx:http cluster IP */ tcp dpt:8080
...
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0  10.96.0.1      /* default/kubernetes:https cluster IP */ tcp dpt:443
+ KUBE-NODEPORTS             all  --  0.0.0.0/0  0.0.0.0/0      /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

# NodePort 一覧チェイン
+ Chain KUBE-NODEPORTS (1 references)
+ target                     prot opt source     destination    
+ KUBE-EXT-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0  0.0.0.0/0      /* default/nginx:http */ tcp dpt:30170

# 個々の NodePort のチェイン。 ペアとなる Service(Cluster)に行く
+ Chain KUBE-EXT-P4Q3KNUAWJVP4ILH (1 references)
+ target                     prot opt source     destination    
+ KUBE-MARK-MASQ             all  --  0.0.0.0/0  0.0.0.0/0      /* masquerade traffic for default/nginx:http external destinations */
+ KUBE-SVC-P4Q3KNUAWJVP4ILH  all  --  0.0.0.0/0  0.0.0.0/0
```

# 結局実体は？

これまでも何度か掲載した以下の図で言えば、赤色の Service Network、IP(Service の ClusterIP)、ロードバランサーは実体がなく iptables 上の定義として存在しているだけということになる。上で見たように NodePort も同様である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/6b23f224-7f1b-c03e-a999-966f70e10c24.png)


# 続き
執筆中。。。

# 補足・おまけ

## Pod からの iptables 設定
kube-proxy の Pod(つまりコンテナ)の中からノード側の iptables を設定できるのかという話があるが、`privileged: true` という特権モード(root 相当)で動いているので可能になる。

```bash
# DaemonSet のマニフェスト(一部のみ抜粋して記載)
$ kubectl get daemonset -n kube-system -o yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-proxy
  namespace: kube-system
spec:
  template:
    spec:
      containers:
      - command:
        - /usr/local/bin/kube-proxy
        - --config=/var/lib/kube-proxy/config.conf
        - --hostname-override=$(NODE_NAME)
        image: registry.k8s.io/kube-proxy:v1.25.2
        name: kube-proxy
        securityContext:
          privileged: true ★これ
```


## kube-proxy のデバッグログ表示
マネージドの Kubernetes だと出来ない場合が多いと思うが、自前で構築した場合はログレベルの変更を変えることができる。
以下のコマンドを実行する。

```bash
kubectl edit daemonset -n kube-system kube-proxy
```

マニフェストが開くので、以下の行を追加して保存する。数字の `5` の部分が大きいほどより詳細なログが出る(たぶん 0 ～ 9 まで指定できる)。これだけで OK。
```yaml
    spec:
      containers:
      - command:
        - /usr/local/bin/kube-proxy
        - --config=/var/lib/kube-proxy/config.conf
        - --hostname-override=$(NODE_NAME)
        - -v5  ★この行を追加
 ```

例えば、以下のようなログが見れて kube-proxy の動作を把握しやすくなる。
```
I0925 02:11:33.975296       1 service.go:440] "Adding new service port" portName="default/nginx:http" servicePort="10.102.246.227:8080/TCP"
I0925 02:11:33.975336       1 proxier.go:850] "Syncing iptables rules"
I0925 02:11:33.975343       1 iptables.go:467] running iptables: iptables [-w 5 -W 100000 -N KUBE-EXTERNAL-SERVICES -t filter]
```

## iptables のチェイン名の Prefix

[ソース](https://github.com/kubernetes/kubernetes/blob/v1.25.0/pkg/proxy/iptables/proxier.go#L687)で以下のように定義されている。
```go
const (
	servicePortPolicyClusterChainNamePrefix = "KUBE-SVC-"
	servicePortPolicyLocalChainNamePrefix   = "KUBE-SVL-"
	serviceFirewallChainNamePrefix          = "KUBE-FW-"
	serviceExternalChainNamePrefix          = "KUBE-EXT-"
	servicePortEndpointChainNamePrefix      = "KUBE-SEP-"

	// For cleanup.  This can be removed after 1.26 is released.
	deprecatedServiceLBChainNamePrefix = "KUBE-XLB-"
)
```