Tanzu Service MeshのGlobal Namespaceを試す

VMwareが提供しているTanzu Service Mesh(以下TSM)はIstioベースのService Meshを提供するが、大きな違いとしてはSaaSを介したGlobal Namespaceと呼ばれる機能があることが1つ言える。
これはクラスタ間のNamespaceやクラスタ内の複数のNamespaceの壁を取っ払い、同じNamespace内にいるかのようにPod間通信を実現するものだ。
今回はこれを試してみる。

なお、付録に初回の構築方法などもまとめている。どう試したらいいかが分からない人はそちらから参照するとよい。

※'23/10/17追記：本記事の中でクラスタのNamespace名をクラスタ間で合わせる話を書いているが、クラスタ間でNamespace名を合わせないといけないという制約がなくなっている模様。現時点では異なるNamespace名でも通信可能となっている。

## Global Namespaceの作成
ここではクラスタを横断したNamespaceとしてのGlobal Namespace(以下GNS)を作成する。
左サイドバーの`NEW WORKFLOW`をクリックし、` New Global Namespace`をクリックする。

`1. General Details`では以下を入力する。
- `GNS Name`: GNSの名前。TSM固有のものとなる。
- `Description`: GNSの説明。
- `Color`: GNSの色の指定（どこで使われているかが不明。。。）
- `Domain`: GNS内で利用するドメイン。クラスタを跨いだ通信をする際に利用する。

![1691470113437.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/b21e1525-3889-9c08-86d9-541f92d1d7d2.png)

`2. Namespace Mapping`では以下を入力する。
- `Cluster Name`: GNSに紐付けるクラスタを選択する
- `Namespace`: GNSに紐付けるNamespaceを選択する。

複数のクラスタを選択することになると思うので、`+ADD MAPPING RULE`をクリックして、紐付けるクラスタ・Namespaceを追加する。
![1691476405776.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/d26de8a3-df1a-8e33-276d-68a51ffbfc65.png)
Namespaceについては、各クラスタごとに同じ名前のServiceがいる場合、Namespace名も揃えておかないと通信に失敗する。（同一クラスタ内の場合は問題なく利用できる）
また、1つのNamespaceは1つのGNSにのみ紐付けることが出来、複数のGNSに紐付けることは出来ない。

`3. External Services (Optional)`ではGNSに紐付ける外部のサービスを設定する。以下は設定する場合の画面だが、ここでは簡単な検証を目的としているので`No External Services`を選択してスキップする。
![1691470900701.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/563c1810-fda3-beb3-5a8e-9f4e9baf0c89.png)
なお、External Servicesの説明は[こちら](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/using-tanzu-service-mesh-guide/GUID-F7DC3814-0C3B-42E8-94A1-64B4B182D783.html)に公式の説明があるが、外部のサービス（Lambda、DB等）にGNS経由でアクセスすることが出来るもので、サポートしているプロトコルとしてはHTTP、HTTPS、TCP、TLSとなっている。

`4. Public Services (Optional)`ではService Meshに対して外部から接続するためのGSLBなどの情報を入力する。以下は設定する場合の画面だが、ここでは簡単な検証を目的としているので`No Public Services`を選択してスキップする。
![1691471848918.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/3ba45765-1d8b-6c47-6ef6-6c351f045cc2.png)
 
なお、Public Servicesの説明は[こちら](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/using-tanzu-service-mesh-guide/GUID-58A3FA7C-4EFC-44B2-B37B-D2152CB16359.html)に公式の説明がある。

`5. Configuration Summary`で確認画面になるので、設定に問題なければ`FINISH`を押してGNSを作成する。

作成が完了すると、対象のNamespaceに`kind: Service`のオブジェクトが存在する場合は`kind: VirtualService`のオブジェクトが作成される。

```sh
$ kubectl get vs -n tsm-test1
NAME                                GATEWAYS   HOSTS                                                                      AGE
nsxsm.gns.gns-test-advanced.nginx              ["nginx.hoge.tanzu","nginx.tsm-test1.svc.cluster.local","243.228.38.11"]   44m
```

## 動作確認(クラスタ跨ぎでの同一Namespace名間アクセス)

クラスタ1の方でサンプルのアプリを立ち上げる。
```sh
kubectl create deploy nginx --image nginx -r 1 -n tsm-test1
kubectl expose deploy nginx --port 80 -n tsm-test1
```
クラスタ2の方でアクセス用Podを立ち上げる。
```sh
kubectl run centos --image centos -n tsm-test1 -- sleep 365d
```
クラスタ2のPodからクラスタ1のServiceにアクセスする。
アクセス先は`<サービス名>.<GNS作成時に設定したドメイン名>`となる。
アクセス先は`kubectl get vs`でも確認できる。
```sh
$ kubectl exec -it centos -n tsm-test1 -- curl nginx.hoge.tanzu
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
：（省略）
```
External-IPなどを利用せずにクラスタを跨いでサービスにアクセスすることが出来た。

