kubernetes
istio

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

More than 1 year has passed since last update.

前回の記事 Istio を用いた Blue Green / Canary Deployment その1の続きで、今回は、もうちょっと複雑なルーティングに挑戦してみる。

なぜ Istio か?

その前に、少しだけなぜ Istio に注目しているのか?という話をしておきたい。 マイクロサービスの環境で、Blue Green Deployment を実施したい場合、どうなるだろうか?普通の Blue Green Deployment だったら、WebApps などの PaaS だったら何もしなくても出来てしまう。マイクロサービスだとどうだろう。

bluegreen.png

Blue Green するためには、API Gateway が必要になる。しかも、マイクロサービス毎に。あなたは、API Gatewayをサービス毎に Deployして、無数にあるマイクロサービスの制御をしないといけない。これはめっちゃ
めんどくさいことだろう。前回みたいなのだとまだいいが、L7 Application Gateway 相当のこと、例えば、クッキーやヘッダを見てルーティングを変えるなどの制御を行いたいときは、HA Proxy なりなんなりを自分でサービス毎に、デプロイして、設定しないといけない。

 また、マイクロサービス間の、障害や遅延が発生した時の対応や、テストはどうしたらいいだろう?クレデンシャルは?とか考えるとめんどくさいことがたくさんあることに気づく。これを Istio がやってる。

Microservices での、Blue Green Deployment

普通にやるとこれはめんどくさいのだが、前回お話しした通り、kubernetes の YAML に Istio を Inject すると、Pod にプロキシを注入して、制御してくれる。たぶん Istio はまだ枯れていないが将来のデファクトになるのではないだろうか。

Cookie による フィルタリング

このマイクロサービス時の Blue Green Deployment を実施する場合、次のようなステップになるだろう。

  • version 2 を version 1 と平行で稼働させる。ただし version 1 にルーティングされる
  • version 2 が正しく稼働しているかを確認する
  • version 2 に切り替える

といったステップだ。version 1 と version 2 の切り替えは実施したが、真ん中の「version 2 が正しく稼働しているか確認する」はどうしたらいいだろう。 一つの手段としては、Istio はヘッダを見てルーティングルールを設定できるので、Cookie を設定して、そこに特定の Cookie が来ていた時のみ、 version 2 にルーティングするようにすると良い。そのためには2つのルールを設定する。

サービスの構成

さて、今回は、サービスの構成を変えてみた。前回は、単純なHTMLファイルをルーティングしたのみだが、今回は

ingress -> web-front -> web-service

という風に、間に、web-front というサービスをかましてみた。なぜかというと、0.1.6のIstio にはバグがあり、ingress の最初に呼ばれるサービスには、上記の Cookie を使ったルーティングがかからないというバグがあったためである。ただ、のちに述べるが、このことが新たな学びを生んだのでよかったことでもある。さて、上記の web-service は前回と同じものだが、web-front は、web-service を呼び出す必要があったので、それを、Rubyで書いて、Docker に入れてデプロイした。

WebService.yaml

apiVersion: v1
kind: Service
metadata:
  name: web-service
  labels:
    app: web-service
spec:
  selector:
    app: web-service
  ports:
    - port: 80
      name: http
---
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.6
          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.6
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: kb16acrsecret
---
apiVersion: v1
kind: Service
metadata:
  name: web-front
  labels:
    app: web-front
spec:
  selector:
    app: web-front
  ports:
    - port: 80
      name: http
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: web-front
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: web-front
        version: 1.0.0
    spec:
      containers:
        - name: web-front
          image: kube16.azurecr.io/webfront:1.0.3
          env:
          - name: SERVICE_URL
            value: "http://web-service"
          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:
      - backend:
          serviceName: web-front
          servicePort: 80 

フィルタリングの適用

さて、今回のフィルタリングは、デフォルトでは、全員 version 1 だが、NAME=v2tester という Cookie を持ってアクセスした人のみが、version 2 にルーティングされるようにしたい。

最初に、全員 version 1 にルーティングされるように設定する。

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

