Istio で main thread と worker thread で Wasm module を動かす

hatappi.blog

前回の記事では最近作成した Wasm module を Envoy 上で動かす部分を解説しました。この記事ではそれを Istio 上で動かす部分を解説します。

Istio の Wasm 周りの歴史

実際の設定に入っていく前に Istio と Wasm の歴史を軽くおさらいしたいと思います。

Istio で Wasm の話がアナウンスに入ったのは 1.5 のアナウンス の認識です。1.5 の前は Mixer を使い外部のテレメトリやモニタリングシステムとの連携を行っていました。しかし Mixier にはパフォーマンスやレイテンシなどの問題があったようです。そのような状況下で Envoy で Wasm を使用した拡張をサポートする議論がはじまりランタイムとして V8 Engine が選択され実装が開始されました。そして 1.5 のアナウンスにあるように Mixier での拡張を Wasm に統合することが発表されました。このあたりの話は公式の「Redefining extensibility in proxies - introducing WebAssembly to Envoy and Istio」が参考になります。

そして Istio 1.9 のリリース では Experimental ではありますが開発者も Wasm module を HTTP で fetch できるようになり module の reload も proxy の restart なしで実現できるようになりました。またこの時点では開発者は EnvoyFilter API を使用して Wasm module を使用する必要がありました。このあたりの話は公式の「Istio and Envoy WebAssembly Extensibility, One Year On」が参考になります。

1.5 がリリースされたから約1年半後ついに 1.12 のリリース で Wasm を使用するためのファーストクラスの WasmPlugin API が導入されました!またこのタイミングで OCI Image のサポートもされたため開発者は OCI 互換のある Docker image を Docker Hub や ECR、GCR などに用意することで今までのように HTTP のエンドポイントを用意する必要もなくなりました。このあたりの話は公式の「Announcing the alpha availability of WebAssembly Plugins」が参考になります。

Istio 上で Wasm module を動かす

前回の記事に書いたように今回作ったものは下記の画像のように main, worker の各スレッドで動かす必要があるので Istio でどのように設定するのかを順番に解説していきます。今回の Istio は 1.13 を使用しています。

worker スレッドで Wasm module を使用する

Worker では WasmPlugin API を使用します。Wasm module の fetch 先として OCI イメージを使用します。そのためまずは イメージの作成を行います。しかし特に複雑なことはなく下記に記載されているように通常の Docker image 作るときのように Dockerfile を記載します。実際に作成した Dockerfile はこちらです。 github.com

作成したイメージは Github Container Registry に push しました。 Package aws-cloudwatch-proxy-wasm/oci-sender · GitHub

ちなみに仕様にそったイメージを作成しない場合、例えば OCI イメージではレイヤーを 1つしか指定できないので base image には scratch を使うのですが、他の alpine などを指定して2つ以上重ねてしまうと下記のようなエラーが表示されます。

could not fetch Wasm OCI image: could not extract Wasm file from the image as Docker container number of layers must be 1 but got 2

最後に WasmPlugin リソースを作成します。 spec.selector でどの workload に対してこの WasmPlugin を使用するかを指定します。次に url に今回 push した OCI のイメージを指定します。そして最後に Wasm module にわたす設定を spec.PluginConfig に指定します。spec.PluginConfig には YAML を指定していますが Wasm module に渡されるときには JSON へと変換されます。

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: aws-cloudwatch-proxy-wasm-sender
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
      istio: ingressgateway
  url: oci://ghcr.io/hatappi/aws-cloudwatch-proxy-wasm/oci-sender:v0.1.0
  imagePullPolicy: IfNotPresent
  pluginConfig:
    receiver_vm_id: aws-cloudwatch-proxy-wasm-receiver

これで worker 側は完了です。今回は public イメージを使用しましたが、private イメージを使用する場合は注意が必要です。1.13のドキュメント には imagePullSecret を指定できるようになっているのですが、実際は 1.13 では未実装となっています。そのため指定しても credential 周りのエラーが表示されて fetch できず悩むことになります。1.14 で導入されたようなので、private イメージを使用したいときは 1.14 を使う必要があります。

main スレッドで Wasm module を使用する

次は main スレッドでの設定です。こちらも WasmPlugin を使用したいところですが、現時点では使用できません。というのも前回の記事に書いたように main スレッド上で動かすために Boostrap configuration の Wasm - envoy.bootstrap.wasm を使用する必要があります。しかし現時点では WasmPlugin は HTTP filter の Wasm を設定するための API のため Bootstrap configuration のためには使用できません。

