OpenShiftでService Internal Traffic Policyを試してみる

# この記事について

Kubernetesのベータ版機能としてService Internal Traffic Policyが提供されています。
この機能がOpenShift環境でも使えるかどうか確認してみました。

# Service Internal Traffic Policyについて

## ドキュメント
https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/

## どんな機能か

クラスター内のPod間通信をローカルに制御する機能です。
とあるPodからService経由で別のPodにリクエストを投げる際、発信元と同ノードにスケジュールされているPodにのみリクエストを割り振ってくれます。

具体例を挙げると、下記図について、podからappのServiceに対してリクエストを投げると、podのあるノード1上のapp podへアクセスすることになります。
![policy.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502403/c01d3079-d740-017f-c5f7-b67673b573eb.png)

## 設定方法

上記例で言うと、app podのServiceに`spec.internalTrafficPolicy: Local`に設定すればOK。

# 検証内容と結論

## ざっくり検証内容

1. アクセスするとホスト名を返すFlaskアプリを作成し、コンテナーイメージ化
2. そのコンテナーイメージをOpenShiftにデプロイ
3. アプリのServiceに`spec.internalTrafficPolicy: Local`を設定する
4. 別Podからアプリにアクセスしてみる

## 結論

同ノードにアプリのPodがあればそのPodにのみリクエストが割り振られる。ない場合はどこにもリクエストは割り振らない。

# 検証

## 環境

Red Hat OpenShift on IBM Cloud上で検証。

```bash
$ oc version
Client Version: 4.10.0-202208301216.p0.gdd2bcd0.assembly.stream-dd2bcd0
Server Version: 4.11.12
Kubernetes Version: v1.24.6+5157800
``` 
ノードは4つで構成。
```
$ oc get node
NAME          STATUS   ROLES           AGE   VERSION
10.245.0.11   Ready    master,worker   13d   v1.24.6+5157800
10.245.0.12   Ready    master,worker   13d   v1.24.6+5157800
10.245.64.7   Ready    master,worker   13d   v1.24.6+5157800
10.245.64.8   Ready    master,worker   13d   v1.24.6+5157800
```

## 検証手順

### アプリ作成

```bash
$ ls
Dockerfile  app.py
```

```python:app.py
from flask import Flask
import socket

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello world from {}</p>".format(socket.gethostname())

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080, debug=True)
```

```dockerfile:Dockerfile
FROM python:3.6-alpine

RUN pip install flask

COPY app.py /opt/

EXPOSE 8080

WORKDIR /opt

ENTRYPOINT ["python", "app.py"]
```

上記Dockerfileとアプリケーションでイメージビルドします。
```sh
$ podman build -t print-hostname .
```

IBM Cloud Container Registry（ICR）にイメージを保管して、そこからOpenShiftにデプロイしたいので、まずイメージのタグ付けをします（事前にレジストリーの名前空間は作成済）。
```
$ podman tag localhost/print-hostname:latest jp.icr.io/mynamespace/print-hostname:latest
```

IBM Cloudにログインした状態で、ICRのリージョンをセット。
```
$ ibmcloud cr region-set jp-tok
```

その状態でICRの認証。
```
$ ibmcloud cr login --client podman
```

認証出来たらレジストリーにプッシュ。
```
$ podman push jp.icr.io/mynamespace/print-hostname
```

### OpenShiftにアプリデプロイ

ICRからイメージプルするため、あらかじめ作成したProjectにイメージプルシークレットを作成します。

```
$ oc get secret all-icr-io -n default -o yaml | sed 's/default/myproject/g' | oc create -n myproject -f -
```

Deploymentはこんな感じ。`imagePullSecret`は直前で作成したイメージプルシークレットを指定。
```yaml:deploy-print-hostname.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: flask-app
  name: flask-app
spec:
  replicas: 4
  selector:
    matchLabels:
      app: flask-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: flask-app
    spec:
      containers:
      - image: jp.icr.io/mynamespace/print-hostname
        name: print-hostname
        resources: {}
      imagePullSecrets:
      - name: all-icr-io
status: {}
```

Deployment作成。
```
$ oc apply -f deploy-print-hostname.yaml
```

