UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Istioでマイクロサービスのテスタビリティを向上させる

SPEEDAの開発チームの石橋です。

最近ではマイクロサービスでプロダクトを開発することが多くなってきていると思います。 そういった状況の中でマイクロサービスのテスト、特に異常系のテストをするコストがやや高いという話を何度か耳にしました。 本記事ではIstioのFault Injectionで「エラーが発生する」、「処理に時間がかかる」などの異常系のテストを容易に実現する方法を紹介します。

異常系のテストをする際の課題

サービスAとサービスBがあるとします。サービスAからサービスBにリクエストした際、サービスBがエラーになる可能性があります。 そうした場合、そのエラーが他のマイクロサービスに伝播して、障害がシステム全体へと波及しないよう適切に処理する必要があります。

適切な処理ができていることをテストするためは、「サービスAが正しいリクエストをしてもサービスBが必ずエラーを返す」状態を作らなければなりません。 このような状況を作るためにはいくつか方法が考えられます。

  • 必ずエラーを返すようにサービスBのコードを修正してデプロイする。
  • 必ずエラーを返すモックサーバーを用意し、サービスAの向き先をサービスBからモックサーバーに変更する。

これらの方法はやや面倒だと思います。特に前者はローカルで一時的に修正して確認する分にはよいかもしれませんが、正常系と混在している自動テストに組み込むことは困難です。 IstioのFault Injectionはこの課題を解決します。設定ファイルを適用するだけで「サービスAが正しいリクエストをしてもサービスBが必ずエラーを返す」状態を作ることができます。

環境構築

本記事ではバージョンが1.2.2のIstioを使用して動作確認しています。

まずはKubernetesに必要なリソースを作成していきます。Gatewayはdefault、ServiceとDeploymentとDestinationRuleとVirtualServiceはexample-nsというnamespaceに作成していきます。VirtualServiceはDestinationRuleの後に作成してください。*1 kubectl apply -f ファイル名を実行すれば、それぞれのリソースが作られます。

※それぞれのリソースの概要や設定ファイルの詳細な書き方については割愛します。

Gateway
Service
Deployment
DestinationRule
VirtualService

また、外部からアクセスするためのエンドポイントを環境変数で定義しておきます。後で何度もアクセスするためです。

export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
export GATEWAY=$INGRESS_HOST:$INGRESS_PORT

Fault Injection

まずはFault Injectionする前に上の環境構築で作ったPodに対してアクセスし、正常に処理されることを確認します。

$ for i in {1..10}; do sleep 1; curl -s -H 'Host: example-svc.example-ns.svc.cluster.local' $GATEWAY -o /dev/null -w '%{http_code}\n' ; done
200
200
200
200
200
200
200
200
200
200

10回リクエストし、HTTPのステータスコード200が10回返ってきているので正常に処理されたことがわかります。

Injecting HTTP Aborts

ここからが本題です。exampleというアプリケーションがHTTPのステータスコード500を返すようにします。virtual-service.yamlの内容を以下のように変更します。

もとのvirtual-service.yamlの最後にHTTPのステータスコード500を必ず返す設定を追加しました。

$ diff virtual-service.yaml 500-virtual-service.yaml -u
--- virtual-service.yaml    2019-07-30 17:21:55.000000000 +0900
+++ 500-virtual-service.yaml    2019-07-30 18:14:22.000000000 +0900
@@ -17,3 +17,7 @@
         port:
           number: 80
         subset: v1
+    fault:
+      abort:
+        httpStatus: 500     
+        percent: 100

500-virtual-service.yamlを適用し、上のcURLを再度実行します。

$ for i in {1..10}; do sleep 1; curl -s -H 'Host: example-svc.example-ns.svc.cluster.local' $GATEWAY -o /dev/null -w '%{http_code}\n' ; done
500
500
500
500
500
500
500
500
500
500

意図通り、すべてHTTPのステータスコード500が返ってきています。もちろんpercentを変更すれば、任意の割合でHTTPのステータスコード500を返すことができます。

Injecting HTTP Delays

次はレスポンスが返ってくるのに時間がかかるパターンです。500-virtual-service.yamlの内容のhttpStatus:fixedDelay:に変更しています。ここでは10sにしているので、レスポンスが10秒遅延します。

システム全体が遅くならないために、呼び出し側のマイクロサービスがタイムアウトなどを適切に行えているかをテストする際に有用です。

最後は少し応用的な例です。ヘッダーにx-e2e-case: errorが付いていれば、HTTPのステータスコード500が返ってきます。付いていなければ、exampleというアプリケーション本来のレスポンスが返ってきます。

1つ目のcURLはヘッダーにx-e2e-case: errorを付けています。2つ目のcURLはヘッダーにx-e2e-case: errorを付けていません。 それぞれのcURLを実行すると、期待通りのステータスコードが返ってきます。

$ curl -s -H 'x-e2e-case: error' -H 'Host: example-svc.example-ns.svc.cluster.local' $GATEWAY -o /dev/null -w '%{http_code}\n'
500
$ curl -s -H 'Host: example-svc.example-ns.svc.cluster.local' $GATEWAY -o /dev/null -w '%{http_code}\n'
200

また、VirtualServiceのmatchは強力で、ヘッダー以外にもURI、ポート番号、クエリパラメータなどでマッチさせることができます。*2

E2Eテストを実行する環境用のVirtualServiceを作り、そこに特定の条件でエラーや遅延が発生する設定を書いておけば、異常系のケースを正しくハンドリングできていることを確認するテストも容易に行えると思います。

まとめ

マイクロサービス化によって様々な恩恵を受けることができる一方で複雑度が上がるとことがあります。複雑度が上がったものの1つがネットワークです。 マイクロサービスではネットワーク越しの通信が当たり前になるので、通信が失敗することを前提として実装するべきです。そして、その実装をテストしやすくするのがFault Injectionです。 Istioの場合だと、VirtualServiceでFault Injectionの設定ができます。

参考