Kubernetes/Istioの機能を利用したクラスタ外からのアクセス方法を比較する

# はじめに
Istioは、マイクロサービスにおけるサービスメッシュ機能（トラフィック制御・セキュア化・可視化など）を提供する。今回、Kubernetesクラスタ上でIstioのサンプルアプリケーションを試してみた際に、Kubernetesクラスタ外部からのアクセスの仕方で混乱した。

マイクロサービス間のトラフィック制御については、いくつか記事が書かれていたものの、クラスタ外部からのルーティングに主眼に置いた記事は少なかったため、通常のKubernetesを利用した場合との比較をまとめておく。

なお、本記事はある程度Kubernetesと各リソースの概念を理解していることを前提とする。また、Istioについては学習中の状態なので、誤解している箇所があれば指摘して頂けると幸いである。

# TL;DR

- Istioを利用する場合、 `Gateway`、`VirtualService`、`DestinationRule`などのCRDを利用することで、Kubernetesの`Ingress`リソースを用いた場合より柔軟なL7ルーティングを実現できる。
- `istio-ingressgateway`が、`Gateway`、`VirtualService`、`DestinationRule`を監視し、メッシュ内の各マイクロサービスへのルーティングを行う

# 環境
本記事の実行環境は以下の通り。Docker for desktopを利用しているため、GKEやEKSなどのマネージドKubernetesサービスを利用した場合とは異なる場合があるので注意。

- MacOS
- Docker for desktop
- Kubernetes v1.13.5
- Istio v1.1


# Kubernetesが提供する外部アクセス方法
まず、Istioを利用せず、通常のKubernetesの機能を利用した場合の外部アクセス方法について簡単に述べる。Kubernetesでは、IPとポート番号によるL4ルーティングの手段と、パスベースのL7ルーティングの機能が提供されている。

## L4ルーティング
最も用いられるのが、`Service`リソースの`Type:NodePort`、`Type:LoadBalancer`を利用する方法である。

### Type:NodePortを用いる場合
`Type:NodePort`では、Kubernetesの各ノードの特定のポートを通じて、`Pod`にアクセスする。外部に公開したいワークロード（`Deployment`、`Pod`など）を作成したあと、`Type:NodePort`の`Service`を作成することで、Kubernetesクラスタの各ノードの特定ポートでアクセスを受け付けるようになる（Kubernetesのシステムコンポーネントである`kube-proxy`が、ノード上のポートでLISTENし、アクセスを受け付けるとiptablesのルールにより`Pod`へフォワーディングされる）。

### Type:LoadBalancerを用いる場合
`Type:LoadBalancer`の`Service`を作成すると、`Type:NodePort`の場合と同様に、各ノードでアクセスを受け付けるノードポートが公開される。これに加え、クラスタ外部にグローバルIPが付与されたロードバランサが作成され、ユーザーはこのロードバランサへアクセスすると、各ノードポートへ転送されたのち、Podへ到達する。つまり、外部に公開されたロードバランサと、Kubernetesの`Service`により２段階のロードバランシングを経てアクセスすることになる。

なお、`Type:LoadBalancer`の場合でも、直接ノードポートにアクセスし、`Pod`と通信することが可能である。

### L4ルーティングの問題点
- 外部公開する`Service`ごとにエンドポイントが必要になる
    - 管理が手間
    - セキュリティ上、公開するポートは少なくしたい
- L4レイヤでルーティングを行うため、エッジでのSSL終端が行えない


## Ingressを用いたL7ルーティング
### Ingressの概要
`Ingress`リソースを利用することで、KubernetesでL7ルーティングを実現することができる。通常、`Ingress`リソースを作成するためには、`Pod`へ通信を転送するためのルールを、YAML形式のマニフェストファイルを用いて記述する。以下より、マニフェストファイルの例を以下に示す。

```yaml:apache-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /apache
        backend:
          serviceName: apache
          servicePort: 80
```

上記のマニフェストを作成後、`Ingress`リソースを作成する。

```shell
kubectl apply -f apache-ingress.yaml
ingress.extensions/apache-ingress created
```

次に、順序が逆転するようではあるが、転送先となるApacheServerの`Deployment`と`Service`リソースを作成するためのマニフェストファイルを記述する。

```yaml:apache-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apache
spec:
  replicas: 3
  selector:
    matchLabels:
      app: apache
  template:
    metadata:
      labels:
        app: apache
    spec:
      containers:
      - image: httpd
        name: apache
---
apiVersion: v1
kind: Service
metadata:
  name: apache
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: apache
```

