Azure
kubernetes
istio

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

今回は Istioを用いて、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

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

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

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

version 1.0.0 用 index.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 と呼ばれるプロキシを注入するためである。こんな感じで、既存のyamlファイルを活用できる。

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

istioctl kube-inject -f webservice.yaml

annotation として, init-container が指定されている。init-container は、通常の app container が動く前に実行されるコンテナだ。また、自分で指定した、コンテナ以外に、proxy-debug というコンテナが注入されているのがわかる。

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のアーキテクチャのようだ。

istio.png

ルーティングの設定変更

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

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

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

blue.png
green.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
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 はもう少し複雑なルーティングに挑戦してみよう。