WasmPlugin は使用できないので今回は EnvoyFilter API を使用します。また EnvoyFilter API で使用する際は OCI image の fetch をサポートしてないので、 HTTP リクエストかファイルで指定する必要があります。今回はファイルで指定する方法を選択しました。

Wasm module をコンテナ経由でわたす

EnvoyFilter で Wasm module をファイルパスで指定するためにはそのファイルをコンテナへ渡す必要があります。今回は Wasm module の入った Docker image を用意して initContainers と emptyDir を使用してそれを実現しました。

  containers:
    ~~~
    volumeMounts:
    - name: aws-cloudwatch-proxy-wasm-vol
      mountPath: /aws-cloudwatch-proxy-wasm
      readOnly: true
  initContainers:
  - name: aws-cloudwatch-proxy-wasm
    image: ghcr.io/hatappi/aws-cloudwatch-proxy-wasm:v0.1.0
    command:
      - cp
      - ./receiver.wasm
      - /aws-cloudwatch-proxy-wasm/
    volumeMounts:
    - name: aws-cloudwatch-proxy-wasm-vol
      mountPath: "/aws-cloudwatch-proxy-wasm"
  volumes:
  - name: aws-cloudwatch-proxy-wasm-vol
    emptyDir: {}

monitoring.ap-northeast-1.amazonaws.com を Cluster に登録

main thread では monitoring.ap-northeast-1.amazonaws.com に対して HTTP リクエストを行いメトリクスを送る処理があります。これを実現するためにはこのエンドポイントを Envoy の Cluster を追加する必要があります。

Istio の場合は ServiceEntry resource で実現することができます。また今回はリクエスト先が HTTPS のため DestinationRule も用意します。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: monitoring-ap-northeast-1-amazonaws-com
  namespace: istio-system
spec:
  hosts:
  - monitoring.ap-northeast-1.amazonaws.com
  location: MESH_EXTERNAL
  ports:
  - number: 443
    name: https
    protocol: TLS
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: monitoring-ap-northeast-1-amazonaws-com
  namespace: istio-system
spec:
  host: monitoring.ap-northeast-1.amazonaws.com
  trafficPolicy:
    tls:
      mode: SIMPLE
      sni: monitoring.ap-northeast-1.amazonaws.com

EnvoyFilter API の作成

あとは下記のような EnvoyFilter resource を作成すれば完了です。設定自体はほとんど前回の記事で紹介した Envoy で動かす時に使用した設定をほぼそのまま使い EnvoyFilter API 固有の適用先の workload などの指定を行います。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: aws-cloudwatch-proxy-wasm-receiver
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      app: istio-ingressgateway
      istio: ingressgateway
  configPatches:
  - applyTo: BOOTSTRAP
    patch:
      operation: MERGE
      value:
        bootstrap_extensions:
        - name: envoy.bootstrap.wasm
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.wasm.v3.WasmService
            singleton: true
            config:
              name: aws-cloudwatch-proxy-wasm-receiver
              vm_config:
                vm_id: "aws-cloudwatch-proxy-wasm-receiver"
                runtime: "envoy.wasm.runtime.v8"
                code:
                  local:
                    filename: "/aws-cloudwatch-proxy-wasm/receiver.wasm"
                environment_variables:
                  host_env_keys:
                    - ACPW_AWS_ACCESS_KEY_ID
                    - ACPW_AWS_SECRET_ACCESS_KEY
              configuration:
                "@type": type.googleapis.com/google.protobuf.StringValue
                value: |
                  {
                    "cloud_watch_region": "ap-northeast-1",
                    "cloud_watch_cluster_name": "outbound|443||monitoring.ap-northeast-1.amazonaws.com"
                  }

Bootstrap configuration に追加した場合 HTTP filter とは違い Pod の restart が必要になります。また Istio はデフォルトの設定で bootstrap configuration を受け取らないようになっています。そのため BOOTSTRAP_XDS_AGENTtrue を設定する必要があります。

istio.io

これで main thread での作業は完了です。

最後に

今回は前回の記事で解説した Wasm module を Istio で動かす方法について記載しました。 main thread で Wasm module を動かしたい時は Bootstrap configuration に追加する必要があるため、少し手間が必要でした。一方 HTTP filter に追加したい場合は WasmPlugin API を使用するだけなので気軽に使えることが分かりました。

今回で拡張の楽しさを感じられたので今後の発展に期待したいです!!