作成したマニフェストファイルを用いてデプロイする。

```shell
❯ kubectl apply -f apache-deployment.yaml
deployment.apps/apache created
service/apache created
```

以上で`Ingress`リソースの作成は完了したが、`Ingress`リソースを作成するだけでは、外部から`Pod`にアクセスすることはできない。

`Ingress`リソースは、あくまで転送ルールを指定するためのものであり、実際に転送の処理を行う仕組みが存在しないためである。外部にサービスを公開するためには、`Ingress`リソースで定義したルールをもとに、実際の転送処理を行う`ingress-controller`が必要である。

### ingress-controller
`ingress-controller`は、`Ingress`リソースのルールに基づき転送の処理を行う実体（`Pod`）を表すもので、その実装はいくつか存在する。最も用いられているのは、nginxに転送処理を行わせる`nginx-ingress-controller`である。

以下、`nginx-ingress-controller`をクラスタにデプロイする。


```shell
# nginx-ingress-controllerのデプロイ
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
namespace/ingress-nginx configured
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount created
clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role created
rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created

# 動作確認
$ kubectl get po -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-78474696b4-zjjd5   1/1     Running   0          1m

# Serviceの作成
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/cloud-generic.yaml
service/ingress-nginx created

# 確認
$ kubectl get svc -n ingress-nginx
NAME            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx   LoadBalancer   10.98.16.250   localhost     80:32326/TCP,443:30979/TCP   7m

```

以上の手順で、クラスタ外部から`nginx-ingress-controller`を経由して、Apache Serverへアクセス可能になったので、実際に試してみる。

```sh
> curl http://localhost:80/apache
<html><body><h1>It works!</h1></body></html>
```

ブラウザからもアクセスしてみる。
<img width="898" alt="スクリーンショット 2019-05-14 20.15.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/d019e4dd-a221-1159-56e5-58597021489f.png">

以上で、Apache Serverに接続できることを確認した。

上記で説明した`Ingress`リソースを利用したルーティング方法はやや煩雑かつ、istioの機能を利用した場合との差異を理解するために必要なので、下図にその概略を示した。

<img width="1106" alt="スクリーンショット 2019-05-15 0.43.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/c94f806f-af63-f70d-704e-2e6f21a3706c.png">


# Istioが提供する外部アクセス方法
Istioでは、各`Pod`にEnvoyで実装されたサイドカーコンテナを組み込むことで、APから透過的なサービスメッシュを構成できる。Istioのサービスメッシュ内では、`Gateway`、`VirtualService`、`DestinationRule`などのCRDを利用することで、`Ingress`を用いる場合より、柔軟なルーティングが可能である。

クラスタ外からメッシュ内のマイクロサービスにアクセスする場合の基本的な考え方は、`Ingress`を用いる場合と類似しており、定義したルールをもとに、`istio-ingressgateway`と呼ばれる`Pod`が転送処理を行う仕組みになっている。

この転送するルールを定義するのが、`Gateway`、`VirtualService`、`DestinationRule`の３種の`Custom Resource Definition`(CRD)である。

