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

Istioは前回紹介したトラフィック管理機能だけでなく、各種統計情報を収集・表示する機能や、指定した条件に従ってサービス間のネットワーク接続を許可/拒否する機能も備えている。今回はこういった機能の概要と基本的な使い方を紹介する。

Istioのポリシー/テレメトリ機能の概要と実装

前回記事では、クラウドインフラストラクチャ上でのサービス間通信を管理する「サービスメッシュ」機能を提供するソフトウェア「Istio」の概要や導入方法を紹介した。Istioはサービス間のネットワーク接続を動的に管理できる点が特徴だが、これ以外にも通信内容やトラフィック状況などの監視およびこれらに応じた接続の許可/拒否を行う機能や、認証やアクセス制限、暗号化などのセキュリティ機能も備えている。今回はこれらの機能のうち、トラフィックの監視や接続管理機能について紹介していく。

ポリシー/テレメトリ機能を提供する「mixer」とプラグイン機構「Adapter」

Istioでは、通信内容やトラフィックを監視する機能を「Telemetry(テレメトリ)」、通信内容に応じて接続を許可/拒否する機能を「Policies/Policy(ポリシー)」と呼んでおり、「mixer」というコンポーネントでこれを実現している。mixerで実行できる処理は大きく分けて次の4つだ。

  • トラフィックに関する情報のファイルなどへの出力(ロギング)
  • 監視ツールへのトラフィック情報の転送(テレメトリ)
  • 接続頻度に応じたトラフィック量の調整(クォータ)
  • 接続の可否の判断(認証)

なお、テレメトリ機能でやり取りされる各種統計情報は「メトリック」と呼ばれ、一般的には送信元/送信先、送信時刻といった情報が含まれる。Istioではこれ以外にも任意の情報をメトリックとして扱うことが可能だ。

Istioではテレメトリおよびポリシー機能を「Adapter」と呼ばれるプラグイン機構を使って実装している。Istioが標準で提供しているAdapterとしては、次の表1のものがある。

表1 Istio 1.0で提供されている主なAdapter
リソース名 用途 説明
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)。

図1 mixerによるポリシー/テレメトリの実現方法
図1 mixerによるポリシー/テレメトリの実現方法

さて、前述のようにIstioでは多彩なAdapterが用意されているが、Adapterごとに処理を実行する際に必要な情報は異なる。そのため、mixerではトラフィックに関する情報をAdapterに渡す前に加工するためのTemplates(テンプレート)と呼ばれる仕組みが用意されている。

また、どのTemplateを使用して情報を加工し、加工後の情報をどのAdapterに渡すか、どのような条件でAdapterでの処理を実行するか、という設定はRules(Rule)というリソースで管理する。なお、「どのAdapterを利用するか」を指定するリソースは「Handler」、「どのテンプレートを使ってデータを処理するか」を指定するリソースは「Instance」と呼ばれる。「Handler」や「Instance」というタイプのリソースが存在するわけではなく、使用するAdapterやTemplateごとに対応するリソースタイプが存在する点に注意したい。HandlerやInstance、Ruleの関係は図2のようになっている。

図2 HandlerおよびInstance、Ruleの関係
図2 HandlerおよびInstance、Ruleの関係

デフォルトで利用できるテレメトリツール「Prometheus」

前回記事ではhelmを使ってIstioをインストールする流れを紹介したが、この場合デフォルトの設定では「Prometheus」という監視ツールがデプロイされ、いくつかの情報が自動的にPrometheusに送信されるようテレメトリ関連の設定が追加される。まずはこれらのデータをPrometheusで確認してみよう。

今回使用するテスト環境

今回はテストのため、前回記事で紹介したnginxをフロントエンド、「http-echo」プログラムをバックエンドとしたサービスをあらかじめデプロイしている。詳しくは前回記事を参照して欲しいが、フロントエンドのnginxに対しリクエストを送信すると、nginxはそのリクエストをそのままhttp-echoに転送する。http-echoはリクエストを受信すると、起動時にコマンドラインで指定した文字列をそのままレスポンスとして返すプログラムだ(図3)。

図3 テストで使用したサービス
図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)。

図4 Prometheusのコンソール
図4 Prometheusのコンソール

デフォルト設定では、Prometheusに送信するメトリックとして表2のものが定義されている。

表2 デフォルト設定でPrometheusに格納される情報
名称 タイプ 説明
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)。

図5 Prometheus内では先頭に「istio_」という文字列を付けてデータが格納される
図5 Prometheus内では先頭に「istio_」という文字列を付けてデータが格納される

これらの情報には送信元/送信先に関する情報なども含まれており、コンソールで適切なクエリを行うことでその詳細を確認できる。たとえば「istio_requests_total」と指定して「Execute」をクリックすると、このカウンター値の時間変化がグラフで表示される(図6)。

図6 Prometheusで「istio_requests_total」のカウント数を表示したグラフ
図6 Prometheusで「istio_requests_total」のカウント数を表示したグラフ

ちなみに、これらのデフォルト設定は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)。ここで配列型のデータについては<属性名>["<キー>"]」のように指定することで対応する値を取り出せる。

表3 使用できる主な属性値
変数名 説明
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)
表4 使用できる主な演算子
演算子 意味
== 一致
!= 非一致
|| 論理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
図7 Prometheusから設定したメトリックが確認できる
図7 Prometheusから設定したメトリックが確認できる

なお、データが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)。

図8 redisquotaで指定できるアルゴリズム
図8 redisquotaで指定できるアルゴリズム

今回は、次のような設定を作成した。 ここでは「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のポリシー/テレメトリ設定はやや煩雑であり、設定ミスがあった場合に明確にその原因が表示されないためデバッグがやや難しいという問題や、ドキュメント自体も一部不十分なところがある。そのため活用するには試行錯誤が必要かもしれない。とはいえ柔軟に設定を行うことが可能であり、またポリシー設定は動的に変更できるため、テストなどにも活用できるだろう。