Istio を用いた Blue Green / Canary Deployment　その１

今回は [Istio](https://istio.io/)を用いて、Blue Green Deployment と Canary の実施方法を試してみた。
　特に Canary に関しては、Vampという素晴らしいツールが DC/OS には存在するが、Kubernetes の方はalpha だし、決定版の Canaryの方法は無かった。しかし、サービスメッシュつまり、マイクロサービスの「隙間」を埋めるためのツールとして、Istioがリリースされた。おそらくこれがデファクトになっていくので、とても注目だ。

# 前提条件

前提として、次の環境をすでに構築していると想定している。私はAzure Container Service と Azure Container Registry で環境を作成したが、他のクラウドサービスでも同じように動作すると思う。

* Azure Container Service (Kubernetes 1.6.6)
* Azure Container Registry
* Istio (0.1.6)

Istio はすでにインストールされている前提である。ちなみに、Azure の場合はこの手順でばっちり動作する。[Deploying Istio on Azure Container Service](https://readon.ly/post/2017-05-25-deploy-istio-to-azure-container-service/)

ちなみに、上記の手順でのインストールには、[helm](https://github.com/kubernetes/helm)　が必要なのでインストールしておくこと。
kubernetes は、 kubectl がダウンロード済み、config ファイルも設定済みであること。Azure の場合はこちらを参照のこと。[](https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-walkthrough) ちなみに、クラスターのSSHのPublic Key に パスワード付きの Private Key を指定した場合、`az acs kubernetes get-credentials ` では、config ファイルは取得できない。マスターノードにログインして、`.kube/config` からローカルに取ってくること。

# サンプルアプリケーション

最もシンプルに検証するために、index.html と、最低限のDockerfile を書いておく。

version 1.0.0 用 index.html

```html
<html>
<head>
    <title>Blue Version 1.0.0</title>
</head>
<body bgcolor="#0000FF">
    <H1>This is Blue Version 1.0.0</H1>
</body>
</html>
```

version 2.0.0 用 index.html

```
<html>
<head>
    <title>Green Version 2.0.0</title>
</head>
<body bgcolor="#00FF00">
    <H1>This is Green Version 2.0.0</H1>
</body>
</html>
```

Dockerfile

```
FROM nginx
ADD index.html /usr/share/nginx/html
```

それぞれにディレクトリを掘って、index.html と、Dockerfile の対をおいておく。
`1.0.0` `2.0.0`それぞれ次のようなコマンドでDocker image を作っておく。`kube16.azurecr.io` は、私のテスト用 Azure Container Registry のアドレスで、ログインに必要な情報はすべて ポータルの `setting > accesss key` から取得できる。

```
docker login docker login -u USERNAME_HERE -p PASSWORD_HERE kube16.azurecr.io
docker build -t kube16.azurecr.io/websample:1.0.0 -t kube16.azurecr.io/websample:latest .

```

それぞれを、ACR (Azure Container Registry) に push しておく。

まず、Secret を作成するコマンドを実行する。

```
kubectl create secret docker-registry acrsecret --docker-server=kube16.azurecr.io --docker-username=USERNAME_HERE --docker-password=PASSWORD_HERE --docker-email=YOURE_E_MAIL_HERE
```
このコマンドで、Kubernetes に ACR にアクセスための Secret が作成される。ただ、毎回手でたたくのは嫌だと思うので、次のコマンドをたたいてから、

```
kubectl get secret kb16acrsecret --output=yaml
```
すると、下記のような情報が返ってくる

```
apiVersion: v1
data:
  .dockercfg: ey...............................=
kind: Secret
metadata:
  creationTimestamp: 2017-07-09T14:17:27Z
  name: kb16acrsecret
  namespace: default
  resourceVersion: "420393"
  selfLink: /api/v1/namespaces/default/secrets/acrsecret
  uid: 56048e53-64b1-11e7-bdb4-000d3a30f128
type: kubernetes.io/dockercfg
```
ここで、ポイントとなるのが、.dockercfg: の値である。これは、ACRに接続するための、情報が、base64 エンコードされたものだ。

これをもとに、secret.yaml を作っていつでも再生成できるようにしておく。ポイントは上記で取得した、.dockercfgの値をとっておくこと。

```
apiVersion: v1
kind: Secret
metadata:
  name: kb16acrsecret
type: kubernetes.io/dockercfg
data:
  .dockercfg: ey......................=
```
さて、これで準備環境。最後に、2 つのバージョンを持った、WebService というサービスをデプロイしてみる。

WebService.yaml

```
apiVersion: v1
kind: Service
metadata:
  name: web-service
  labels:
    app: web-service
spec:
  selector:
    app: web-service
  ports:
    - port: 80
      name: http
      targetPort: 80
  type: LoadBalancer
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: web-deployment-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: web-service
        version: 1.0.0
    spec:
      containers:
        - name: web-service
          image: kube16.azurecr.io/websample:1.0.0
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: kb16acrsecret
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: web-deployment-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: web-service
        version: 2.0.0
    spec:
      containers:
        - name: web-service
          image: kube16.azurecr.io/websample:2.0.0
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: kb16acrsecret

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: webservice-ingress
  annotations:
    kubernetes.io/ingress.class: istio
spec:
  rules:
  - http:
      paths:
      - path: /
        backend:
          serviceName: web-service
          servicePort: 80
```

このような結果が返ってくる。ここで、Web Service の EXTERNAL-IP にブラウザで、アクセスしてみる。このままの yaml では、v1.0.0 もしくは、v2.0.0 のどちらかにルーティングされる。

```
NAME                               CLUSTER-IP     EXTERNAL-IP     PORT(S)                       AGE
details                            10.0.94.56     <none>          9080/TCP                      2d
istio-egress                       10.0.53.207    <none>          80/TCP                        3d
istio-ingress                      10.0.229.66    123.45.67.89   80:32656/TCP,443:31504/TCP    3d
kubernetes                         10.0.0.1       <none>          443/TCP                       3d
productpage                        10.0.19.13     <none>          9080/TCP                      2d
prometheus                         10.0.103.100   <none>          9090/TCP                      3d
quiet-lambkin-istio-grafana        10.0.83.157    <none>          3000/TCP                      3d
quiet-lambkin-istio-mixer          10.0.40.41     <none>          9091/TCP,9094/TCP,42422/TCP   3d
quiet-lambkin-istio-pilot          10.0.185.10    <none>          8080/TCP,8081/TCP             3d
quiet-lambkin-istio-servicegraph   10.0.149.187   <none>          8088/TCP                      3d
quiet-lambkin-istio-zipkin         10.0.135.63    <none>          9411/TCP                      3d
ratings                            10.0.161.28    <none>          9080/TCP                      2d
reviews                            10.0.137.87    <none>          9080/TCP                      2d
web-service                        10.0.76.169    123.45.67.10    80:31228/TCP                  32m
```
# istio を Injection する

さて、この至って普通の アプリケーションに対して、Istio化してみよう。

```
kubectl delete -f webservice.yaml
kubectl apply -f <(istioctl kube-inject -f webservice.yaml)
```
これは、istioctl が、通常の yaml に対して、istioの [envoy](https://istio.io/docs/concepts/what-is-istio/overview.html) と呼ばれるプロキシを注入するためである。こんな感じで、既存のyamlファイルを活用できる。

試しにどうなるか見てみよう。

```
istioctl kube-inject -f webservice.yaml
```

annotation として, [init-container](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/)　が指定されている。init-container は、通常の app container が動く前に実行されるコンテナだ。また、自分で指定した、コンテナ以外に、proxy-debug というコンテナが注入されているのがわかる。

```yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  name: web-deployment-v1
spec:
  replicas: 1
  strategy: {}
  template:
    metadata:
      annotations:
        alpha.istio.io/sidecar: injected
        alpha.istio.io/version: jenkins@ubuntu-16-04-build-12ac793f80be71-0.1.6-dab2033
        pod.beta.kubernetes.io/init-containers: '[{"args":["-p","15001","-u","1337"],"image":"docker.io/istio/init:0.1","imagePullPolicy":"Always","name":"init","securityContext":{"capabilities":{"add":["NET_ADMIN"]}}},{"args":["-c","sysctl
          -w kernel.core_pattern=/tmp/core.%e.%p.%t \u0026\u0026 ulimit -c unlimited"],"command":["/bin/sh"],"image":"alpine","imagePullPolicy":"Always","name":"enable-core-dump","securityContext":{"privileged":true}}]'
      creationTimestamp: null
      labels:
        app: web-service
        version: 1.0.0
    spec:
      containers:
      - image: kube16.azurecr.io/websample:1.0.0
        name: web-service
        ports:
        - containerPort: 80
        resources: {}
      - args:
        - proxy
        - sidecar
        - -v
        - "2"
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        image: docker.io/istio/proxy_debug:0.1
        imagePullPolicy: Always
        name: proxy
        resources: {}
        securityContext:
          runAsUser: 1337
        volumeMounts:
        - mountPath: /etc/certs
          name: istio-certs
          readOnly: true
      imagePullSecrets:
      - name: kb16acrsecret
      volumes:
      - name: istio-certs
        secret:
          secretName: istio.default![blue.png](https://qiita-image-store.s3.amazonaws.com/0/3470/e96e853b-1cdd-a648-d60a-2ca90e2ac6de.png)

status: {}
```

Envoyというproxy が注入されることで Kubernetes にデプロイされた Istio の管理用コンテナと動作して、ターゲットの container の前に、Envoy が動作して、プロキシする。プロキシをかますことで、L7レベルのトラフィックルーティングが可能になる。他にも、タイムアウトの設定や、Fault Injection等も可能になる。こちなみに、このパターンは、[Sidecar pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar)のアーキテクチャのようだ。


![istio.png](https://qiita-image-store.s3.amazonaws.com/0/3470/a7f18d7e-840a-a31b-fad9-57459298558c.png)

# ルーティングの設定変更

さて、サーバーへのアクセスは、Istio の Ingress 経由で行う。

```
$ kubectl get ingress
NAME                 HOSTS     ADDRESS         PORTS     AGE
webservice-ingress   *         104.210.39.92   80        6m
```

といった感じで Istio の ingress が見える。何も設定しなければ、`1.0.0` と `2.0.0` のどちらが出てくるかはわからない。

![blue.png](https://qiita-image-store.s3.amazonaws.com/0/3470/4b4fbfc8-ca39-e137-07ea-949d76386a96.png)
![green.png](https://qiita-image-store.s3.amazonaws.com/0/3470/708c5333-c83d-ff1b-a4b8-466547abad90.png)


ルーティングを例えばこんなyamlで指定すると、必ず 1.0.0 にルーティングされる

```
type: route-rule
name: web-service-default
namespace: default
spec:
  destination: web-service.default.svc.cluster.local
  precedence: 1
  route:
  - tags:
      version: 1.0.0
    weight: 100
```

実行してみよう。

```
istioctl create -f all-v1.yaml --configAPIService=quiet-lambkin-istio-pilot:8081
```
![blue.png](https://qiita-image-store.s3.amazonaws.com/0/3470/4b4fbfc8-ca39-e137-07ea-949d76386a96.png)
istioctl コマンドで、ルールを適用すると、`1.0.0` のみにルーティングされた。

ちなみに、--configAPIService がついているのは、普通にインストールすると大丈夫だが、helmでインストールすると、pilot と呼ばれるIstioの管理サービスの名前がデフォルト(istio-pilot:8081)からかわるので、指定しないといけないから。これらの値は、`kubectl get svc` で見つけることができる。

# はまったところ

ハマりどころとしては、Ingress の設定部分。

上記の webservice.yaml の抜粋だが、Istio の Ingress はすべてのサービスで共有するので、当初 `path: /web` と設定していた。すると、pod へのリクエストも、`/web` でルーティングされるので、404が出るという現象があった。バックエンドには、/でルーティングしてほしいものだが、やる方法があるかは今後の宿題。

```
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: webservice-ingress
  annotations:
    kubernetes.io/ingress.class: istio
spec:
  rules:
  - http:
      paths:
      - path: /
        backend:
          serviceName: web-service
          servicePort: 80
```

というわけで、ルーティングの切り替えまでは実施できた。次は Canary はもう少し複雑なルーティングに挑戦してみよう。