各ノードにPodがスケジュールされていることを確認（`flask-app`から始まるPod）。
```
$ oc get po -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP              NODE          NOMINATED NODE   READINESS GATES
access-test                  1/1     Running   0          6m36s   172.17.29.22    10.245.64.8   <none>           <none>
flask-app-574649d9b8-4dhhk   1/1     Running   0          13s     172.17.43.133   10.245.0.12   <none>           <none>
flask-app-574649d9b8-4ff9x   1/1     Running   0          13s     172.17.29.36    10.245.64.8   <none>           <none>
flask-app-574649d9b8-br769   1/1     Running   0          13s     172.17.57.8     10.245.64.7   <none>           <none>
flask-app-574649d9b8-qjjr6   1/1     Running   0          13s     172.17.60.249   10.245.0.11   <none>           <none>
```
全ノードにスケジュールされない場合はDeschedulerとかPod Topology Spread Constraintとかを使う（といいつつレプリカ数増やせば全ノードにスケジュールされそう）。

Deploymentに対してService作成。
```
$ oc expose deployment flask-app --port 8080
```

### アプリにアクセスするPodの作成

`access-test`という名前のPodを作成して、このPodからアプリにアクセスします。
```yaml:pod-access-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: access-test
spec:
  restartPolicy: OnFailure
  containers:
    - name: curl
      image: curlimages/curl
      imagePullPolicy: Always
      command: ["/bin/sh", "-c", "tail -f /dev/null"]
```

```
$ oc apply -f pod-access-test.yaml
```

この状態でアプリにアクセスしてみると、各Podにリクエストが投げられたことが分かります。
```
$ oc rsh access-test
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4ff9x</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-br769</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-qjjr6</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4ff9x</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4dhhk</p>/ $
```

### `internalTrafficPolicy`の設定とアクセス検証

アプリのServiceに`spec.internalTrafficPolicy: Local`を設定します。
```
$ oc edit svc flask-app
```

`access-test`Podからアプリに複数回アクセスしてみると、1つのPod（`flask-app-574649d9b8-4ff9x`）のみにリクエストが投げられたことが分かります。。
```
$ oc rsh access-test
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4ff9x</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4ff9x</p>/ $
/ $ curl flask-app:8080
<p>Hello world from flask-app-574649d9b8-4ff9x</p>/ $
```

Podのスケジュールされているノードを確認すると、`access-test`Podと`flask-app-574649d9b8-4ff9x`Podが同ノードにスケジュールされているので、Internal Traffic Policyが動作していることが確認できました。
```
$ oc get po -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP              NODE          NOMINATED NODE   READINESS GATES
access-test                  1/1     Running   0          6m36s   172.17.29.22    10.245.64.8   <none>           <none>
flask-app-574649d9b8-4dhhk   1/1     Running   0          13s     172.17.43.133   10.245.0.12   <none>           <none>
flask-app-574649d9b8-4ff9x   1/1     Running   0          13s     172.17.29.36    10.245.64.8   <none>           <none>
flask-app-574649d9b8-br769   1/1     Running   0          13s     172.17.57.8     10.245.64.7   <none>           <none>
flask-app-574649d9b8-qjjr6   1/1     Running   0          13s     172.17.60.249   10.245.0.11   <none>           <none>
```

### `internalTrafficPolicy: Local`に該当するPodがないとき

まず、`access-test`Podの乗るノードにtaintを付けます。
```
$ oc adm taint node 10.245.64.8 key1=value1:NoSchedule
```

この状態で、taintありのノード上にあるアプリのPodを削除します。
```
$ oc delete po flask-app-574649d9b8-4ff9x
```

アプリのPodが別ノードにスケジュールされ、`access-test`Podと同ノードにはアプリのPodが存在しない状態になります。
```
$ oc get po -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP              NODE          NOMINATED NODE   READINESS GATES
access-test                  1/1     Running   0          21h   172.17.29.22    10.245.64.8   <none>           <none>
flask-app-574649d9b8-4dhhk   1/1     Running   0          21h   172.17.43.133   10.245.0.12   <none>           <none>
flask-app-574649d9b8-6kqfj   1/1     Running   0          82s   172.17.57.50    10.245.64.7   <none>           <none>
flask-app-574649d9b8-br769   1/1     Running   0          21h   172.17.57.8     10.245.64.7   <none>           <none>
flask-app-574649d9b8-qjjr6   1/1     Running   0          21h   172.17.60.249   10.245.0.11   <none>           <none>
```

この状態でアプリへのアクセスを試みるもアクセスできません。
```
$ oc rsh access-test
/ $ curl flask-app:8080
curl: (7) Failed to connect to flask-app port 8080 after 1058 ms: Couldn't connect to server
/ $ curl flask-app:8080
curl: (7) Failed to connect to flask-app port 8080 after 1025 ms: Couldn't connect to server
/ $ curl flask-app:8080
curl: (7) Failed to connect to flask-app port 8080 after 1033 ms: Couldn't connect to server
```