ポイントは、precedence でこの値が高いルールほど、優先順位が高くなる。

type: route-rule
name: web-service-test-v2
spec:
  destination: web-service.default.svc.cluster.local
  precedence: 2
  match:
    httpHeaders:
      cookie:
        regex: "^(.*?;)?(NAME=v2tester)(;.*)?$"
  route:
  - tags:
      version: 2.0.0

次にこのルールを適用する。matchのルールに関しては MatchConditionを見ればよい。ここでは、Cookie に NAME=v2tester がある場合のみルーティングが有効になって、ルーティングされる。

しかし、これを実施しても当初動作しなかった。もちろん、cookie は渡している。

cookie へのマッチは正規表現で記述できるが、最初に疑ったのは正規表現だ。なぜなら、この正規表現を、そのまま持ってきて、node 等で NAME=v2tester; user=jason; といった実際のcookie が格納される形式でマッチさせようとしても、マッチしない。^(.*?;\s?)?(NAME=v2tester)(;.*)?$ だとマッチする。スペースが考慮されていないからだ。コードを読んでいないのでわからないが、Istioの内部でスペースを取り除いているからマッチするのかもしれない。(サンプルプログラムがこの正規表現だった。サンプルは動作しているので、いろいろ悩んだ末これを持ってきた)

Cookie を転送する必要がある

先に述べた Bug を除いて、Cookie がマッチしない問題の大きなポイントがわかった。ingress -> web-front -> web-service といった構成の場合、web-front が受け取った Cookie を web-service 側に転送してあげる必要があるのだ。私は下のようなコードを、web-front に書いて、実現したらうまくルーティングできた。ただし、これは相当かっこわるいので、Issueとしてレポートしておいた。

#!/usr/bin/ruby

require 'webrick'
require 'net/http'

if ARGV.length < 1 then
    puts "usage: #{$PROGRAM_NAME} port"
    exit(-1)
end

port = Integer(ARGV[0])

server = WEBrick::HTTPServer.new :BindAddress => '0.0.0.0', :Port => port

trap 'INT' do server.shutdown end

login = '
<html>
<head>
    <title>V2Tester Login</title>
    <meta http-equiv="Set-Cookie" content="NAME=v2tester"> 
</head>
<body>
    <H1>You logged in as a v2 Tester.</H1>
</body>
</html>
'

logout = '
<html>
<head>
    <title>V2Tester Logout</title>
    <meta http-equiv="Set-Cookie" content="NAME=v2tester; expires=Fri, 31-Dec-1999 23:59:59 GMT;"> 
</head>
<body>
    <H1>You logged out as a v2 Tester.</H1>
</body>
</html>

'

server.mount_proc '/webpage' do |req, res|
    uri = URI.parse(ENV["SERVICE_URL"])
    http = Net::HTTP.new(uri.host, uri.port)
    request = Net::HTTP::Get.new(uri.request_uri)

    puts("---------transfar cookie")
    puts(req.cookies)
    cookies = {}
    req.cookies.each{|cookie|
        cookies[cookie.name] = cookie.value
        puts(cookie.name + "=" + cookie.value)
    }
    request['Cookie'] = cookies.map{|k,v|
        "#{k}=#{v}"
    }.join(';')
    puts "Cookie:" + request['Cookie']
    puts "-----end "

    response = http.request(request)
    res.body = response.body

    res.status = 200
    res['Content-Type'] = 'text/html'
end

server.mount_proc '/loginpage' do |req, res|
    res.body = login
    res['Content-Type'] = 'text/html'
end

server.mount_proc '/logoutpage' do |req, res|
    res.body = logout
    res['Content-Type'] = 'text/html'
end

server.start

おわりに

というわけで、ほとんど、サンプルと同じような機能を自分で実現しようとしただけで、結構ハマってしまったがおかげで構造も理解できた。次回は、Fault Injection や、Circuit Breaker 等の機能も試してみたい。

リソース

ちなみに、今回のソースコードはこちらに置いている。ソースコード