Istioを利用してカナリアリリースをやってみた

# はじめに
今回は、サービスメッシュを提供する `Istio` を利用して kubernetes 上カナリアリリースの実験をしてみたいと思います。

Istio やそれに付随する Envoy の詳しい内容は @seikoudoku2000 さんの「[Envoy (Envoy proxy)、Istio とは？](https://qiita.com/seikoudoku2000/items/9d54f910d6f05cbd556d)」を参考にしてみてください。

# 実験
それでは実際に実験してみます。
Istio のインストールはすでに完了しているものとします。

なお、今回テストする環境は下記環境となります。

- OS
    - CentOS Linux release 7.6.1810 (Core)
- kubernetes
    - v1.13.1
    - master:1台
    - node:3台
- Istio
    - v1.0.5

## 今回試すこと
今回は下記のような流れを試してみたいと思います。

1. nginx を利用した Web サービス（今回は静的コンテンツを表示するのみ）を `version 1` としてサービスリリース
2. 静的コンテンツの中身を変更したバージョンを `version 2` としてカナリアリリース
    1. `version 1` に 50% / `version 2` に 50% 振り分けされるように設定をします。

なお、今回は kubernetes のクラスタ内部からのみアクセスをする想定で各種設定をしていきます。
（NodePort や LoadBalancer のを利用する外部公開は行わない）

## 手順
### Web サービス（Version 1）のリリース
では、最初に nginx を利用した Web サービスを作成していきます。

①. nginx の image を展開する Deployment を 作成

```yaml:istio-deployment-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: istio-deployment-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      istio-test-version: v1
  template:
    metadata:
      labels:
        istio-test-app: istio-test-app
        istio-test-version: v1
    spec:
      containers:
        - name: nginx-container
          image: nginx:latest
          ports:
            - containerPort: 80
```
この Deployment では、 `istio-test-version: v1` にマッチした POD を管理します。
`istio-test-app: istio-test-app` については Service にて利用するため事前に設定しておきます。

しかし、このマニュフェストファイルをそのまま apply しても Istio で提供されるサービスメッシュ配下では管理されません。
というのも、最初にご紹介させていただいた @seikoudoku2000 さんの「[Envoy (Envoy proxy)、Istio とは？](https://qiita.com/seikoudoku2000/items/9d54f910d6f05cbd556d)」を参照するとわかりますが、Envoy と呼ばれる Proxy を各 pod にサイドカーとして提供することでサービスメッシュとしての各種コントロールを行います。

Envoy の提供方法は複数ある（指定した namespace 内で作成される pod にはすべて Envoy を提供する設定をする / 既存のマニュフェストファイルに Envoy の定義を追加して apply する）のですが、今回は後者の方法で実施します。

②. `istio-deployment-v1.yaml` に Envoy の定義を追加

```shell
$ istioctl kube-inject -f istio-deployment-v1.yaml > istio-deployment-v1-injection.yaml
```

上記コマンドを実行することで下記ファイルが出力されます。

```yaml:istio-deployment-v1-injection.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  name: istio-deployment-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      istio-test-version: v1
  strategy: {}
  template:
    metadata:
      annotations:
        sidecar.istio.io/status: '{"version":"50128f63e7b050c58e1cdce95b577358054109ad2aff4bc4995158c06924a43b","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
      creationTimestamp: null
      labels:
        istio-test-app: istio-test-app
        istio-test-version: v1
    spec:
      containers:
      - image: nginx:latest
        name: nginx-container
        ports:
        - containerPort: 80
        resources: {}
      - args:
        - proxy
        - sidecar
        - --configPath
        - /etc/istio/proxy
        - --binaryPath
        - /usr/local/bin/envoy
        - --serviceCluster
        - istio-proxy
        - --drainDuration
        - 45s
        - --parentShutdownDuration
        - 1m0s
        - --discoveryAddress
        - istio-pilot.istio-system:15007
        - --discoveryRefreshDelay
        - 1s
        - --zipkinAddress
        - zipkin.istio-system:9411
        - --connectTimeout
        - 10s
        - --proxyAdminPort
        - "15000"
        - --controlPlaneAuthPolicy
        - NONE
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: INSTANCE_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: ISTIO_META_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: ISTIO_META_INTERCEPTION_MODE
          value: REDIRECT
        - name: ISTIO_METAJSON_LABELS
          value: |
            {"istio-test-app":"istio-test-app","istio-test-version":"v1"}
        image: docker.io/istio/proxyv2:1.0.5
        imagePullPolicy: IfNotPresent
        name: istio-proxy
        ports:
        - containerPort: 15090
          name: http-envoy-prom
          protocol: TCP
        resources:
          requests:
            cpu: 10m
        securityContext:
          readOnlyRootFilesystem: true
          runAsUser: 1337
        volumeMounts:
        - mountPath: /etc/istio/proxy
          name: istio-envoy
        - mountPath: /etc/certs/
          name: istio-certs
          readOnly: true
      initContainers:
      - args:
        - -p
        - "15001"
        - -u
        - "1337"
        - -m
        - REDIRECT
        - -i
        - '*'
        - -x
        - ""
        - -b
        - "80"
        - -d
        - ""
        image: docker.io/istio/proxy_init:1.0.5
        imagePullPolicy: IfNotPresent
        name: istio-init
        resources: {}
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
      volumes:
      - emptyDir:
          medium: Memory
        name: istio-envoy
      - name: istio-certs
        secret:
          optional: true
          secretName: istio.default
status: {}
---
```

③. deployment を apply する

```shell
$ kubectl apply -f istio-deployment-v1-injection.yaml

$ kubectl get deployment
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
istio-deployment-v1   1/1     1            1           47h

$ kubectl get pods
NAME                                   READY   STATUS             RESTARTS   AGE
details-v1-8bd954dbb-czq6b             2/2     Running            0          2d23h
```

apply された pod について `READY 2/2` となっていることが確認できます。
これは、上記マニュフェストの通り、Proxy のコンテナがサイドカーとして含まれているためとなります。

また、`version 1` ということで下記 `index.html` を pod 内に配置します。

```shell
$ cat html/v1/index.html
version1 server

$ kubectl cp v1/index.html istio-deployment-v1-7c8c9f47fb-p9fvm:/usr/share/nginx/html
```

④. Service を apply する

```yaml:istio-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: istio-service
spec:
  type: ClusterIP
  ports:
    - name: "http"
      protocol: "TCP"
      port: 80
      targetPort: 80
  selector:
    istio-test-app: istio-test-app
```

今回は `type: ClusterIP` を提供します。
なお、先程説明した通り `istio-test-app: istio-test-app` に当てはまる pod に振り分けを行います。

```shell
$ kubectl get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
istio-service   ClusterIP   10.105.126.251   <none>        80/TCP     44h
```

⑤. Service のトラフィック制御ルールを定義

予め、作成した Service に対してトラフィック制御ルールを定義しておきます。
必要となる `kind` は下記 2 つとなります。

- VirtualService
    - リクエストに対するルーティング制御設定
- DestinationRule
    - ルーティング後にチェックされるポリシー設定

また、クラスタ外部からのアクセスに対してトラフィック制御するには `Gateway` という `kind` を定義する必要がありますが、今回はクラスタ内部からのアクセスをテストするため、今回は省略させていただきます。
詳しい内容は、調べてみてください。

まずは、 `DestinationRule` を定義、apply します。

```yaml:istio-destinationrule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: istio-destinationrule
spec:
  host: istio-service
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      istio-test-version: v1
```

```shell
$ kubectl apply -f istio-destinationrule.yaml
$ kubectl get destinationrule
NAME                    AGE
istio-destinationrule   1d
```

`spec.subsets` リソースで定義されている内容が振り分け先の pod 情報となります。
`istio-test-version: v1` に合致する pod に振り分けされます。

イメージ的に世代が増えたら、このリソースに対して各世代の情報を記載していくイメージになると思います。
また、`spec.trafficPolicy` にて負荷分散方式を指定しております。（デフォルトは RoundRobin のようです。）

次に、`VirtualService` を定義、apply します。

```yaml:istio-virtualservice.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-virtualservice
spec:
  hosts:
  - istio-service
  http:
  - route:
    - destination:
        host: istio-service
        subset: v1
```

```shell
$ kubectl apply -f istio-virtualservice.yaml
$ kubectl get virtualservice
NAME                   AGE
istio-virtualservice   1d
```

`spec.hosts` リソースで定義されている `istio-service` という ホスト名（FQDN でも可）に対する通信に対する設定となります。
`spec.http.route.destination` リソースが定義されているのがルーティング先のルールとなります。
これが、先程定義した `DestinationRule` の `spec.subnets` に対応しています。

⑥. 実際にアクセスしてみる

適当に作成した pod 内部から、作成した Service に対してアクセスしてみます。

```shell
$ curl http://istio-service/
version1 server

...（複数回実行）...

version1 server
```

### Web サービス（Version 2）のリリース
次に、静的コンテンツの中身を変更したものを `version 2` としてデプロイしていきます。

①. nginx の image を展開する Deployment を 作成

```yaml:istio-deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: istio-deployment-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      istio-test-version: v2
  template:
    metadata:
      labels:
        istio-test-app: istio-test-app
        istio-test-version: v2
    spec:
      containers:
        - name: nginx-container
          image: nginx:latest
          ports:
            - containerPort: 80
```

マニュフェストとしては `istio-test-version` label が `v2` に変更している部分程度です。

②. `istio-deployment-v1.yaml` に Envoy の定義を追加

```shell
$ kubectl apply -f istio-deployment-v2-injection.yaml

$ cat istio-deployment-v2-injection.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  name: istio-deployment-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      istio-test-version: v2
  strategy: {}
  template:
    metadata:
      annotations:
        sidecar.istio.io/status: '{"version":"50128f63e7b050c58e1cdce95b577358054109ad2aff4bc4995158c06924a43b","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
      creationTimestamp: null
      labels:
        istio-test-app: istio-test-app
        istio-test-version: v2
    spec:
      containers:
      - image: nginx:latest
        name: nginx-container
        ports:
        - containerPort: 80
        resources: {}
      - args:
        - proxy
        - sidecar
        - --configPath
        - /etc/istio/proxy
        - --binaryPath
        - /usr/local/bin/envoy
        - --serviceCluster
        - istio-proxy
        - --drainDuration
        - 45s
        - --parentShutdownDuration
        - 1m0s
        - --discoveryAddress
        - istio-pilot.istio-system:15007
        - --discoveryRefreshDelay
        - 1s
        - --zipkinAddress
        - zipkin.istio-system:9411
        - --connectTimeout
        - 10s
        - --proxyAdminPort
        - "15000"
        - --controlPlaneAuthPolicy
        - NONE
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: INSTANCE_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: ISTIO_META_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: ISTIO_META_INTERCEPTION_MODE
          value: REDIRECT
        - name: ISTIO_METAJSON_LABELS
          value: |
            {"istio-test-app":"istio-test-app","istio-test-version":"v2"}
        image: docker.io/istio/proxyv2:1.0.5
        imagePullPolicy: IfNotPresent
        name: istio-proxy
        ports:
        - containerPort: 15090
          name: http-envoy-prom
          protocol: TCP
        resources:
          requests:
            cpu: 10m
        securityContext:
          readOnlyRootFilesystem: true
          runAsUser: 1337
        volumeMounts:
        - mountPath: /etc/istio/proxy
          name: istio-envoy
        - mountPath: /etc/certs/
          name: istio-certs
          readOnly: true
      initContainers:
      - args:
        - -p
        - "15001"
        - -u
        - "1337"
        - -m
        - REDIRECT
        - -i
        - '*'
        - -x
        - ""
        - -b
        - "80"
        - -d
        - ""
        image: docker.io/istio/proxy_init:1.0.5
        imagePullPolicy: IfNotPresent
        name: istio-init
        resources: {}
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
      volumes:
      - emptyDir:
          medium: Memory
        name: istio-envoy
      - name: istio-certs
        secret:
          optional: true
          secretName: istio.default
status: {}
---
```

③. deployment を apply する

```shell
$ kubectl apply -f istio-deployment-v2-injection.yaml

$ kubectl get deployment
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
istio-deployment-v1   1/1     1            1           2d
istio-deployment-v2   1/1     1            1           2d
```

これで、カナリアリリースの準備が整ったので肝心のルーティング制御をしていきましょう。

④. Service のトラフィック制御ルールを変更

今回は、`version 1` のサービスに対して `50%` / `version 1` のサービスに対して `50%` の割合で振り分けます。

まずは、`DestinationRule` の定義を変更します。

```yaml:istio-destinationrule.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: istio-destinationrule
spec:
  host: istio-service
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      istio-test-version: v1
  - name: v2
    labels:
      istio-test-version: v2
```

新たに `spec.subsets` リソースに `name: v2` を追加しました。
こちらの内容で apply し直します。

```shell
$ kubectl apply -f istio-destinationrule.yaml

$ kubectl get destinationrule
NAME                    AGE
istio-destinationrule   1d
```

続いて、`VirtualService` の定義を変更します。

```yaml:istio-virtualservice.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-virtualservice
spec:
  hosts:
  - istio-service
  http:
  - route:
    - destination:
        host: istio-service
        subset: v1
      weight: 50
    - destination:
        host: istio-service
        subset: v2
      weight: 50
```

`spec.http.route.detination` リソースを追加し、それぞれの `destination` に対して `weight: 50` を追加してあげました。

こちらの内容で、apply し直します。
これにより、ルーティングの分散が開始されます。

```shell
$ kubectl apply -f istio-virtualservice.yaml

$ kubectl get virtualservice
NAME                   AGE
istio-virtualservice   1d
```

⑤. アクセステスト

適当に作成した pod 内部から、作成した Service に対して再度アクセスしてみます。

```shell:
$ curl http://istio-service/

...（複数回アクセス）...

version1 server
version2 server
version1 server
version2 server
version1 server
version1 server
version2 server
version2 server
version1 server
version1 server
version2 server
version1 server
version1 server
version1 server
version1 server
version1 server
version1 server
version1 server
```

若干偏ってる気がしますが、アクセスが分散されました。
`weight` をの値を変更してあげればこれで自由に重み付けが変更できます！

# 気づき
今回、curl を実行している pod も `Envoy` がサイドカーとしてデプロイされている pod となります。
しかし、`Envoy` がデプロイされていない pod からアクセスを行ってもルーティングルールは無視されてしまいます。

これは、`Istio` のアーキテクチャとして `Envoy` がルーティング制御の役割を担っている（制御ルール等を担っているのは別ですが）ため、`Envoy` で Proxy されないとそもそもルーティング制御が行われないためです。

では、クラスタ外からの通信はどうやって `Istio` 配下に落とし込むか？というとそれは前述した `Gateway` によって制御されるのだと思います。

# おわりに
`Istio` を使えばマイクロサービスの管理/提供がとても簡単になると思います。
まだまだ、触り始めたばかりではありますが今後も色々試していきたいと思います。