Istioが備えるテレメトリ/ポリシー管理機能を使った統計情報取得と接続管理

Istioは前回紹介したトラフィック管理機能だけでなく、各種統計情報を収集・表示する機能や、指定した条件に従ってサービス間のネットワーク接続を許可/拒否する機能も備えている。今回はこういった機能の概要と基本的な使い方を紹介する。
目次
Istioのポリシー/テレメトリ機能の概要と実装
前回記事では、クラウドインフラストラクチャ上でのサービス間通信を管理する「サービスメッシュ」機能を提供するソフトウェア「Istio」の概要や導入方法を紹介した。Istioはサービス間のネットワーク接続を動的に管理できる点が特徴だが、これ以外にも通信内容やトラフィック状況などの監視およびこれらに応じた接続の許可/拒否を行う機能や、認証やアクセス制限、暗号化などのセキュリティ機能も備えている。今回はこれらの機能のうち、トラフィックの監視や接続管理機能について紹介していく。
ポリシー/テレメトリ機能を提供する「mixer」とプラグイン機構「Adapter」
Istioでは、通信内容やトラフィックを監視する機能を「Telemetry(テレメトリ)」、通信内容に応じて接続を許可/拒否する機能を「Policies/Policy(ポリシー)」と呼んでおり、「mixer」というコンポーネントでこれを実現している。mixerで実行できる処理は大きく分けて次の4つだ。
- トラフィックに関する情報のファイルなどへの出力(ロギング)
- 監視ツールへのトラフィック情報の転送(テレメトリ)
- 接続頻度に応じたトラフィック量の調整(クォータ)
- 接続の可否の判断(認証)
なお、テレメトリ機能でやり取りされる各種統計情報は「メトリック」と呼ばれ、一般的には送信元/送信先、送信時刻といった情報が含まれる。Istioではこれ以外にも任意の情報をメトリックとして扱うことが可能だ。
Istioではテレメトリおよびポリシー機能を「Adapter」と呼ばれるプラグイン機構を使って実装している。Istioが標準で提供しているAdapterとしては、次の表1のものがある。
| リソース名 | 用途 | 説明 |
|---|---|---|
| cloudwatch | テレメトリ | Amazon CloudWatchにメトリックを送信する |
| prometheus | テレメトリ | Prometheusにメトリックを送信する |
| stackdriver | ロギング、テレメトリ | Stackdriverにログやメトリックを送信する |
| stdio | ロギング | 標準出力や標準エラー出力、ファイルなどにログを出力する |
| fluentd | ロギング | ログをFluentdに出力する |
| denier | 認証 | 接続を拒否してエラー情報を返す |
| listchecker | 認証 | ホワイトリスト/ブラックリストを使った認証を行う |
| memquota | クォータ | 永続的ストレージを使用しないクォータ機能 |
| redisquta | クォータ | Redisストレージを使ったクォータ機能 |
また、これ以外にもKubernetesの設定情報を読み出して別のAdapterに送信する「kubernetesenv」といったユーティリティ的な機能を持つAdapterも用意されている。Istioが用意するAdapterだけでなく、独自のAdapterを作成して独自のサービスやルールに対応させることも可能だ。
mixerによるポリシー/テレメトリの実現方法
前回記事で解説したとおり、Istioでは各コンテナ上でEnvoyというコンポーネントをプロクシとして実行させ、コンテナ内のプロセスが行う通信をEnvoy経由で行わせることでサービス間のネットワーク接続を管理している。Envoyはネットワーク接続を受け付けた際にその情報をmixerに送信するようになっており、mixerは受け取った情報をAdapterに渡して処理させ、その結果に応じてクォータや認証といった処理を実行する仕組みになっている(図1)。

さて、前述のようにIstioでは多彩なAdapterが用意されているが、Adapterごとに処理を実行する際に必要な情報は異なる。そのため、mixerではトラフィックに関する情報をAdapterに渡す前に加工するためのTemplates(テンプレート)と呼ばれる仕組みが用意されている。
また、どのTemplateを使用して情報を加工し、加工後の情報をどのAdapterに渡すか、どのような条件でAdapterでの処理を実行するか、という設定はRules(Rule)というリソースで管理する。なお、「どのAdapterを利用するか」を指定するリソースは「Handler」、「どのテンプレートを使ってデータを処理するか」を指定するリソースは「Instance」と呼ばれる。「Handler」や「Instance」というタイプのリソースが存在するわけではなく、使用するAdapterやTemplateごとに対応するリソースタイプが存在する点に注意したい。HandlerやInstance、Ruleの関係は図2のようになっている。

