Istio によるクラスタ間通信のながれと仕組み

# はじめに
Istio を使うと簡単に mTLS によるセキュアなクラスタ間通信を実現できる。
しかし、Istio に対する理解不足のためデバッグ時に困ることが何度かあったため、この検証時に理解したことを残しておきたい。
本ドキュメントでは Istio 1.8, Kubernetes 1.19 を使って検証・実装している。

# Istio によるマルチクラスタのアーキテクチャ
[公式ドキュメント](https://istio.io/latest/docs/setup/install/multicluster/)にもあるとおり、クラスタの状態によってクラスタ間通信を実現するアーキテクチャが異なる。
主に各クラスタのネットワークと、コントロールプレーンの管理方針による違いによるものである。

クラスタごとにネットワークを分けるか否かは開発プロジェクトの事情によると思われる。ネットワークが分かれている場合、クラスタ間通信のための ingress gateway を作成して通信したいクラスタ向けに公開する必要がある。

コントロールプレーンは管理コストの面を考えると一つのクラスタによせる Primary-Remote 構成にすると良いと思われるが、クラスタ間に依存性が発生して冗長性も失われるデメリットがあることを理解しておく必要がある。

今回は同一クラスタで、各クラスタにコントロールプレーンを立てる下記構成を前提に手順・仕組みを紹介していく。
![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269590/57d58bd0-97b5-d28b-6738-2ac60e571e88.png)

# クラスタ間通信実現の手順
### 1. マルチクラスタ設定を有効にして IstioOperator を install する

meshID, network は各クラスタで同じにする。clusterName はクラスタごとに適当な名前を付ける。この名前が後の手順で作成される secret 内で使われるため、判別可能な名前付けをすると良い。

```yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  values:
    global:
      meshID: mesh1
      multiCluster:
        clusterName: cluster1
      network: network1
...
```

### 2. remote Secret を作り、通信したいクラスタに Secret を登録する
* cluster1 の認証情報を付与した Secret を cluter2 に作成する

```bash
istioctl x create-remote-secret \
    --context="${CTX_CLUSTER1}" \
    kubectl apply -f - --context="${CTX_CLUSTER2}"
```
* cluster2 の認証情報を付与した Secret を cluter1 に作成する

```bash
istioctl x create-remote-secret \
    --context="${CTX_CLUSTER2}" \
    kubectl apply -f - --context="${CTX_CLUSTER1}"
```

※ CTX_CLUSTER1: cluster1 のコンテキスト名、CTX_CLUSTER2: cluster2 のコンテキスト名

### 3. Istio citadel の CA 証明書を更新する
中間証明書(intermediate.crt)、中間キー(intermediate.key)、ルート証明書(root.crt)の3ファイルを用意し、下記コマンドにて各クラスタの CA 証明書を更新する。

```bash
kubectl create secret generic cacerts -n istio-system \
  --from-file=ca-cert.pem=./intermediate.crt \
  --from-file=ca-key.pem=./intermediate.key  \
  --from-file=root-cert.pem=./root.crt       \
  --from-file=cert-chain.pem=./intermediate.crt
```

### 4. Namespace, Service stub を作る
例えば cluster1 にある namespace1, service1 に cluster2 からアクセスしたい場合、cluster2 に namespace1 を作り下記 service を作成する。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: service1
spec:
  ports:
  - protocol: TCP
    port: 8080
```
### 4. remote クラスタに向けて通信する
例えば cluster2 から cluster1 の service1 にアクセスしたい場合、下記を cluster2 の Pod 上で実行する。

```bash
curl http://service1.namespace1.svc.cluster.local:8080/
```

# クラスタ間通信のながれ
cluster2 から cluster1 にある pod にアクセスできるようになるまで、様々なコンポーネントが関連している。
擬態的には、下記図のように7つのステップでクラスタ間通信が実現される。ここで各々のステップで何が行われているかを簡単に紹介する。
![istio-internal-num.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269590/1fe8cadd-3380-8410-f1e9-d0442be8ac91.png)

### リモート呼び出し前
##### ①. citadel -> istio-proxy
citadel から各 Pod に対して SSL 証明書が配布される。
これは cluster1, cluster2 で同じルート認証局のものである必要がある。

##### ②. galley -> kube-apiserver(remote cluster)
remote Secret をもとに remote kube-apiserver にアクセスする。
remote Secret は例えば下記のような中身になっており、 kube-apiserver へのアクセスを可能にしている。

```yaml
apiVersion: v1
  clusters:
  - cluster:
      certificate-authority-data: Sis3WWhtcHNI
      server: https://1.2.3.4
    name: cluster1
  contexts:
  - context:
      cluster: cluster1
      user: cluster1
    name: cluster1
  current-context: cluster1
  kind: Config
  preferences: {}
  users:
  - name: cluster1
    user:
      token: zLmlvL3NlcnZ
```

remote kube-apiserver を source とした config processer が galley 内で実行され、cluster1 のエンドポイントが取得される。

##### ③. galley -> pilot
取得した endpoint を [MCP(Mesh Configuration Protocol)](https://github.com/istio/api/tree/master/mcp)  で pilot に渡す。
MCP は gRPC streaming で通信され、リソースの更新・追加・削除をサブスクリプションしている。

##### ④. pilot -> istio-proxy
istio-proxy 内に pilot-agent が立てられ、pilot からの EDS(Endpoint Service Discovery) 更新の通信を受け取る。

## remote Service 呼び出し
##### ⑤. app -> kube-dns
app は remote Service を service1.namespace1.svc.cluster.local ドメインを使って呼び出しする。
kube-dns は cluster1 に作成された service1 stub をもとに DNS 解決し、仮想IPを app に返す。

##### ⑥. app -> istio-proxy
ドメイン名から istio-proxy に登録された EDS を使って remote Pod IP (Pod1 の IP) への解決を行う。

##### ⑦. istio-proxy -> remote Pod1
istio-proxy は remote Pod IP へリクエストをルーティングする。

### 番外編: istio dns proxy を利用する
Istio 1.8 以降のバージョンを使っている場合、ISTIO_META_DNS_CAPTURE を有効化して install できる。詳細は[こちらの公式ドキュメント](https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/)を参考。
これを適用すると、istio-proxy で DNS 結果がキャッシュされる。このキャッシュされる対象は Service だけでなくカスタムの ServiceEntry (remote kube-apiserver 含む) も含まれるため、remote Service の stub が不要になる。まとめると、下記メリットがある。

- kube-dns への負荷を軽減できる
- Namespace, Service stub の作成が不要になる

# クラスタ間通信のトラブルシューティング
実際に Istio クラスタ間通信を検証した際に実際行ったトラブルシューティング・切り分け方法を残しておく。
これに加え、[公式のトラブルシューティングドキュメント](https://istio.io/latest/docs/ops/diagnostic-tools/multicluster/) も参照すると良い。

### remote secret の適用に失敗している
cluster1 上で service1 を作成した際、 cluster2 上の istiod pod に下記3行のログが出ていなければ remote secret の適用が失敗していると思われる。

```
Handle EDS endpoint: skip collecting workload entry endpoints, service service1/namespace1 has not been populated
ads Incremental push, service service1.namespace1.svc.cluster.local has no endpoints
ads Full push, new service namespace1/service1.namespace1.svc.cluster.local
```

create-remote-secret コマンド実行に間違いがないか、remote cluster の kube-apiserver にアクセス可能かを確認する必要がある。

### Namespace, Service stub を作成し忘れている
下記コマンドを使うと、 Istio による EDS 結果の一覧を取得できる。下記は cluster2 の namespace2 にある pod2 に対する EDS 結果取得のコマンドとなる。

```bash
istioctl proxy-config endpoint pod2.namespace2
```

この結果に cluster1 の service1 が存在していれば、 EDS は成功している。

そのうえで service1 へアクセスに失敗した場合、 Namespace, Service stub の作成漏れ、あるいは Service stub を Headless Service にしているためだと思われる。
clusterIP を付与した service stub を作成する必要がある

- Service stub の spec.ports[] の設定が remote Service と一致していない

### 作成した Service stub の設定が間違っている

remote Secret の適用に問題ないが EDS 結果取得のコマンドで service1 が含まれていない場合、Service stub の spec.ports[] 設定が remote Service と一致していないためと思われる。