なお、Istioサイドカーが入っていない場合、名前解決に失敗する。
この動きも確認する。
[Istioの仕様](https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/)によると、Labelに`sidecar.istio.io/inject: "false"`を指定するとIstioサイドカーが挿入されないので、これを設定したアクセス用Podを作成する。
```sh
kubectl delete pod centos -n tsm-test1
kubectl run centos --image centos -n tsm-test1 -l sidecar.istio.io/inject="false"  -- sleep 365d
```
アクセスしてみる。
```sh
$ kubectl exec -it centos -n tsm-test1 -- curl nginx.hoge.tanzu
curl: (6) Could not resolve host: nginx.hoge.tanzu
command terminated with exit code 6
```
先程と異なり、アクセスできないことが確認できた。

また、ここで第3のクラスタをGNSに追加し、nginxを同じようにtsm-test1にデプロイする。
```
kubectl create deploy nginx --image nginx -r 1 -n tsm-test1
kubectl expose deploy nginx --port 80 -n tsm-test1
```
この状態で`ServiceEntry`オブジェクトを確認する。`ServiceEntry`は実際にそのホストにアクセスした時のアクセス先の情報を含んでいる。
それぞれのクラスタで`kubectl get se -o yaml nsxsm.gns.gns-test-advanced.nginx`を実行すると結果は以下のようになった。
クラスタ1：
```
  endpoints:
  - address: nginx.tsm-test1
    ports:
      http1: 80
    weight: 4294967293
  - address: istio-egressgateway.istio-system
    ports:
      http1: 443
    weight: 1
```
クラスタ2：
```
  endpoints:
  - address: istio-egressgateway.istio-system
    ports:
      http1: 443
    weight: 1
```
クラスタ3：
```
  endpoints:
  - address: nginx.tsm-test1
    ports:
      http1: 80
    weight: 4294967293
  - address: istio-egressgateway.istio-system
    ports:
      http1: 443
    weight: 1
```
クラスタ1、3は宛先が2つあるものの、weightが異なっており、自クラスタの宛先を優先的に使うように設定される。
クラスタ2は自クラスタにnginxのServiceを持っていないため、istio-egressgateway経由で別クラスタにアクセスしに行くことが分かる。


## 動作確認(同一クラスタ内別Namespace名間アクセス)
同一のクラスタ内で別Namespaceへのアクセスも、GNSを作成することでNamespaceを意識せずにアクセスすることが出来る。

