IstioのKubernetes上におけるTraffic Managementの仕組みを理解する

Istioを本番システムで利用する場合は、Istioの機能がどのように実現されているかを理解していないと、いざという時トラブルシュートができません。そこで、Istioを勉強するにあたって、IstioのTraffic ManagementがKubernetes上でどのように実現されているかをドキュメントを見たり、kubectlでリバースエンジニアリングしながら調べてみました。

- 想定読者
    - IstioがTraffic ManagementをKubernetes上でどのように実現するか理解したい人
    - Kubernetesの基礎は理解している
        - Service、Podなどの基本的なリソースの説明はしません

- 前提
    - 外部からmesh内サービスへのアクセスまでを対象とし、mesh内から外部への通信については触れない
        - ServiceEntryについては触れません、いつかもっと理解したら載せるかもです
        
    - Istioのサンプルアプリ[Istio / Bookinfo Application](https://istio.io/docs/examples/bookinfo/)を題材とする
        - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230552/2454c655-04f9-19f7-0e8e-c4d907771c66.png)



# IstioのTraffic Managementを実現するリソースの理解

参考：[Istio / Traffic Management](https://istio.io/docs/concepts/traffic-management/#rule-configuration)

本記事ではIstioのTraffic にManagementを実現する以下3つのCRD（Custom Resource Definition）について説明をする。

- VirtualService
- DestinationRule
- Gateway

各リソースを用いてBookinfoにおけるIstioのリクエストフローを表現すると、以下のようになる。Kubernetes上で具体的にどのように実現されているかは後述する。あくまでも、リソースを理解するうえでのイメージ図であることに注意。

![スクリーンショット 2019-04-28 17.48.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230552/b72bca05-a214-62dd-cd6c-9c7170ea2459.png)




## Gateway
- [Istio / Gateway](https://istio.io/docs/reference/config/networking/v1alpha3/gateway/)
- mesh外からのリクエスト受付窓口
- exposeするポート、プロトコル、SNIの設定情報を記載する
- GatewayとVirtualServiceの紐付けはVirtualServiceで行う

## VirtualService
- [Istio / Virtual Service](https://istio.io/docs/reference/config/networking/v1alpha3/virtual-service/)
- mesh上のリクエストのルーティング設定を行う
    - DestinationRule（後述）でより細かいルーティング設定ができる
- bookinfoではまずbookinfo VirtualServiceでGatewayへのアクセスをproductpage VirtualServiceへルーティングしている
    - 次にproductpage VirtualServiceの設定によりproductpage Podにリクエストがルーティングされる

```bookinfo-virtualservice.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
  namespace: ""
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 # <= 上記でmatchしたリクエストをproductpageにルーティングする
        port:
          number: 9080
```

```productpage-viertualservice.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage
  namespace: ""
  resourceVersion: ""
spec:
  hosts:
  - productpage   <= productpageへのリクエストを受けるVirtualService
  http:
  - route:
    - destination:
        host: productpage    <= productpageに
        subset: v1           <= subset: v1というDestinationRuleでルーティングを行う
```

## DestinationRule
- [Istio / Destination Rule](https://istio.io/docs/reference/config/networking/v1alpha3/destination-rule/)
- VirtualServiceがPodにリクエストをルーティングする際の細かいポリシーなどを設定する
- バージョンごとのルーティングや、負荷分散のポリシーなどを設定できる

```productpage-destinatnionrule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
  namespace: default
spec:
  host: productpage      <= productpageへ来たリクエストに対する細かいルールを設定
  subsets:
  - labels:
      version: v1
    name: v1             <= version: v1のラベルへルーティングするsubsetをv1という名前で定義している
```

上記productpageは単一バージョンしかないが、reviewsは複数バージョンがあるため、複数のsubsetが定義されている。

```reviews-destinationrule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
  namespace: default
spec:
  host: reviews
  subsets:
  - labels:
      version: v1
    name: v1
  - labels:
      version: v2
    name: v2
  - labels:
      version: v3
    name: v3
```


# Kubernetes上での実現方法を理解する

IstioのTraffic ManagementがKubernetesでどのように実現されるかを説明する。
（参考：[Istio / Traffic Management](https://istio.io/docs/concepts/traffic-management/#ingress-and-egress)）

![スクリーンショット 2019-04-29 8.21.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230552/aca3929f-2972-a4a9-13c8-6ab74e67782e.png)



## mesh外からのリクエストルーティング
- istio-ingressgateway ServiceからProxyコンテナに入ってきたリクエストは、各Pod内にsidecarとして起動しているProxyコンテナを介してリクエストのやりとりを行う
- ProxyコンテナはIstioのPilotコンポーネントが作成したルーティングルールにしたがってリクエストをルーティングする

## istio-ingressgateway Service
- Istio install時にデフォルトでinstallされている
- KubernetesのServiceリソース（type LoadBalancer）で作成されている
    - EKSなどで作成すると、ELBが自動的に作成される
    - 外部からのリクエストを受け付ける窓口となる     
        - Gatewayリソース作成時に紐付けをする
- 宛先にはProxyコンテナが指定されている
    - 外部からのアクセスはこの仕組みにより最初のProxyコンテナにルーティングされ、ルーティング設定にしたがってmesh上の各Podにルーティングされていく

## IstioのCRDどこいった？
お気づきかと思うが、上記の説明ではGatewayやVirtualService、DestinationRuleがでてこない。これらのリソースはPilotが作成するProxyコンテナのルーティングルールの作成に利用されている。それぞれの設定はPilotによりルーティングルールとして全Proxyコンテナに設定される。

## Serviceリソースをどこに使ってるんだ？
通信は全てProxyコンテナ間で行われ、各ProxyコンテナはPilotによって作成されたルーティングルールに沿って通信を行う。つまりProxyコンテナは直接はKubernetesのServiceリソースによる名前解決を利用していない。ではどこで利用しているかというと、Pilotがルーティングルールを作成する際に利用している。（参考：[Istio / Traffic Management](https://istio.io/docs/concepts/traffic-management/#pilot-and-envoy)）

実際にServiceの定義を見てみるとselectorには `app: reviews` しか指定されていない。

```reviews-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: reviews
    service: reviews
  name: reviews
spec:
  ports:
  - name: http
    port: 9080
    protocol: TCP
    targetPort: 9080
  selector:
    app: reviews
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}
```

以下のようにDeploymentを確認すると、reviewsの全てのバージョン(V1~V3）に `app: reviews` ラベルが貼られている。つまり、reviews Serviceで名前解決をすると全てのバージョン(V1~V3）の宛先が返ってくるはずである。


```
$ k get deploy -o wide                                                                                                                                                       
NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS    IMAGES                                          SELECTOR
details-v1       1         1         1            1           5h        details       istio/examples-bookinfo-details-v1:1.10.1       app=details,version=v1
productpage-v1   1         1         1            1           5h        productpage   istio/examples-bookinfo-productpage-v1:1.10.1   app=productpage,version=v1
ratings-v1       1         1         1            1           5h        ratings       istio/examples-bookinfo-ratings-v1:1.10.1       app=ratings,version=v1
reviews-v1       1         1         1            1           5h        reviews       istio/examples-bookinfo-reviews-v1:1.10.1       app=reviews,version=v1
reviews-v2       1         1         1            1           5h        reviews       istio/examples-bookinfo-reviews-v2:1.10.1       app=reviews,version=v2
reviews-v3       1         1         1            1           5h        reviews       istio/examples-bookinfo-reviews-v3:1.10.1       app=reviews,version=v3
```

上記のようなServiceの設定を見てもわかるように、Proxyコンテナ自体はServiceリソースを直接は用いておらず、Pilotがルーティングルールを作成するために利用していることがわかる。


## （参考）Pod上のAPコードの変更なしでリクエストがProxyを通るようにする仕組み

<details><summary>内容</summary><div>

IstioではAPの変更なしで、PodがProxyコンテナを通すようにすることができる。PodからのリクエストをどのようにProxyコンテナを通すようにするか気になったので少し調べてみた。結論は以下。

- PodからのリクエストはよしなにProxyコンテナを仲介するように設定される
    - たぶん自動injectionをtrueにすれば勝手にやってくれる

Podのyamlを見てみるとProxyコンテナがリクエストを奪い取るっぽい設定がされたいる。

以下にproductpageの元のDeploymentのyamlと実際にデプロイされていたpodのyamlを記載する。

```productpage-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: productpage-v1
  labels:
    app: productpage
    version: v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: productpage
        version: v1
    spec:
      containers:
      - name: productpage
        image: istio/examples-bookinfo-productpage-v1:1.10.1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9080
```        

```product-pod.yaml
$ k get po productpage-v1-5cb458d74f-wnlf8 -o yaml                                                                                                                           [~/SDropbox/Dropbox/k8s/istio]
apiVersion: v1
kind: Pod
metadata:
  annotations:
    sidecar.istio.io/status: '{"version":"db2f6e5a0d19dde20f8dc886fe87dbb029dd3d2c3f77bfc11196d156ca5f8069","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
  creationTimestamp: 2019-04-28T00:50:50Z
  generateName: productpage-v1-5cb458d74f-
  labels:
    app: productpage
    pod-template-hash: "1760148309"
    version: v1
  name: productpage-v1-5cb458d74f-wnlf8
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: productpage-v1-5cb458d74f
    uid: aaf0a34b-694f-11e9-89ac-067d8b9d0d08
  resourceVersion: "4680"
  selfLink: /api/v1/namespaces/default/pods/productpage-v1-5cb458d74f-wnlf8
  uid: aaf1f726-694f-11e9-89ac-067d8b9d0d08
spec:
  containers:
  - image: istio/examples-bookinfo-productpage-v1:1.10.1
    imagePullPolicy: IfNotPresent
    name: productpage
    ports:
    - containerPort: 9080
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-c5qk5
      readOnly: true
  - args:
    - proxy
    - sidecar
    - --domain
    - $(POD_NAMESPACE).svc.cluster.local
    - --configPath
    - /etc/istio/proxy
    - --binaryPath
    - /usr/local/bin/envoy
    - --serviceCluster
    - productpage.$(POD_NAMESPACE)
    - --drainDuration
    - 45s
    - --parentShutdownDuration
    - 1m0s
    - --discoveryAddress
    - istio-pilot.istio-system:15010
    - --zipkinAddress
    - zipkin.istio-system:9411
    - --connectTimeout
    - 10s
    - --proxyAdminPort
    - "15000"
    - --concurrency
    - "2"
    - --controlPlaneAuthPolicy
    - NONE
    - --statusPort
    - "15020"
    - --applicationPorts
    - "9080"
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.namespace
    - name: INSTANCE_IP
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: status.podIP
    - name: ISTIO_META_POD_NAME
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.name
    - name: ISTIO_META_CONFIG_NAMESPACE
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.namespace
    - name: ISTIO_META_INTERCEPTION_MODE
      value: REDIRECT
    - name: ISTIO_METAJSON_LABELS
      value: |
        {"app":"productpage","pod-template-hash":"1760148309","version":"v1"}
    image: gcr.io/istio-release/proxyv2:release-1.1-latest-daily
    imagePullPolicy: IfNotPresent
    name: istio-proxy
    ports:
    - containerPort: 15090
      name: http-envoy-prom
      protocol: TCP
    readinessProbe:
      failureThreshold: 30
      httpGet:
        path: /healthz/ready
        port: 15020
        scheme: HTTP
      initialDelaySeconds: 1
      periodSeconds: 2
      successThreshold: 1
      timeoutSeconds: 1
    resources:
      limits:
        cpu: "2"
        memory: 128Mi
      requests:
        cpu: 100m
        memory: 128Mi
    securityContext:
      readOnlyRootFilesystem: true
      runAsUser: 1337
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /etc/istio/proxy
      name: istio-envoy
    - mountPath: /etc/certs/
      name: istio-certs
      readOnly: true
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-c5qk5
      readOnly: true
  dnsPolicy: ClusterFirst
  initContainers:
  - args:
    - -p
    - "15001"
    - -u
    - "1337"
    - -m
    - REDIRECT
    - -i
    - '*'
    - -x
    - ""
    - -b
    - "9080"
    - -d
    - "15020"
    image: gcr.io/istio-release/proxy_init:release-1.1-latest-daily
    imagePullPolicy: IfNotPresent
    name: istio-init
    resources:
      limits:
        cpu: 100m
        memory: 50Mi
      requests:
        cpu: 10m
        memory: 10Mi
    securityContext:
      capabilities:
        add:
        - NET_ADMIN
      runAsNonRoot: false
      runAsUser: 0
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
  nodeName: ip-192-168-20-207.us-west-2.compute.internal
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: default-token-c5qk5
    secret:
      defaultMode: 420
      secretName: default-token-c5qk5
  - emptyDir:
      medium: Memory
    name: istio-envoy
  - name: istio-certs
    secret:
      defaultMode: 420
      optional: true
      secretName: istio.default
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: 2019-04-28T00:50:53Z
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: 2019-04-28T00:51:03Z
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: 2019-04-28T00:50:50Z
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: docker://ff9bcd670cfdf0ebb511e2a354f275eccdfd87adb0e08a62c3b4ea7634271519
    image: gcr.io/istio-release/proxyv2:release-1.1-latest-daily
    imageID: docker-pullable://gcr.io/istio-release/proxyv2@sha256:01d50d0390058d52197727499dcad3f0ccc6f65e0cf6a82ddf85bd0083341dfe
    lastState: {}
    name: istio-proxy
    ready: true
    restartCount: 0
    state:
      running:
        startedAt: 2019-04-28T00:51:01Z
  - containerID: docker://5786b8460526512f321dba0f5317c6a474f319b8c63c2d22ed5e582840af3197
    image: istio/examples-bookinfo-productpage-v1:1.10.1
    imageID: docker-pullable://istio/examples-bookinfo-productpage-v1@sha256:a427e10277a814b27c066d9ea5f2605b656fc1948714ed09e55c97342c5a721d
    lastState: {}
    name: productpage
    ready: true
    restartCount: 0
    state:
      running:
        startedAt: 2019-04-28T00:51:00Z
  hostIP: 192.168.20.207
  initContainerStatuses:
  - containerID: docker://49956b55bed3950f0a4a3c35207994c4ad1925ea82e304a870533db82073825f
    image: gcr.io/istio-release/proxy_init:release-1.1-latest-daily
    imageID: docker-pullable://gcr.io/istio-release/proxy_init@sha256:30e629a68500f45b73dd7258cf86c6c1c20920bdd5d92eb6a26aacb6d1c54739
    lastState: {}
    name: istio-init
    ready: true
    restartCount: 0
    state:
      terminated:
        containerID: docker://49956b55bed3950f0a4a3c35207994c4ad1925ea82e304a870533db82073825f
        exitCode: 0
        finishedAt: 2019-04-28T00:50:53Z
        reason: Completed
        startedAt: 2019-04-28T00:50:52Z
  phase: Running
  podIP: 192.168.3.132
  qosClass: Burstable
  startTime: 2019-04-28T00:50:50Z
```

istio-proxyとistio-initが追記されていることがわかりますね。
こいつらがよしなにやってくれているのだろう。

</div></details>



# まとめ
IstioのTrafficがどのようにKubernetes上で実現されるかを説明しました。