前回の記事 Istio を用いた Blue Green / Canary Deployment その1の続きで、今回は、もうちょっと複雑なルーティングに挑戦してみる。
なぜ Istio か?
その前に、少しだけなぜ Istio に注目しているのか?という話をしておきたい。 マイクロサービスの環境で、Blue Green Deployment を実施したい場合、どうなるだろうか?普通の Blue Green Deployment だったら、WebApps などの PaaS だったら何もしなくても出来てしまう。マイクロサービスだとどうだろう。
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 等の機能も試してみたい。
リソース
ちなみに、今回のソースコードはこちらに置いている。ソースコード