デフォルトで利用できるテレメトリツール「Prometheus」
前回記事ではhelmを使ってIstioをインストールする流れを紹介したが、この場合デフォルトの設定では「Prometheus」という監視ツールがデプロイされ、いくつかの情報が自動的にPrometheusに送信されるようテレメトリ関連の設定が追加される。まずはこれらのデータをPrometheusで確認してみよう。
今回使用するテスト環境
今回はテストのため、前回記事で紹介したnginxをフロントエンド、「http-echo」プログラムをバックエンドとしたサービスをあらかじめデプロイしている。詳しくは前回記事を参照して欲しいが、フロントエンドのnginxに対しリクエストを送信すると、nginxはそのリクエストをそのままhttp-echoに転送する。http-echoはリクエストを受信すると、起動時にコマンドラインで指定した文字列をそのままレスポンスとして返すプログラムだ(図3)。

このシステムは、次の「simple-echo.yaml」というマニフェストファイルを使ってデプロイできる。
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-proxy-config
data:
proxy.conf: |
server {
listen 80;
location / {
proxy_pass http://http-echo.default.svc.cluster.local/;
proxy_http_version 1.1;
}
}
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d
volumes:
- name: config-volume
configMap:
name: nginx-proxy-config
---
apiVersion: v1
kind: Service
metadata:
name: http-nginx
labels:
app: http-nginx
spec:
ports:
- name: http
port: 80
protocol: TCP
selector:
app: nginx
---
apiVersion: v1
kind: Service
metadata:
name: http-echo
labels:
app: http-echo
spec:
ports:
- name: http
port: 80
protocol: TCP
selector:
app: http-echo
---
apiVersion: v1
kind: Pod
metadata:
name: http-echo-bar
labels:
app: http-echo
version: bar
spec:
containers:
- name: http-echo-bar
image: hashicorp/http-echo
ports:
- containerPort: 80
name: http
args:
- "-text=bar"
- "-listen=:80"
---
apiVersion: v1
kind: Pod
metadata:
name: http-echo
labels:
app: http-echo
version: echo
spec:
containers:
- name: http-echo
image: hashicorp/http-echo
ports:
- containerPort: 80
name: http
args:
- "-text=echo"
- "-listen=:80"
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: http-echo
spec:
host: http-echo
subsets:
- name: echo
labels:
version: echo
- name: bar
labels:
version: bar
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: http-echo
spec:
hosts:
- http-echo
http:
- match:
- uri:
prefix: /bar
route:
- destination:
port:
number: 80
host: http-echo
subset: bar
- match:
- uri:
prefix: /
route:
- destination:
port:
number: 80
host: http-echo
subset: echo
この設定ではリクエストのパスが「/bar」で始まるリクエストを「bar」という文字列を返す「http-echo-bar」Podに、それ以外のリクエストを「echo」という文字列を返す「http-echo」Podにルーティングするよう指定している。
$ kubectl apply -f simple-echo.yaml ↓nginxにアクセスするためのIPアドレスを確認 $ kubectl get svc http-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-nginx ClusterIP 10.105.19.210 <none> 80/TCP 5m4s ↓パスによって受け取る文字列が変化する $ curl 10.105.19.210 echo $ curl 10.105.19.210/bar bar $ curl 10.105.19.210/hoge echo
Prometheusコンソールへのアクセス
Prometheusについての詳細は以前の記事で紹介しているが、PrometheusではWebブラウザからアクセスできるコンソールで取得したデータの統計情報を閲覧できる。デフォルトの設定では「prometheus」という名称でServiceが作成されており、このリソースによって設定されたIPアドレス/ポート番号にWebブラウザでアクセスすることでコンソールを利用できる。たとえば次の例では、IPアドレスが「10.107.156.230」、ポート番号が9090となっている。
$ kubectl -n istio-system get svc prometheus NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE prometheus ClusterIP 10.107.156.230 <none> 9090/TCP 3d17h
ただし、このIPアドレスはクラスタ内からのみアクセスできるプライベートIPアドレスであり、クラスタ外のクライアントから直接アクセスすることはできない。Kubernetesにはクラスタ外からのコネクションを転送する「kubectl port-forward」コマンドがあるので、クラスタ外からコンソールにアクセスする場合はこちらを利用しよう。
$ kubectl -n istio-system port-forward --address 0.0.0.0 $(kubectl -n istio-system get pod -l app=prometheus -o jsonpath='{.items[0].metadata.name}') 9090:9090
このコマンドを実行したマシンの9090番ポートに外部からアクセスできるようfirewallの設定も変更しておこう。このコマンドを実行した状態で、Webブラウザで「http://<コマンドを実行したマシンのIPアドレス>:9090」というURLを開けばPrometheusのコンソールが表示されるはずだ(図4)。
デフォルト設定では、Prometheusに送信するメトリックとして表2のものが定義されている。
| 名称 | タイプ | 説明 |
|---|---|---|
| requests_total | COUNTER | リクエストの合計 |
| request_duration_seconds | DISTRIBUTION | リクエストの処理にかかった時間 |
| request_bytes | DISTRIBUTION | リクエストのサイズ |
| response_bytes | DISTRIBUTION | レスポンスのサイズ |
| tcp_sent_bytes_total | COUNTER | 送信バイト数 |
| tcp_received_bytes_total | COUNTER | 受信バイト数 |
Prometheusでは、これらの名称の先頭に「istio_」を付け、さらにタイプが「DISTRIBUTION」のものは末尾に「_bucket」もしくは「_count」、「_sum」を付けたものがメトリック名として使用される(図5)。
これらの情報には送信元/送信先に関する情報なども含まれており、コンソールで適切なクエリを行うことでその詳細を確認できる。たとえば「istio_requests_total」と指定して「Execute」をクリックすると、このカウンター値の時間変化がグラフで表示される(図6)。
ちなみに、これらのデフォルト設定はIstioの配布アーカイブ内にあるinstall/kubernetes/helm/istio/charts/mixer/templates/config.yamlというファイルで定義されている(GitHub)。もしこれらの設定を行っているリソースを削除してしまった場合、このファイルから設定を復元することが可能だ。
独自のテレメトリ・ロギング設定を追加する
続いて、独自にテレメトリのための設定を追加する方法を解説していこう。前述のとおり、mixerに対してポリシーやテレメトリの設定を追加するには次の3つのリソースを作成する必要がある。
- Templateに関する設定を記述するInstanceリソース
- Adapterに関する設定を記述するHandlerリソース
- 使用するInstanceとHandlerを指定したり、実行する条件を記述したりするRuleリソース
今回はPrometheusに対しメトリックを送信するため、Prometheus Adapterを使用する。この場合Handlerとして「prometheus」リソースを、Templateとして「metric」リソースを作成すれば良い(ドキュメント)。また、この2つを紐付ける「rules」リソースも必要だ。要するに、Prometheusを利用して統計情報を収集するには「prometheus」および「metric」、「rule」の3つのリソースを用意すれば良いということだ。
なお、helmを使ってインストールしたIstio環境では、デフォルトは次のようなmetricおよびprometheusリソースが作成されている。
$ kubectl -n istio-system get metric NAME AGE requestcount 4d requestduration 4d requestsize 4d responsesize 4d tcpbytereceived 4d tcpbytesent 4d
$ kubectl -n istio-system get prometheus NAME AGE handler 4d
「metric」リソースの作成
まずはPrometheusに対しどのような情報を格納するかを指定するmetricリソースを作成していこう。このリソースではメトリックの値を指定する「value」、属性値を指定する「dimensions」といった属性を指定する。
apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
value: <値>
dimensions:
<属性名1>: <値>
<属性名2>: <値>
:
:
ここで指定した「value」の値がPrometheusに送信される値となり、また「dimensions」で指定した属性名と値の組み合わせはPrometheusではラベルとして認識される。値には任意の定数(数値や文字列)だけでなく、トラフィックに関する情報を示す属性値(変数)も指定できるほか、演算子や関数を使った式を記述することもできる(表3、4)。ここで配列型のデータについては<属性名>["<キー>"]」のように指定することで対応する値を取り出せる。
| 変数名 | 説明 |
|---|---|
| source.ip | 送信元IPアドレス |
| source.labels | 送信元のPodに付与されているラベルを格納した配列 |
| source.name | 送信元Podのインスタンス名 |
| destination.ip | 送信先IPアドレス |
| destination.port | 送信先ポート |
| destination.labels | 送信先のPodに付与されているラベルを格納した配列 |
| destination.name | 送信先Podのインスタンス名 |
| request.headers | リクエストヘッダの情報を格納した配列 |
| request.path | HTTPリクエストのリクエストURLのパス部分 |
| request.host | HTTPのHost:ヘッダの値 |
| request.method | HTTPのリクエストメソッド |
| request.size | リクエストサイズ(HTTPならContent-Lengthの値) |
| request.total_size | リクエスト全体のサイズ |
| request.time | リクエスト発生時を示すタイムスタンプ |
| response.headers | レスポンスヘッダの情報を格納した配列 |
| response.size | レスポンスボディのサイズ |
| response.total_size | レスポンス全体のサイズ |
| response.time | レスポンス発生時を示すタイムスタンプ |
| response.code | レスポンスのHTTPステータスコード |
| context.reporter.kind | トラフィックの方向(inboundもしくはoutbound) |
| 演算子 | 意味 |
|---|---|
| == | 一致 |
| != | 非一致 |
| || | 論理OR |
| && | 論理AND |
| + | 加算 |
| | | 空でない |
| match | 正規表現マッチ |
なお、利用できる属性値はAttribute Vocabularyドキュメントを、演算子についてはExpression Languageドキュメントを参照して欲しい。
今回は次のように、送信元および送信先、URLのパス部分、リクエストメソッド、トラフィックの方向という5つの情報をラベルとして付与するような設定でリソースを作成した。
apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
name: my-metric
spec:
value: "1"
dimensions:
source: source.labels["app"] | "unknown"
destination: destination.labels["app"] | "unknown"
path: request.path | "unknown"
method: request.method | "unknown"
kind: context.reporter.kind | "unknown"
このデータはPrometheusでは「Counter」(カウンター)というタイプとして扱うことを想定している。Counterではその名の通り頻度などを扱うためのタイプで、統計情報を受信した際にその値だけカウンターの値を増やすという挙動を行う(Prometheusのドキュメント)。今回は1回のリクエストごとにカウンターを1増やしたいので、「value」には1を指定する。
「prometheus」リソースの作成
続いて、Prometheus側でどのような処理を行うかを指定する「prometheus」リソースを作成する。このリソースではPrometheusでどのようにメトリックを扱うかを指定する配列型属性「metrics」を指定する。
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
metrics:
- name: <Prometheus内で使用するメトリック名>
kind: <メトリックタイプを指定。UNSPECIFIED/GAUGE/COUNTERDISTRIBUTIONが選択可能>
instance_name: <使用するmetricリソース名>
label_names: # ラベルとして使用する属性を指定する
- <属性名1>
- <属性名2>
:
:
なお、「instance_name」属性では「<metricリソース名>.metric.<ネームスペース>」という形で、ネームスペース名まで指定しなければならない点に注意したい。今回は次のように指定した。
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
name: my-handler
spec:
metrics:
- name: my_http_echo_requests
kind: COUNTER
instance_name: my-metric.metric.default
label_names:
- source
- destination
- path
- method
- kind
ここでは「my_http_echo_requests」という名前を持つCOUNTERタイプのメトリックを作成するよう指定している。metrics属性では、使用するmetric名(ここでは先ほど定義した「my-metric」)と、ラベルとして付与する属性(ここでは「source」および「destination」、「path」、「method」、「kind」)を指定する。
「rule」リソースの作成
最後にmetricリソースとprometheusリソースを対応付ける「rule」リソースを作成する。このリソースでは「match」属性で処理を実行する条件を、「actions」属性で使用するInstanceタイプのリソースとHandlerタイプのリソースを指定する。なお、Instanceリソースについてはリスト形式で複数が指定可能だ。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
match: <処理を実行する条件>
actions:
- handler: <使用するHandlerリソース>
instances:
- <使用するInstanceリソース>
今回は次のように指定した。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: requests-rule
spec:
match: destination.labels["app"] == "http-echo"
actions:
- handler: my-handler.prometheus
instances:
- my-metric.metric
これは送信先が「app=http-echo」というラベルを持つPodを送信先とするトラフィックが発生した場合、その情報を元に「my-metric」というmetricリソースの情報に基づいてメトリックを作成し、それを「my-handler」というprometheusリソースの情報に基づいてPrometheusに送信する、という内容となっている。
これらの設定を1ファイルにまとめたのが次の「my-metric.yaml」だ。
apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
name: my-metric
spec:
value: "1"
dimensions:
source: source.labels["app"] | "unknown"
destination: destination.labels["app"] | "unknown"
path: request.path | "unknown"
method: request.method | "unknown"
kind: context.reporter.kind | "unknown"
---
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
name: my-handler
spec:
metrics:
- name: my_http_echo_requests
kind: COUNTER
instance_name: my-metric.metric.default
label_names:
- source
- destination
- path
- method
- kind
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: requests-rule
spec:
match: destination.labels["app"] == "http-echo"
actions:
- handler: my-handler.prometheus
instances:
- my-metric.metric
このマニフェストファイルを「kubectl apply -f」コマンドで適用してリソースを作成する。
$ kubectl apply -f my-metric.yaml
リソースの作成後、次のようにcurlコマンドでnginxに対しリクエストをいくつか送信したのちPrometheusのコンソールを確認すると、送信したリクエストの情報が確認したりできるはずだ(図7)。
$ curl 10.105.19.210 echo $ curl 10.105.19.210/bar bar $ curl 10.105.19.210/hoge echo
なお、データがPrometheusコンソール上で確認できない場合、設定に不備がある可能性がある。その場合、次のようにして「istio-telemetry 」Podのmixerのログにエラーメッセージなどが出力されていないかをチェックしてみよう。
$ kubectl -n istio-system logs deploy/istio-telemetry mixer
たとえばAdapter(ここではprometheusリソース)の指定で不適切な部分がある場合、次のようなメッセージが出力される。
2019-01-21T10:20:12.200730Z error api Report failed:1 error occurred: * failed to report all values: 1 error occurred: * could not find metric info from adapter config for my-metric.metric.default
ログの出力
Prometheus用のAdapterではmetric型のTemplateを使用して出力するメトリックを指定したが、Istioではこれ以外にも統計情報を出力するためのTemplateが用意されている。その1つがlogentryで、このTemplateはその名の通りログを出力する「stdio」などのAdapterで利用される。続いてはこの「logentry」と「stdio」を使ってmixerの標準出力にログを出力する方法を紹介する。
helmを使ってインストールしたIstio環境では、次のように標準出力にログを出力するための「handler」という名前のstdioリソースや、「accesslog」および「tcpaccesslog」というlogentryリソースが用意されるようになっている。
$ kubectl -n istio-system get stdio NAME AGE handler 1d
$ kubectl -n istio-system get logentry NAME AGE accesslog 1d tcpaccesslog 1d
これによって、デフォルトでmixerを実行しているコンテナの標準出力にアクセスログが出力されるようになっている。 mixerを実行しているコンテナは「istio-telemetry」というDeploymentによって作成されており、次のように「kubectl logs」コマンドを実行することでその内容を確認できる。
$ kubectl -n istio-system logs deploy/istio-telemetry mixer
デフォルト設定では標準出力にはログ以外の情報も多数出力されているのでやや見にくいが、次のように「accesslog」でgrepを実行すればログが出力されていることが確認できるはずだ。
$ kubectl -n istio-system logs deploy/istio-telemetry mixer | grep accesslog
{"level":"info","time":"2019-01-17T16:16:11.661970Z","instance":"accesslog.logentry.istio-system","apiClaims":"","apiKey":"","clientTraceId":"","connection_security_policy":"none","destinationApp":"http-echo","destinationIp":"10.128.1.79","destinationName":"http-echo","destinationNamespace":"default","destinationOwner":"/api/v1/namespaces/default/pods/http-echo","destinationPrincipal":"","destinationServiceHost":"http-echo.default.svc.cluster.local","destinationWorkload":"http-echo","httpAuthority":"http-echo.default.svc.cluster.local","latency":"286.323µs","method":"GET","protocol":"http","receivedBytes":720,"referer":"","reporter":"destination","requestId":"107f01bc-28a2-407c-95b2-ad2ae6e7ec34","requestSize":0,"requestedServerName":"","responseCode":403,"responseSize":65,"responseTimestamp":"2019-01-17T16:16:11.662129Z","sentBytes":157,"sourceApp":"nginx","sourceIp":"10.128.2.19","sourceName":"nginx","sourceNamespace":"default","sourceOwner":"/api/v1/namespaces/default/pods/nginx","sourcePrincipal":"","sourceWorkload":"nginx","url":"/hoge","userAgent":"curl/7.52.1","xForwardedFor":"0.0.0.0"}
:
:
さて、このように標準出力にログを出力させるにはPrometheusの場合と同様、Instanceに相当するlogentryリソースとHandlerに相当するstdioリソース、これらの対応付けを指定するruleリソースを作成すれば良い。まずlogentryリソースだが、次のような構造となっている。
apiVersion: config.istio.io/v1alpha2
kind: logentry
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
severity: <ログのSeverity(重要度)>
timestamp: <ログのタイムスタンプ>
variables: # 出力する値を指定する
<名前1>: <値>
<名前1>: <値>
:
:
例えば重要度を「WARNING」で、送信元および送信先、URLのパス部分、リクエストメソッド、トラフィックの方向という5つの情報を出力する設定は次のようになる。
apiVersion: config.istio.io/v1alpha2
kind: logentry
metadata:
name: my-logentry
spec:
severity: '"WARNING"'
timestamp: request.time
variables:
source: source.labels["app"] | "unknown"
destination: destination.labels["app"] | "unknown"
path: request.path | "unknown"
method: request.method | "unknown"
kind: context.reporter.kind | "unknown"
また、stdioリソースは次のような構造になっている。
apiVersion: config.istio.io/v1alpha2 kind: stdio metadata: name: <リソース名> namespace: <ネームスペース(省略可)> spec: logStream: <出力先> outputLevel: <出力を行う最小のSeverityレベル> outputAsJson: <true/false。trueにするとJSON形式で出力される> outputPath: <出力先ファイルのパス名。logStreamがSTDOUTやSTDERRの場合は省略可>
なお、出力先としては標準出力(STDOUT)および標準エラー出力(STDERR)、ファイル(FILE)、ローテーションするファイル(ROTATED_FILE)を指定できる。
たとえば、重要度が「WARNING」以上のログを非JSON形式で標準エラー出力に出力したい場合、次のようになる。
apiVersion: config.istio.io/v1alpha2 kind: stdio metadata: name: my-stderr spec: logStream: STDERR outputLevel: WARNING outputAsJson: false
ruleリソースについてはPrometheusで設定した場合と同じだ。これらの設定をまとめたマニフェストファイルは次のようになる(「my_logging.yaml」)。なお、ここではmatch属性を省略しているが、この場合このリソースが属するネームスペースの全トラフィックが処理対象となる。
apiVersion: config.istio.io/v1alpha2
kind: logentry
metadata:
name: my-logentry
spec:
severity: '"WARNING"'
timestamp: request.time
variables:
source: source.labels["app"] | "unknown"
destination: destination.labels["app"] | "unknown"
path: request.path | "unknown"
method: request.method | "unknown"
kind: context.reporter.kind | "unknown"
---
apiVersion: config.istio.io/v1alpha2
kind: stdio
metadata:
name: my-stderr
spec:
logStream: STDERR
outputLevel: WARNING
outputAsJson: false
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: my-logging
spec:
actions:
- handler: my-stderr.stdio
instances:
- my-logentry.logentry
このマニフェストを次のように反映させたのち、nginxに対し適当なリクエストを送信する。
$ kubectl apply -f my-logging.yaml
その後mixerのログを確認すると、次のように指定した形式でログが出力されていることが確認できる。
2019-01-21T14:21:49.919369Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "inbound", "method": "GET", "path": "/bar", "source": "nginx"}
2019-01-21T14:21:49.918696Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "outbound", "method": "GET", "path": "/bar", "source": "nginx"}
2019-01-21T14:21:52.047354Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "outbound", "method": "GET", "path": "/foo", "source": "nginx"}
2019-01-21T14:21:52.047783Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "inbound", "method": "GET", "path": "/foo", "source": "nginx"}
2019-01-21T14:21:53.892299Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "outbound", "method": "GET", "path": "/", "source": "nginx"}
2019-01-21T14:21:53.892788Z warn my-logentry.logentry.default {"destination": "http-echo", "kind": "inbound", "method": "GET", "path": "/", "source": "nginx"}
ポリシーの設定
前述のとおり、mixerではテレメトリだけでなく接続を管理するポリシー機能(クォータおよび認証)も提供されている。クォータはトラフィック量を制限するもの、認証は条件に合致したトラフィックを遮断するものだ。これらの設定もテレメトリの場合と同様、InstanceとHandler、Ruleの3つのリソースを作成することで行う。
条件に合致したトラフィックを遮断する
まずは条件に応じたトラフィック遮断について解説しよう。この機能は、「denier」や「listchecker」というAdapterで実現されている。
まずdenier Adapterだが、こちらはトラフィックを遮断するという処理を行うAdapterとなっている。このリソースでは遮断の条件などは指定できず、ruleリソース側で条件を指定する仕組みだ。
ドキュメントによると「checknothing」および「listentry」、「quota」Templateと組み合わせて利用できるとされているが、今回は何も処理を行わない「checknothing」Templateと組み合わせ、特定の接続を遮断する例を紹介しよう。
denier Adapterの設定を行う「denier」リソースでは、「status」属性で接続を遮断した場合にその送信元にレスポンスとして返すエラーステータスを指定できる。このエラーステータスはRPCフレームワークであるgRPCで使われるフォーマットで指定するようになっており、整数で表されるエラーコードと、エラーの内容を示す文字列であるメッセージで構成されている。
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
status:
code: <エラーコード>
message: <メッセージ>
ステータスコードについてはgRPCのドキュメントにまとめられているが、例えばパラメータが不適切な場合は3(INVALID_ARGUMENT)、権限がない場合は7(PERMISSION_DENIED)、サービスが利用できない場合は14(UNAVAILABLE)などとされている。たとえばエラーコードが7、メッセージが「Request Not allowed」という設定の場合、以下のようになる。
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: deny-request
spec:
status:
code: 7
message: Request Not allowed
また、何も処理を行わないTemplateを定義する「checknothing」リソースでは、リソース名とネームスペースのみが指定できる。
apiVersion: config.istio.io/v1alpha2 kind: checknothing metadata: name: <リソース名> namespace: <ネームスペース(省略可)> spec:
今回は「deny-request」という名前でリソースを作成する。
apiVersion: config.istio.io/v1alpha2 kind: checknothing metadata: name: deny-request spec:
ruleリソースでは、Handlerとしてdenierリソース、Instanceとしてchecknothingリソースを指定すると共に、match属性で遮断する条件を指定する。たとえば「app=nginx」というラベルが付与されたPodからのトラフィックをブロックする場合、次のようになる。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: deny-nginx
spec:
match: source.labels["app"] == "nginx"
actions:
- handler: deny-request.denier
instances:
- deny-request.checknothing
これらの設定を1ファイルにまとめたのが次の「nginx-deny.yaml」だ。
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: deny-request
spec:
status:
code: 7
message: Request Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
name: deny-request
spec:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: deny-nginx
spec:
match: source.labels["app"] == "nginx"
actions:
- handler: deny-request.denier
instances:
- deny-request.checknothing
このマニフェストファイルを次のように適用したのち、curlコマンドでnginxに対してリクエストを送信すると接続が遮断され、エラーが返されるようになる。
$ kubectl apply -f nginx-deny.yaml $ curl -v 10.105.19.210 * Rebuilt URL to: 10.105.19.210/ * Trying 10.105.19.210... * TCP_NODELAY set * Connected to 10.105.19.210 (10.105.19.210) port 80 (#0) > GET / HTTP/1.1 > Host: 10.105.19.210 > User-Agent: curl/7.52.1 > Accept: */* > < HTTP/1.1 403 Forbidden < Server: nginx/1.15.8 < Date: Mon, 21 Jan 2019 14:29:00 GMT < Content-Type: text/plain < Content-Length: 65 < Connection: keep-alive < x-envoy-upstream-service-time: 0 < * Curl_http_done: called premature == 0 * Connection #0 to host 10.105.19.210 left intact PERMISSION_DENIED:deny-request.denier.default:Request Not allowed
また、作成したこれらのリソースを削除すればトラフィックの遮断は行われなくなる。
$ kubectl delete -f nginx-deny.yaml $ curl 10.105.19.210/ echo
トラフィック制限を行う(クォータ)
続いてはクォータの設定だが、こちらは「memquota」や「redisquota」というAdapterで実装されている。これらのAdapterでは、トラフィックが発生するたびにその値がインクリメントされ、一定時間が経過したらリセットされるようなカウンターの値が一定を超えた場合に、カウンターが次にリセットされるまでそのトラフィックを遮断する、という挙動を実現できる。
この2つのAdapterの違いは、カウンターの値をどこに記録するかという点だ。memquotaは「istio-policy」Pod内で稼働するmixerプロセスが管理するメモリ内に、redisquotaではRedisストレージにカウンターの値を記録する。そのため、memquotaではクラスタ内に複数のistio-policy Podが稼働するような構成では正しく動作せず、プロダクション環境での利用は推奨されていない。そのため今回はredisquotaを使ったクォータ設定を紹介する。
なお、今回はテスト用に次のようなマニフェストファイル(redis.yaml)を用意してクラスタ内にRedisサーバーを作成する。
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: istio-system
labels:
app: redis
spec:
ports:
- name: redis
port: 6379
protocol: TCP
selector:
app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: istio-system
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5
ports:
- containerPort: 6379
name: redis
次のようにこのマニフェストファイルを適用することで、Redisサーバーを実行するPodとそこに接続するためのサービスが作成される。
$ kubectl apply -f redis.yaml $ kubectl -n istio-system get pod NAME READY STATUS RESTARTS AGE : : redis-6b8d55469b-bjf2t 1/1 Running 0 2m12s $ kubectl -n istio-system get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE : : redis ClusterIP 10.104.228.37 <none> 6379/TCP 2m16s
redisquotaの設定
redisquota Adapterは「quota」テンプレートと組み合わせて利用する。quotaテンプレートは次のように、metricテンプレートと似たような構造を持つ。
apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
dimensions:
<属性名1>: <値>
<属性名2>: <値>
:
:
ここで、dimensionsで指定した属性がクォータを実施するかどうかやどのような設定を行うかの判断に利用できるパラメータとなる。例えば接続先のパスに応じて設定を変えたい場合、次のように「request.path」をdimensionsに追加しておく。
apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
name: my-quota
spec:
dimensions:
path: request.path | ""
また、redisquota自体の設定は次のようになる。
apiVersion: config.istio.io/v1alpha2
kind: redisquota
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
redisServerUrl: <redisサーバーのURL>
quotas:
- name: <使用するquotaテンプレート>
maxAmount: <クォータの適用を開始するカウント値>
validDuration: <カウントの有効間隔>
bucketDuration: <ROLLING_WINDOWアルゴリズムを利用する場合のrolling間隔>
rateLimitAlgorithm: <アルゴリズム:FIXED_WINDOWもしくはROLLING_WINDOW>
overrides: # dimensionsの値に応じて設定を変更する場合、以下にそのパラメータを記述する
- dimensions: # dimensionsの条件を指定(省略可)
<属性値1>: <値>
<属性値2>: <値>
:
:
maxAmount: <指定した条件に合致した場合のmaxAmount>
- dimensions:
:
:
redisquotaでは、アルゴリズムとして「FIXED_WINDOW」と「ROLLING_WINDOW」が選択でき、「validDuration」および「bucketDuration」でその挙動を制御できる。ROLLING_WINDOWアルゴリズムではより細かい粒度でカウンターの値を管理できるが、その分Redisのリソースを多く消費するという(図8)。
今回は、次のような設定を作成した。 ここでは「5秒間に1回」を基本の上限とし、「/foo」というパスについては「5秒間に5回」という上限を設定している。
apiVersion: config.istio.io/v1alpha2
kind: redisquota
metadata:
name: my-quota-handler
spec:
redisServerUrl: redis.istio-system.svc:6379
quotas:
- name: my-quota.quota.default
maxAmount: 1
validDuration: 5s
rateLimitAlgorithm: FIXED_WINDOW
overrides:
- dimensions:
path: "/foo"
maxAmount: 5
あとは、ruleリソースを作成したquotaテンプレートとredisquota Adapterの紐付けを行えば良い。ここでは指定していないが、match属性を使って特定の条件に合致するトラフィックに対してのみクォータを適用することも可能だ。
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: my-quote-rule
spec:
actions:
- handler: my-quota-handler.redisquota
instances:
- my-quota.quota
これらに加えて、1回のトラフィックが発生するたびにカウンターの値をいくつ増加させるかを指定する「quotaspec」リソースと、quotaspecリソースをサービスに紐付ける「quotaspecbinding」リソースも用意する必要がある。まずquotaspecリソースは次のようになる。
apiVersion: config.istio.io/v1alpha2
kind: quotaspec
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
rules:
- quotas:
- charge: <カウンターの増加数>
quota: <対応するquotaリソース>
また、quotaspecbindingリソースは次のようになる。
apiVersion: config.istio.io/v1alpha2
kind: quotaspecbinding
metadata:
name: <リソース名>
namespace: <ネームスペース(省略可)>
spec:
quotaSpecs:
- name: <使用するquotaspecリソース>
namespace: <使用するquotaspecリソースのネームスペース>
services:
- name: <quotaspecを紐付けるService名>
namespace: <Serviceのネームスペース>
今回はhttp-echoサービスに対しクォータを適用するので、次のような設定となる。
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
name: my-quotaspec
spec:
rules:
- quotas:
- charge: 1
quota: my-quota
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
name: my-binding
spec:
quotaSpecs:
- name: my-quotaspec
namespace: default
services:
- name: http-echo
namespace: default
以上の設定を1ファイルにまとめたのが、次の「my-quota.yaml」だ。
apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
name: my-quota
spec:
dimensions:
path: request.path | ""
---
apiVersion: config.istio.io/v1alpha2
kind: redisquota
metadata:
name: my-quota-handler
spec:
redisServerUrl: redis.istio-system.svc:6379
quotas:
- name: my-quota.quota.default
maxAmount: 1
validDuration: 5s
rateLimitAlgorithm: FIXED_WINDOW
overrides:
- dimensions:
path: "/foo"
maxAmount: 5
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: my-quote-rule
spec:
actions:
- handler: my-quota-handler.redisquota
instances:
- my-quota.quota
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
name: my-quotaspec
spec:
rules:
- quotas:
- charge: 1
quota: my-quota
---
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
name: my-binding
spec:
quotaSpecs:
- name: my-quotaspec
namespace: default
services:
- name: http-echo
namespace: default
このマニフェストを次のように適用したのち、curlコマンドでnginxに対し連続したリクエストを発生させたりすると、次のように「RESOURCE_EXHAUSTED」というメッセージが表示されるようになる。
$ curl 10.105.19.210 echo : : $ curl 10.105.19.210 RESOURCE_EXHAUSTED:Quota is exhausted for: my-quota $ curl -v 10.105.19.210 * Rebuilt URL to: 10.105.19.210/ * Trying 10.105.19.210... * TCP_NODELAY set * Connected to 10.105.19.210 (10.105.19.210) port 80 (#0) > GET / HTTP/1.1 > Host: 10.105.19.210 > User-Agent: curl/7.52.1 > Accept: */* > < HTTP/1.1 429 Too Many Requests < Server: nginx/1.15.8 < Date: Mon, 21 Jan 2019 15:13:05 GMT < Content-Type: text/plain < Content-Length: 51 < Connection: keep-alive < x-envoy-upstream-service-time: 2 < * Curl_http_done: called premature == 0 * Connection #0 to host 10.105.19.210 left intact RESOURCE_EXHAUSTED:Quota is exhausted for: my-quota
また、「/foo」というパスに対するリクエウトについては緩いクォータ設定になっているため、ほかのパスにアクセスできない場合でもアクセスが行える。
$ curl 10.105.19.210 RESOURCE_EXHAUSTED:Quota is exhausted for: my-quota $ curl 10.105.19.210/foo echo
なお、うまく設定が行われない場合、次のようにして「istio-policy」Podのログを確認してみよう。
$ kubectl -n istio-system logs deploy/istio-policy mixer
たとえばHandlerの指定にミスがあった場合、次のようなログが出力される。
2019-01-21T14:35:10.358974Z error Handler not found: handler='my-quota-handler.memquota' 2019-01-21T14:35:10.359195Z error No valid actions found in rule
動的かつ柔軟な設定が可能なポリシー/テレメトリ設定
Istioのポリシー/テレメトリ設定はやや煩雑であり、設定ミスがあった場合に明確にその原因が表示されないためデバッグがやや難しいという問題や、ドキュメント自体も一部不十分なところがある。そのため活用するには試行錯誤が必要かもしれない。とはいえ柔軟に設定を行うことが可能であり、またポリシー設定は動的に変更できるため、テストなどにも活用できるだろう。