本稿では、Istioが公式で提供しているbookinfoというサンプルアプリケーションを利用する場合を例にとって説明していく。今回は、Istioのインストール、bookinfoのデプロイ手段は[Istioの公式ドキュメント](https://istio.io/docs/setup/kubernetes/)を参照されたい。

bookinfoの構成は以下の通りである。この例では、外部からproductpageサービスにアクセスする。この際、productpageサービスが、バックエンドの３種のいずれかのReviewsサービスを呼び出し、Ratingの情報を取得している。

<img width="763" alt="スクリーンショット 2019-05-14 22.10.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/8e4c64b6-769d-67ef-a59a-ff28f52f9bbd.png">


## 各CRDの説明
クラスタ外からのアクセス方法を説明するため、`Gateway`、`VirtualService`、`DestinationRole`の概要を簡単に紹介する。詳細は、Istioの公式ドキュメントなどを参照されたい。

### Gateway
少々ややこしいが、サービスメッシュのエッジで、実際の転送処理を行う`istio-ingressgateway`を外部に公開するためのがロードバランサーが、リクエストを受け付けるポート、プロトコル、FQDN名を指定するリソースである。

パスや、パスに基づく転送先は、`Gateway`リソースでなく、`VirtualService`リソースで指定する。以下のマニフェストファイルでは、`istio-ingressgateway`が80番ポート、HTTPプロトコル、任意のFQDN名のリクエストを受け付ける設定を定義している。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80 # ポート番号
      name: http
      protocol: HTTP # プロトコル
    hosts:
    - "*" # FQDN名（任意）
```

### VirtualService
その名の通り、`Service`リソースに関連するリソースであり、Istioのサービスメッシュに対するリクエストのルーティング方法を定義するリソースである。１つの`VirtualService`に対して１つ以上の`Service`リソースが紐づけられ、各`Service`に対して、パスベースでルーティングを行う。（※パスだけでなく、HTTPヘッダの値による振り分けなども可能。）

またメッシュ間通信でのリトライ処理、フォールトインジェクション、カナリアリリースなどの設定は、`VirtualService`にて指定することができる。

以下の例では、パスが/productpageから始まるリクエストを、productpageという名前の`Service`へルーティングする。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - bookinfo-gateway # Gatewayリソースの指定
  http:
  - match:
    - uri:
        exact: /productpage # パスの指定
    route:
    - destination:
        host: productpage # 転送Serviceの指定
        port:
          number: 9080
```

### DestinationRule
`DestinationRule`は、`VirtualService`のルールにより、振り分ける`Service`が決定したあと、その`Service`に対する追加のポリシーを定義するリソースである。`Pod`への振り分けのアルゴリズム（ラウンドロビン方式、ランダム方式、コネクション数など）、TLS認証、サーキットブレイカー、keepaliveの時間など、さまざまなポリシーを付加することができる。

以下の例では、version:v1の`label`が付与されたreviewの`Service`に対し、サーキットブレイカーを設定している。このポリシーを作成すると、コネクション数が100になった`Pod`に対しては新たにリクエストを送信されないことが保証される。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      connectionPool:
        tcp:
          maxConnections: 100
```

### ３種のCRDの役割の整理

以上を簡潔にまとめると、`Gateway`と`VirtualService`は、`istio-ingressgateway`が外部からアクセスを受け付けるエンドポイントと振り分け先の`Service`リソースを決定するためのリソースであり、また`DestinationRule`は`Service`から`Pod`へリクエストを振り分けるポリシーを定義するためのリソースであるといえる。

<img width="758" alt="スクリーンショット 2019-05-15 19.38.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/0475d577-40a7-3921-8d8b-6e51241ff346.png">



## ルーティング方法

以上のように、istioを用いて外部からアクセスする場合、３つのCRDを用いて転送ルールを指定する。`istio-ingressgateway`は、この３つのCRDを監視し、そのルールに基づき実際の転送処理を行う`Custom Controller`である。

それでは、３つのWorkerノードを持つクラスタにbookinfoをデプロイした際のルーティング経路を下図に示す。なお、デフォルトでは`istio-ingressgateway`のレプリカ数は1つだが、便宜上レプリカ数を2つとして表記している。

実際に、多数のリクエストが想定される場合は`istio-ingressgateway`をスケールするケースは想定され、それぞれの動作に影響はない。

<img width="1106" alt="スクリーンショット 2019-05-15 0.33.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/b42f729d-c10b-8c80-3c3c-c77cdabed6e5.png">

### 大まかな流れ

- `istio-ingressgateway`のLoadBalancerに到達したリクエストは、各ノードのNodePortへ転送される
- 到達したノードで、iptablesによりポートフォワーディングが行われ、`istio-ingressgateway`の`Pod`に到達する（図ではレプリカ数は2）
- `istio-ingressgateway`の`Pod`は、`Gateway`、`VirtualService`、`DestinationRole`のルールをもとに、`productpage`の`Service`にリクエストを転送する
- `productpage`の`Pod`にリクエストが到達する（図ではレプリカ数は1）

## 実際に試してみる

### bookinfoのデプロイ時に作成されるCRDの確認

作成された`Gateway`では、`istio-ingressgateway`の前段に置かれるロードバランサのIPアドレスと80番ポートに解決される任意のFQDN名を持つリクエストを受け付ける。

ここで、Docker for desktopの場合は、ロードバランサのipはlocalhostなので、FQDN名はlocalhost:80に解決されるものであれば、何でも構わない。また、FQDN名が任意の場合に限り、そのままhttp://localhost:80/<path>としても構わない。（`Spec.Servers.Hosts`にfoo.bar.comのように何らかのFQDN名を指定した場合、このようなリクエストは失敗する点に注意）

```sh
> kubectl describe gateway bookinfo-gateway
~~
Spec:
  Selector:
    Istio:  ingressgateway
  Servers:
    Hosts:
      *
    Port:
      Name:      http
      Number:    80
      Protocol:  HTTP
```

次に、`VirtualService`では、パスが/productpage,/login,/logout,/api/v1/products/*のリクエストをproductpageの`Service`へ転送するルールを定義している。


```sh
~~
> kubectl describe virtualservice bookinfo
Spec:
  Gateways:
    bookinfo-gateway
  Hosts:
    *
  Http:
    Match:
      Uri:
        Exact:  /productpage
      Uri:
        Exact:  /login
      Uri:
        Exact:  /logout
      Uri:
        Prefix:  /api/v1/products
    Route:
      Destination:
        Host:  productpage
        Port:
          Number:  9080
~~
```

以上から、http://localhost:80/productpage に対するリクエストは、productpageの`Service`へ転送されるルールが適用される。

最後に、productpageの`Service`に対する追加のポリシーを確認するため、`DestinationRule`を表示する。

```sh
> kubectl describe destinationrule productpage
~~
Spec:
  Host:  productpage
  Subsets:
    Labels:
      Version:  v1
    Name:       v1
~~
> kubectl get pod --show-labels | grep productpage
productpage-v1-97d4c545d-rpfrh   2/2     Running   0          22h   app=productpage,pod-template-hash=538071018,version=v1
```

`DestinationRule`では、version=v1の`Label`を持つproductpageの`Pod`に転送するルールが記述されているが、今回は該当する`Pod`が1つ存在だけであるため、結果としてルーティング結果には影響しない。

### 動作確認
ブラウザからhttp://localhost:80/productpage にアクセスすると、bookinfoのページが表示される。
<img width="1106" alt="スクリーンショット 2019-05-15 21.14.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/7de02171-2352-40aa-9bc3-c20983aa88a9.png">

同様に、

- http://localhost:80/login
- http://localhost:80/logout 
- http://localhost:80/api/v1/products 

にアクセスすると、productpageの`Pod`に到達し、応答を返す。ただし、http://localhost:80/login, http://localhost:80/logout へのリクエストでは、`Pod`側で処理が実装されていないため、WEBページは返却されない。

### 補足
本題とは少しずれるが、http://localhost:80/productpage へ数回アクセスすると、WEBページ右下のReviewsの部分の表示が毎回変化していることが分かる。これは、productpageの`Pod`がReviewsの`VirtualService`を通じて、毎度異なるReviewsの`Pod`から情報を取得しているためである。

`VirtualService`や`DestinationRule`の定義を変更することで、柔軟なルールで情報を取得するReviewsの`Pod`を制御することができる。情報興味をもった方は、[Istioの公式ドキュメント-Task](https://istio.io/docs/tasks/traffic-management/)

<img width="1106" alt="スクリーンショット 2019-05-15 21.32.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/ac1929d0-1978-d9d1-6add-80cb8870400e.png">

<img width="1106" alt="スクリーンショット 2019-05-15 21.34.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/301312/78e07a03-bc31-455e-6f1a-d78529dc20f1.png">


# まとめ
Kubernetes/Istioの機能を利用してクラスタ外部から`Pod`へアクセスする方法を紹介した。

- Kubernetesの機能
    - Type:NodePort, Type:LoadBalancerの`Service`を利用したL4ルーティング
    - `Ingress`を利用したパスベースのL7ルーティング
        - SSL終端が可能
        - トラフィックの入り口を集約
        - 処理を行う実体は`nginx-ingress-controller`など
- Istioの機能
    - `Gateway`、`VirtualService`、`DestinationRole`のルールに基づくL7ルーティング
        - `Ingress`より柔軟なトラフィック制御が可能
        - 処理を行う実体は`istio-ingressgateway`

今回は、Istioの機能のうち、外部からのアクセスに関わる部分にフォーカスを当てて説明したが、Istioの本領はマイクロサービス間のトラフィック制御、セキュア化、可視化についての機能であり、今後更に注目されていく技術と考えられる。私個人としても、継続的に学習を行っていきたい。

# 参考資料

- [Istio公式ドキュメント](https://istio.io/docs/)
- [Kubernetes公式ドキュメント](https://kubernetes.io/docs/)
- [nginx-ingress-controller公式ドキュメント](https://kubernetes.github.io/ingress-nginx/)
- Kubernetes完全ガイド (impress top gear)　青山真也氏 著
- [API GatewayにIstioを使う](https://qiita.com/rawr/items/b9f01303e67aa0034bca)