クラスタ内のNamespace `tsm-test2`と`tsm-test3`をGNS `red`で繋いだ時の挙動を確認する。（GNSの作成方法については省略する）
アクセスすると自身のPodIPを表示する[サンプルアプリ](https://github.com/imurata/ipchecker)をそれぞれのNamespaceで起動し、公開する。
```
kubectl run ipchecker --image imuratavmware/ipchecker -n tsm-test2
kubectl expose pod ipchecker --port 80 -n tsm-test2
kubectl run ipchecker --image imuratavmware/ipchecker -n tsm-test3
kubectl expose pod ipchecker --port 80 -n tsm-test3
```
`VirtualService`としては以下となる。
```
$ kubectl get vs -A
tsm-test2   nsxsm.gns.red.ipchecker                            ["ipchecker.tanzu.tkg","ipchecker.tsm-test2.svc.cluster.local","244.177.154.138"]   3m44s
tsm-test3   nsxsm.gns.red.ipchecker                            ["ipchecker.tanzu.tkg","ipchecker.tsm-test3.svc.cluster.local","244.177.154.138"]   3m43s
```
作成したPodのIPは以下となっている。
```sh
$ kubectl get pod -n tsm-test2 -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP             NODE                                           NOMINATED NODE   READINESS GATES
centos      2/2     Running   0          4h26m   100.96.2.234   imurata-wdc-tsm2-md-0-6jww9-5d554f5cc7-kf5kv   <none>           <none>
ipchecker   2/2     Running   0          13m     100.96.1.130   imurata-wdc-tsm2-md-0-6jww9-5d554f5cc7-99fqb   <none>           <none>
$ kubectl get pod -n tsm-test3 -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP             NODE                                           NOMINATED NODE   READINESS GATES
ipchecker   2/2     Running   0          3m56s   100.96.2.243   imurata-wdc-tsm2-md-0-6jww9-5d554f5cc7-kf5kv   <none>           <none>
```

この状態では`ipchecker.tanzu.tkg`がそれぞれのNamespaceに存在しているが、この状態でアクセスすると以下のような結果となった。
```sh
$ kubectl exec -it centos -- curl ipchecker.tanzu.tkg
Client IP: 127.0.0.6
Server IP: 100.96.2.243
$ kubectl exec -it centos -- curl ipchecker.tanzu.tkg
Client IP: 127.0.0.6
Server IP: 100.96.1.130
```



これにより、同一クラスタ内別Namespace名間アクセスでは以下が確認できた。
- Namespaceを意識せずにサービスにアクセス出来る
- 同名のサービスがNamespaceを跨いで存在する場合、分散される

なお、`ServiceEntry`オブジェクト内では、それぞれのNamespaceに対するアクセスの重さが均一に定義される。
```sh
$ kubectl get se nsxsm.gns.red.ipchecker
NAME                      HOSTS                     LOCATION        RESOLUTION   AGE
nsxsm.gns.red.ipchecker   ["ipchecker.tanzu.tkg"]   MESH_INTERNAL   DNS          23h
```
```yaml
$ kubectl get se nsxsm.gns.red.ipchecker -o yaml
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
：（省略）
  endpoints:
  - address: ipchecker.tsm-test2
    ports:
      http1: 80
    weight: 2147483647
  - address: ipchecker.tsm-test3
    ports:
      http1: 80
    weight: 2147483647
```


## まとめ
GNSを使うと、`<サービス名>.<GNSで定義したドメイン名>`でアクセスできるため、Namespaceを意識せずにアプリケーション間通信を行うことが出来る。
また、同名のサービスがいれば分散するため、クラスタ間で同名のサービスを用意しておけば、冗長性を持たせることも出来る。
マイクロサービスでシステムを構築する場合にはかなり使えそうだと思われる。

## 付録：Tanzu Service Meshのセットアップ

### 要件
#### システム要件
[こちら](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/tanzu-service-mesh-environment-requirements-and-supported-platforms/GUID-D0B939BE-474E-4075-9A65-3D72B5B9F237.html)によるとWorkloadクラスタの要件は以下となる。
|コンポーネント|要件|
|---|---|
|Node|3台、4CPU、6GBメモリ|
|Daemonset|各ノードで稼働するDaemonset用に0.25CPUと650MBメモリ|
|Ephemeral ストレージ|クラスタ全体で24GB、各ノードごとに1GB|
|Pod|クラスタ全体で30個、各ノードごとに3個|

サポートしているKubernetesは[こちら](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/tanzu-service-mesh-environment-requirements-and-supported-platforms/GUID-D0B939BE-474E-4075-9A65-3D72B5B9F237.html#supported-platforms-1)。
各ディストリビューションの最新バージョンが含まれていないように見える点に注意。

#### ネットワーク要件
- クラスタ間で通信させる場合はクラスタ間でネットワークのreachabilityが必要
- クラスタでCNI、RBACが利用可能
- クラスタ上のTSMコンポーネントからSaaSおよびECRに接続出来る必要あり。
- `Type: LoadBalancer`が使える状態になっている\
※NodePortはサポートされない

### クラスタの登録（Onboarding）

TSMでクラスタを管理するために、TSMにクラスタを登録してAgentをクラスタ内にインストールすることをOnboardingと呼ぶ。
公式ドキュメントは[このあたり](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/getting-started-guide/GUID-DE9746FD-8369-4B1E-922C-67CF4FB22D21.html)。
TSMにアクセスし、左サイドバーの`NEW WORKFLOW`をクリックし、`Onboard New Cluster`をクリックする。
![1669703547750.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/a4c9b102-f78c-5877-b017-1503b17feccd.png)
すると以下のような入力フォームが現れる。
![1691122338052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/33e96be5-7c28-fb16-20fe-f155536de8dc.png)
ここでの設定項目は以下となる。
- `Enter cluster name`: クラスタ名を入力する
- `Cluster Labels`: TSM内で識別するためのラベルを割り当てる
- `Cluster admin owned`: チェックするとTSM側でNamespaceを管理しなくなる

最初にTSM上でクラスタを識別するための名前を入力する。これはTKG上のクラスタ名と同じである必要はない。
ハイフン以外の記号や大文字は利用できないので注意。
`Cluster Labels`に関しては、[プロキシを利用してNSX ALBに繋ぐ場合は設定が必要](https://docs.vmware.com/en/VMware-Tanzu-Service-Mesh/services/configure-avi-gslb-integration/GUID-B6F32148-9AAF-4015-B094-B567604A4EF8.html#procedure-2)だが、今回は利用しないためスキップする。
`Cluster admin owned`はチェックするとTSM側でIstioサイドカーを挿入するNamespaceを制御しなくなる。
ユーザー側でNamespaceに`istio-injection=enabled`というラベルを付与した場合にのみ、そのNamespaceで作成したPodに対し、Istioサイドカーが挿入される。
`Integrations`に関しては、mTLSなどで利用する証明書を統合する場合に変更する。今回はTSMの自己署名証明書を利用するので変更しない。
`GENERATE SECURITY TOKEN`をクリックすると、yamlのapplyを促す画面に遷移する。
![1677136447273.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/4df77b81-fe69-8b4f-ef49-2c946cb68b50.png)
表示されているコマンドをコピーし、クラスタで実行する。
1番目のコマンドを実行すると、以下のようにTSMのためのカスタムリソースやロール等のリソースがデプロイされる。
```
namespace/vmware-system-tsm created
customresourcedefinition.apiextensions.k8s.io/aspclusters.allspark.vmware.com created
customresourcedefinition.apiextensions.k8s.io/clusters.client.cluster.tsm.tanzu.vmware.com created
customresourcedefinition.apiextensions.k8s.io/tsmclusters.tsm.vmware.com created
customresourcedefinition.apiextensions.k8s.io/clusterhealths.client.cluster.tsm.tanzu.vmware.com created
configmap/tsm-agent-operator created
serviceaccount/tsm-agent-operator-deployer created
clusterrole.rbac.authorization.k8s.io/tsm-agent-operator-cluster-role created
role.rbac.authorization.k8s.io/vmware-system-tsm-namespace-admin-role created
clusterrolebinding.rbac.authorization.k8s.io/tsm-agent-operator-crb created
rolebinding.rbac.authorization.k8s.io/tsm-agent-operator-rb created
deployment.apps/tsm-agent-operator created
job.batch/update-scc-job created
```
2番目のコマンドを実行するとTSMと接続するためのTokenがSecretとしてクラスタに作成される。
なお、ここのSecretの作成前についうっかりTMCからIntegrationをしてたりすると既にSecretがあって作成に失敗する。
TMCからのIntegrationとはバッティングしないようにすること。
暫く待つと、以下のように画面が推移して、TSMをインストールする画面が出てくる。
![1677136761835.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/5cf0bd05-a9d5-5b16-d886-67d3b6759f4a.png)
TSMの対象のNamespaceを聞いているので、除外するNamespaceがあればそれを指定する。
今回は全Namespaceを対象としてデプロイする。
`INSTALL TANZU SERVICE MESH`をクリックすると、クラスタに以下のようにジョブが走ってクラスタとの接続が行われる。
```
$ kubectl get job -n vmware-system-tmc
NAME                                                COMPLETIONS   DURATION   AGE
agentupdater-workload-27952283                      0/1           9s         9s
```
ちなみにvSphere with TanzuだとPSPの設定が適切ではないと以下のような感じでエラーが出てReplicaSetの作成が終わらない。
```
$ kubectl get event -n vmware-system-tsm
LAST SEEN   TYPE      REASON         OBJECT                            MESSAGE
2m42s       Warning   FailedCreate   job/check-client-crds             Error creating: pods "check-client-crds--1-" is forbidden: PodSecurityPolicy: unable to admit pod: []
4m5s        Warning   FailedCreate   replicaset/policy-d58dbf556       Error creating: pods "policy-d58dbf556-" is forbidden: PodSecurityPolicy: unable to admit pod: []
4m5s        Warning   FailedCreate   replicaset/telemetry-86c497947d   Error creating: pods "telemetry-86c497947d-" is forbidden: PodSecurityPolicy: unable to admit pod: []
4m45s       Normal    Synced         tsmcluster/tsm-client-cluster     TsmCluster synced successfully
```
```
$ kubectl get rs
NAME                             DESIRED   CURRENT   READY   AGE
allspark-ws-proxy-6cf5859b4c     1         1         1       26h
config-service-74587c9cfd        1         1         1       26h
k8s-cluster-manager-79df485f94   1         1         1       26h
policy-d58dbf556                 1         0         0       26h
telemetry-86c497947d             1         0         0       26h
tsm-agent-operator-d866f497b     1         1         1       26h
```
適切にPSPを設定するか、面倒なら以下のように一律で権限付与してあげる必要がある。
```
kubectl create clusterrolebinding default-tkg-admin-privileged-binding --clusterrole=psp:vmware-system-privileged --group=system:authenticated
```
インストールが終わると、以下のようにHome画面からクラスタを確認することが出来る。
![1691472294222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/2050f1f2-5ae4-a0c8-6d90-a9eb26197ca7.png)