IstioのMesh内に仮想マシンを取り込む

あまり知られていないような気がするが、Istioは仮想マシン（VM）もMeshに取り込むことが出来る。
これを利用すると以下のような事が出来るようになる。
- Istioで保護されたネットワーク内でPod↔VM間通信が出来るようになる \
→DBだけVMを使いたいようなケースでもMeshを利用することが出来る
- VM側でKubernetesの名前を使ってクラスタ内のサービスにアクセスできる　\
（例：`curl nginx.mynamespace.svc`みたいなのが通るようになる）

めちゃくちゃ便利そうなので実際に構築して挙動を確認する。
ここではIstioの[Virtual Machine Installation](https://istio.io/latest/docs/setup/install/virtual-machine/)を参考に進める。

## 基礎知識
ネットワークをまたぐ構成でPodとVMを直接通信する場合のアーキテクチャは以下となる。
![1706683040217.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/7b409321-0080-1b6f-8a3d-be518fe38ce8.png)
（引用元：[Virtual Machine Architecture](https://istio.io/latest/docs/ops/deployment/vm-architecture/)）

IstiodはKubernetes側で用意し、DataplaneをVMに配置するような形となる。
PodとVM間はIstioのGatewayを介して通信する形となる。

注意点として、VM上に展開するサイドカーのパッケージが、RPMだとCentOS8しかサポートしていないようなので、試すならCentOS8を使うかUbuntu/Debian系のディストリビューションにしておく方が楽ができる。

## 前提
以下を用意する
- Kubernetesクラスタ
- VM
- istioctl
- istioctlのインストール時にダウンロードされるディレクトリ・ファイル一式

今回はKubernetesクラスタにvSphere with Tanzuで構築したKubernetesクラスタを使い、VMはUbuntuを使用した。

また検証するIstioの構成としては、
- Multi Network（VMとKubernetesで別のネットワークを利用）
- WorkloadEntry（後述）の自動作成を有効

の構成とする。

## 検証
### 事前準備
Multi Network構成では`spec.values.global.network`にKubernetes側のネットワーク名を適当に定義してIstioをインストールする。
インストールに使うYAMLファイルを作成する。
```yaml
export CLUSTER=tkg-cluster-2
export CLUSTER_NETWORK=kube-network
cat << EOF > ./vm-cluster2.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: istio
spec:
  values:
    global:
      meshID: mesh1
      multiCluster:
        clusterName: "${CLUSTER}"
      network: "${CLUSTER_NETWORK}"
EOF
```
`istioctl`でインストールする。WorkloadEntryの自動作成を有効にするために設定用パラメータもコマンドラインに追加して実行する。
```sh
istioctl install -f vm-cluster2.yaml --set values.pilot.env.PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION=true --set values.pilot.env.PILOT_ENABLE_WORKLOAD_ENTRY_HEALTHCHECKS=true -y
```
East-Westで通信するためのEast-West Gatewayをインストールする。
```sh
samples/multicluster/gen-eastwest-gateway.sh \
--mesh mesh1 --cluster "${CLUSTER}" --network "${CLUSTER_NETWORK}" | \
istioctl install -y -f -
```
Istiodを公開するための`kind: VirtualService`と`kind: Gateway`を作成し、`Namespace`の`istio-system`にネットワークが複数ある場合にどのネットワークを使うかのラベルを付与する。
```sh
kubectl apply -n istio-system -f samples/multicluster/expose-istiod.yaml
kubectl apply -n istio-system -f samples/multicluster/expose-services.yaml
kubectl label namespace istio-system topology.istio.io/network="${CLUSTER_NETWORK}"
```


VMを管理するための`Namespace`と`ServiceAccount`を作成する。
```sh
kubectl create ns vmtest
kubectl create sa vmtestsa -n vmtest
```

次に`WorkloadGroup`を作成する。`WorkloadGroup`というのはMeshに追加するワークロード(VM)のグループを定義したものの模様。
`Pod`に対する`Deployment`のようなものらしい。
なお、`Pod`に相当するものは`WorkloadEntry`になり、今回の手順ではこの`WorkloadEntry`を自動でつくる手筈で進めている。
`WorkloadGroup`の仕様は[こちら](https://istio.io/latest/docs/reference/config/networking/workload-group/)参照。
ということで、以下で作成してクラスタに適用する。
```yaml
cat <<EOF > workloadgroup.yaml
apiVersion: networking.istio.io/v1alpha3
kind: WorkloadGroup
metadata:
  name: "ubuntu"
  namespace: "vmtest"
spec:
  metadata:
    labels:
      app: "ubuntu"
  template:
    serviceAccount: "vmtestsa"
    network: "${CLUSTER_NETWORK}"
EOF
kubectl apply -f workloadgroup.yaml -n vmtest
```
ちなみに`WorkloadGroup`側でVMにReadinessProbeみたいな感じでヘルスチェックを設定することも出来るが、ここでは割愛する。

作成したら、`istioctl`に食わせて設定ファイルを生成する。
```sh
mkdir vmfiles
istioctl x workload entry configure -f workloadgroup.yaml -o ./vmfiles --clusterID "${CLUSTER}" --autoregister
```
生成されたファイルはそれぞれ以下となる。

|||
|---|---|
|cluster.env|サービス名やNamespaceなどの複数の環境変数を定義したファイル|
|istio-token|CAから証明書を取得するのに使うKubernetesトークン|
|mesh.yaml|`discoveryAddress`を設定するための[`ProxyConfig`](https://istio.io/latest/docs/reference/config/networking/proxy-config/)|
|root-cert.pem| 認証に使用されるルート証明書|
|hosts|/etc/hostsに追記する内容（istio-eastwestgatewayのExternal IPとistiod.istio-system.svcの組が記載）|

ちなみに[`ProxyConfig`](https://istio.io/latest/docs/reference/config/networking/proxy-config/)と[`MeshConfig`](https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/)の違いはWorkloadやNamespace単位など細かな単位でも設定出来るのが`ProxyConfig`で、Mesh全体への設定を行うのが`MeshConfig`だそう。

生成されたファイルをVMに転送する。
```sh
scp ./vmfiles/* ubuntu@10.215.76.71:
```

以上で下準備は終了となる。

### VM側の設定
ルート証明書を配置する。
```sh
sudo mkdir -p /etc/certs
sudo cp ./root-cert.pem /etc/certs/root-cert.pem
```
Kubernetesトークンを配置する。
```sh
sudo  mkdir -p /var/run/secrets/tokens
sudo cp ./istio-token /var/run/secrets/tokens/istio-token
```
サイドカーをインストールする。なお、ここではDebian系のパッケージで入れたが、rpmも用意されている。（ただしCentOS8しかサポートしていない点に注意）
```sh
curl -LO https://storage.googleapis.com/istio-release/releases/1.20.2/deb/istio-sidecar.deb
sudo dpkg -i istio-sidecar.deb
```
ちなみにこれの中身は以下のようになっている。
```sh
$ dpkg-deb -c istio-sidecar.deb
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./lib/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./lib/systemd/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./lib/systemd/system/
-rw-r--r-- 0/0             382 2024-01-03 14:37 ./lib/systemd/system/istio.service
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./var/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./var/lib/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./var/lib/istio/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./var/lib/istio/envoy/
-rw-r--r-- 0/0            4818 2024-01-03 14:37 ./var/lib/istio/envoy/sidecar.env
-rw-r--r-- 0/0           22679 2024-01-03 14:37 ./var/lib/istio/envoy/envoy_bootstrap_tmpl.json
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/local/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/local/bin/
-rwxr-xr-x 0/0        44118016 2024-01-03 14:38 ./usr/local/bin/pilot-agent
-rwxr-xr-x 0/0            5847 2024-01-03 14:37 ./usr/local/bin/istio-start.sh
-rwxr-xr-x 0/0       114688376 2024-01-03 14:38 ./usr/local/bin/envoy
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/share/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/share/doc/
drwxr-xr-x 0/0               0 2024-01-03 14:42 ./usr/share/doc/istio-sidecar/
-rw-r--r-- 0/0             133 2024-01-03 14:42 ./usr/share/doc/istio-sidecar/changelog.gz
```
また、パッケージインストール時に`istio-proxy`というユーザが作成される。

環境変数を定義したファイルをEnvoyのディレクトリにコピーする。
```sh
sudo cp ./cluster.env /var/lib/istio/envoy/cluster.env
```
`ProxyConfig`をIstioのMeshの設定ディレクトリにコピーする。
```sh
sudo cp ./mesh.yaml /etc/istio/config/mesh
```
istio-eastwestgatewayにアクセスするために/etc/hostsに追記する。
```sh
sudo sh -c 'cat $(eval echo ~$SUDO_USER)/hosts >> /etc/hosts'
```
`istio-proxy`ユーザがディレクトリを参照できるようアクセス権を設定する。
```sh
sudo mkdir -p /etc/istio/proxy
sudo chown -R istio-proxy /var/lib/istio /etc/certs /etc/istio/proxy /etc/istio/config /var/run/secrets /etc/certs/root-cert.pem
```

以上でVM上でIstioのサイドカーを動かすための設定は終了となる。
次にIstioのサイドカーを起動する。
```sh
sudo systemctl start istio
```
起動すると以下のような感じのstatusとなる。
```sh
$ systemctl status istio
● istio.service - istio-sidecar: The Istio sidecar
     Loaded: loaded (/lib/systemd/system/istio.service; disabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-01-29 07:40:15 UTC; 15s ago
       Docs: http://istio.io/
    Process: 24624 ExecStartPre=/usr/bin/install -d -o istio-proxy -m 0755 /var/run/secrets (code=exited, status=0/SUCCESS)
   Main PID: 24625 (sudo)
      Tasks: 16 (limit: 1012)
     Memory: 34.5M
        CPU: 2.934s
     CGroup: /system.slice/istio.service
             ├─24625 sudo -E -u istio-proxy -s /bin/bash -c "ulimit -n 1024; INSTANCE_IP=10.215.76.71 POD_NAME=ubuntuguest POD_NAMESPACE=vmtest exec /usr/local>
             ├─24711 /usr/local/bin/pilot-agent proxy
             └─24720 /usr/local/bin/envoy -c etc/istio/proxy/envoy-rev.json --drain-time-s 45 --drain-strategy immediate --local-address-ip-version v4 --file-f>

Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_OUTPUT -o lo -p tcp -m tcp ! --dport 53 -m owner ! --gid-owner 997 -j RETURN
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_OUTPUT -m owner --gid-owner 997 -j RETURN
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_OUTPUT -d 127.0.0.53/32 -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 15053
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_OUTPUT -j ISTIO_REDIRECT
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: COMMIT
Jan 29 07:40:15 ubuntuguest istio-start.sh[24701]: # Completed on Mon Jan 29 07:40:15 2024
Jan 29 07:40:15 ubuntuguest sudo[24625]:     root : PWD=/ ; USER=istio-proxy ; COMMAND=/bin/bash -c '\\/bin\\/bash -c ulimit\\ -n\\ 1024\\;\\ INSTANCE_IP\\=10\>
Jan 29 07:40:15 ubuntuguest sudo[24625]: pam_unix(sudo:session): session opened for user istio-proxy(uid=997) by (uid=0)
```
ただし、正常にサービスが起動してもエラーとなってることもあるので、挙動がおかしいと思ったら`/var/log/istio/istio.log`や`/var/log/istio/istio.err.log`を確認するとよい。
また、正常に起動すれば`istioctl proxy-status`で*ホスト名.Namespace名*で見えるようになる。
```sh
$ istioctl proxy-status
NAME                                                    CLUSTER           CDS        LDS        EDS        RDS          ECDS         ISTIOD                      VERSION
istio-eastwestgateway-6fdf6bdf69-tzssk.istio-system     tkg-cluster-2     SYNCED     SYNCED     SYNCED     NOT SENT     NOT SENT     istiod-75f9cbf8b7-249xh     1.20.2
istio-ingressgateway-846c5b8ccd-d7kwl.istio-system      tkg-cluster-2     SYNCED     SYNCED     SYNCED     NOT SENT     NOT SENT     istiod-75f9cbf8b7-249xh     1.20.2
ubuntuguest.vmtest                                      tkg-cluster-2     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-75f9cbf8b7-249xh     1.20.2
```
また、クラスタ内に`kind: WorkloadEntry`のオブジェクトが自動生成されることも確認できる。
```sh
$ kubectl get workloadentries.networking.istio.io -n vmtest
NAME                  AGE    ADDRESS
ubuntu-10.215.76.71   4m3s   10.215.76.71
```

### 動作確認
ここではVMからPodへのアクセス、およびPodからVMへのアクセスを確認する。

まず、検証用にVMからアクセスする検証用のPod(nginx)を起動し公開する。
```sh
SAMPLE_NS=nginx-ns
kubectl create ns $SAMPLE_NS
kubectl label namespace $SAMPLE_NS istio-injection=enabled
kubectl run nginx --image nginx -n $SAMPLE_NS
kubectl expose pod nginx --port 80 -n $SAMPLE_NS
```
また、VMにアクセスするPod(CentOS)も用意する。
```sh
kubectl run --image centos centos -n nginx-ns -- sleep infinity
```

また、VM側でも検証用にHTTPサーバを構築する。
Pythonでhttp.serverモジュールを呼べばポート8000で公開してくれる。
```sh
python3 -m http.server &
```
このHTTPサーバにPodからアクセスするためにはKubernetes側で`Service`を作成する必要があるので、以下のように作成する。
```yaml
cat <<EOF | kubectl apply -f - 
apiVersion: v1
kind: Service
metadata:
  name: ubuntu-svc
  namespace: vmtest
  labels:
    app: ubuntu
spec:
  ports:
  - port: 8000
    name: tcp
  selector:
    app: ubuntu
EOF
```

以上で検証用のサービスの準備は完了となる。

まず、VMからPodへのアクセスを確認する。Pod間通信と同じ要領で先程作成したnginxのService（`nginx.nginx-ns.svc`）にアクセスする。
```sh
$ curl nginx.nginx-ns.svc
<!DOCTYPE html>
<html>
<head>
：（省略）
```
問題なくアクセスできた。
次は逆向き、KubernetesクラスタからVMにアクセスしてみる。
先程作成したUbuntu用のServiceにCentOSからアクセスする。
```sh
$ kubectl exec -it centos -n nginx-ns -- curl ubuntu-svc.vmtest.svc:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
：（省略）
```
こちらも問題なくアクセスできた。

## 所感
今回はVMをMeshに追加してPodとVM間で通信する方法を確認した。
特にDNSなどの設定をせずにPodとVM間をKubernetes上の名前でアクセスできるのは非常に便利だと感じた。
ただ、以下あたりはちょっと課題なのかなとも感じた。
- Control PlaneはKubernetes上に必要 \
→Kubernetes抜きでの構成は組めなさそう
- RedHat系のサポートOSがCentOS8のみ \
→エンタープライズ系のOSサポートが必要な環境だと利用が厳しい

上記の課題が将来的に解決できればService Meshの幅がかなり広がりそうなので、将来的に改善されることを期待したい。


## おまけ：可視化
Kiali、Grafanaを入れて可視化してみる。
なお、本来はKubernetes内の名前を使ってVMのメトリクスを取りたかったのだが、PodからVMのメトリクスのエンドポイントへのアクセス時にエラーが出て断念し、VMのIPを直接叩いてメトリクスを取得した。
（起きた問題は多分[これ](https://github.com/istio/istio/issues/35870)と同件っぽい）

まずメトリクスを取得するためにPrometheusをインストールする。
Istioに付属しているPrometheusのYAMLを以下のように修正し、VMをスクレイピングの対象とする。
```diff
--- prometheus.yaml.orig	2024-02-02 14:47:15
+++ prometheus.yaml	2024-02-02 15:42:20
@@ -43,6 +43,15 @@
     - /etc/config/rules
     - /etc/config/alerts
     scrape_configs:
+    - job_name: ubuntu
+      scrape_interval: 15s
+      scrape_timeout: 10s
+      scheme: http
+      metrics_path: /metrics
+      static_configs:
+      - targets:
+        - 10.215.76.71:15020
+        #- ubuntu-svc.vmtest.svc:15020
     - job_name: prometheus
       static_configs:
       - targets:

```
修正したManifestを使ってPrometheusをインストールする。
```sh
kubectl apply -f samples/addons/prometheus.yaml
```
次にIstioに付属しているKiali、Grafanaをインストールする。
```sh
kubectl apply -f samples/addons/kiali.yaml -f samples/addons/grafana.yaml 
```
適当にPodからVMに負荷を掛けた状態でKialiにアクセスしてみる。
![1706856787715.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/e0f19ecb-a9bf-5d0d-ceeb-c863b8e3ece9.png)

上手く見えているようだ。
Grafanaにもアクセスしてみる。
![1706857129256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/16bc4713-2f11-7348-d88a-d85eb7032e57.png)

こちらも問題なさそうだ。
これを上手く活用すれば、Prometheusのメトリクスのエンドポイントを持たないアプリケーションを動作させているVM環境でも、Prometheusによる監視やKiali,Grafanaによる可視化を実現できそうである。