はじめに
先日公開した「Envoy での WebAssembly サポートと WebAssembly Hub, WASM OCI Image Specification について」では、Envoy で WebAssembly がサポートされるまでの背景と WebAssembly Hub、WASM OCI Image Specification を紹介しました。
今回は上記の記事で軽く紹介した wasme を利用して、以下のステップを踏みながら自ら開発した Wasm Filter を Istio により Pod 内に展開された Envoy にデプロイするまでの流れを紹介していきたいと思います。
- Wasm Filter の開発
- Wasm OCI Image のビルド(以下 Wasm イメージ)
- Wasm イメージを WebAssembly Hub で公開
- Wasm Filter を Istio の Envoy にデプロイ
wasme の利用方法
今回は 2020年6月18日 時点での最新バージョン v0.0.20
を利用していきます。
$ wasme --version
wasme version 0.0.20
v0.0.20
でサポートされているサブコマンドは以下となります。
サブコマンド | 概要 |
---|---|
init | Wasm Filter プロジェクトの初期化 |
build | Wasm イメージのビルド |
tag | Wasm イメージのタグ付け |
list | Wasm イメージの一覧表示 |
login | Wasm イメージレジストリへのログイン |
push | Wasm イメージのプッシュ |
pull | Wasm イメージのプル |
deploy | Wasm Filter を Envoy にデプロイ |
undeploy | Wasm Filter を Envoy から削除 |
help | ヘルプ |
wasme は Wasm Filter を Docker コンテナと同じくらい簡単に扱うことを目的に開発されたものなので、Docker の CLI に慣れ親しんでいる人には違和感がないコマンド群だと思います。それでは、上記のコマンドを利用しながら進めていきたいと思います。
wasme のインストール
以下のコマンドで wasme をインストールします。
curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH
Wasm Filter プロジェクトの初期化
以下のコマンドで Wasm Filter プロジェクトの初期化を行います。
wasme init <任意のディレクトリ>
コマンドを実行するとインタラクティブなプロンプトが起動されるので、開発に利用する言語(C++ もしくは AssemblyScript)と Wasm Filter を使用するプラットフォーム(Gloo, Istio 互換のみ)を選択します。
$ wasme init custom-filter
Use the arrow keys to navigate: ↓ ↑ → ←
? What language do you wish to use for the filter:
cpp
▸ assemblyscript
Use the arrow keys to navigate: ↓ ↑ → ←
? With which platforms do you wish to use the filter?:
▸ gloo:1.3.x, istio:1.5.x
✔ assemblyscript
✔ gloo:1.3.x, istio:1.5.x
INFO[0070] extracting 1757 bytes to /Users/ryysud/Desktop/custom-filter
今回は Typescript のサブセットである AssemblyScript ベースで Gloo 1.3.x, Istio 1.5.x に互換性を持つ Wasm Filter を開発していきます。現在は選択できる言語とプラットフォームに限りがありますが、Wasm Filter が盛り上がっていくにつれて充実していくのではないかなと思います。
初期化によって Wasm Filter の開発に必要なファイルが生成されます。
$ cd custom-filter
$ tree .
.
├── assembly
│ ├── index.ts
│ └── tsconfig.json
├── package-lock.json
├── package.json
└── runtime-config.json
assembly/index.ts
が Wasm Filter の実装となっているので、要件にあったものを開発する場合にはこのファイルをベースに処理を追加していく形となります。
以下のように、生成されたファイルが既に HTTP レスポンスに hello
というヘッダーを追加する Wasm Filter になっているので、今回はこれをそのまま使います。
export * from "@solo-io/proxy-runtime/proxy";
import { RootContext, Context, RootContextHelper, ContextHelper, registerRootContext, FilterHeadersStatusValues, stream_context } from "@solo-io/proxy-runtime";
class AddHeaderRoot extends RootContext {
configuration : string;
onConfigure(): bool {
let conf_buffer = super.getConfiguration();
let result = String.UTF8.decode(conf_buffer);
this.configuration = result;
return true;
}
createContext(): Context {
return ContextHelper.wrap(new AddHeader(this));
}
}
class AddHeader extends Context {
root_context : AddHeaderRoot;
constructor(root_context:AddHeaderRoot){
super();
this.root_context = root_context;
}
onResponseHeaders(a: u32): FilterHeadersStatusValues {
const root_context = this.root_context;
if (root_context.configuration == "") {
stream_context.headers.response.add("hello", "world!");
} else {
stream_context.headers.response.add("hello", root_context.configuration);
}
return FilterHeadersStatusValues.Continue;
}
}
registerRootContext(() => { return RootContextHelper.wrap(new AddHeaderRoot()); }, "add_header");
Wasm イメージのビルド
以下のコマンドで Wasm Filter プロジェクトから Wasm イメージのビルドを行います。
$ wasme build assemblyscript -t custom-header:dev .
...
INFO[0012] adding image to cache... filter file=/tmp/wasme784972573/filter.wasm tag="custom-header:dev"
INFO[0012] tagged image digest="sha256:a515a5d244b021c753f2e36c744e03a109cff6f5988e34714dbe725c904fa917" image="docker.io/library/custom-header:dev"
Wasm イメージのビルドには builder という wasme リポジトリに含まれている Docker イメージで起動した Docker コンテナが利用されています。興味がある方は --verbose
オプションを付与して実行すると良いかと思います。
以下のコマンドで Wasm イメージが正常にビルドされたことがわかります。
$ wasme list
NAME TAG SIZE SHA UPDATED
docker.io/library/custom-header dev 12.6 kB a515a5d2 18 Jun 20 00:42 JST
Wasm イメージの実体は ~/.wasme/store
に保存されるので、Wasm イメージがどのようなファイルで構成されているかが気になる方は WASM OCI Image Specification と併せて確認してみると理解が深まるかと思います。
$ ll ~/.wasme/store
total 0
drwxr-xr-x 6 ryysud staff 192B Jun 18 00:42 c9259e4c7263877b1c2bdb729a0c235f
$ ll ~/.wasme/store/c9259e4c7263877b1c2bdb729a0c235f
total 56
-rw-r--r-- 1 ryysud staff 224B Jun 18 00:49 descriptor.json
-rw-r--r-- 1 ryysud staff 12K Jun 18 00:49 filter.wasm
-rw-r--r-- 1 ryysud staff 35B Jun 18 00:49 image_ref
-rw-r--r-- 1 ryysud staff 166B Jun 18 00:49 runtime-config.json
Wasm イメージを WebAssembly Hub で公開
OCI イメージ互換なレジストリであれば何でも良いのですが、今回は WebAssembly Hub で Wasm イメージを公開していきます。事前に WebAssembly Hub にサインアップしたあとで、以下のようにリモートレジストリへのログインを行います。
$ wasme login
Enter username: ryysud
Enter password: ****************************
INFO[0017] Successfully logged in as ryysud (Ryuma Yoshida)
INFO[0017] stored credentials in /Users/ryysud/.wasme/credentials.json
以下のコマンドで先述のステップでビルドした Wasm イメージにタグ付けして WebAssembly Hub にプッシュします。
$ wasme tag custom-header:dev webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0000] tagged image digest="sha256:a515a5d244b021c753f2e36c744e03a109cff6f5988e34714dbe725c904fa917" image="docker.io/library/custom-header:dev"
$ wasme list
NAME TAG SIZE SHA UPDATED
docker.io/library/custom-header dev 12.6 kB a515a5d2 18 Jun 20 00:42 JST
webassemblyhub.io/ryysud/custom-header v0.1 12.6 kB a515a5d2 18 Jun 20 01:00 JST
$ wasme push webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0000] Pushing image webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0007] Pushed webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0007] Digest: sha256:d3e989365d75489e969439f4dc56df517cade0edbf2cda6232536564df910743
WebAssembly Hub にアクセスしてみると正常に Wasm イメージをプッシュできたことがわかります。
https://webassemblyhub.io/user/organizations/227
Web からでなくても、以下のコマンドで wasme から WebAssembly Hub 上の Wasm イメージを検索することも可能です。
$ wasme list --search ryysud/custom-header
NAME TAG SIZE SHA UPDATED
webassemblyhub.io/ryysud/custom-header v0.1 13.8 kB d3e98936 17 Jun 20 16:01 UTC
Wasm Filter を Istio の Envoy にデプロイ
まずは手元に Istio がインストールされた Kubernetes クラスタを作成します。
# Kubernetes クラスタの作成
kind create cluster --name istio-dev --image kindest/node:v1.18.0
# Istio のインストール
istioctl manifest apply --set profile=demo
今回開発した Wasm Filter は Istio 1.5.x に互換性があるものなので 1.5.6
を利用していきます。
$ istioctl version
client version: 1.5.6
control plane version: 1.5.6
data plane version: 1.5.6 (3 proxies)
サンプルアプリとして Bookinfo をデプロイします。
kubectl create ns bookinfo
kubectl label namespace bookinfo istio-injection=enabled --overwrite
kubectl apply -n bookinfo -f https://raw.githubusercontent.com/istio/istio/1.5.6/samples/bookinfo/platform/kube/bookinfo.yaml
以下のコマンドで Bookinfo が正しくデプロイできたか確認します。この時点ではレスポンスに hello
ヘッダーは含まれていないことを覚えておいてください。
$ kubectl exec -ti -n bookinfo deploy/productpage-v1 -c istio-proxy -- curl -I http://details.bookinfo:9080/details/123
HTTP/1.1 200 OK
content-type: application/json
server: istio-envoy
date: Thu, 18 Jun 2020 05:01:33 GMT
content-length: 180
x-envoy-upstream-service-time: 2
x-envoy-peer-metadata: CjcKDElOU1RBTkNFX0lQUxInGiUxMC4yNDQuMC4xMSxmZTgwOjo4OGZiOjhhZmY6ZmU3ZDo1N2Y3CtUBCgZMQUJFTFMSygEqxwEKEAoDYXBwEgkaB2RldGFpbHMKIQoRcG9kLXRlbXBsYXRlLWhhc2gSDBoKNmM5ZjhiY2JjYgokChlzZWN1cml0eS5pc3Rpby5pby90bHNNb2RlEgcaBWlzdGlvCiwKH3NlcnZpY2UuaXN0aW8uaW8vY2Fub25pY2FsLW5hbWUSCRoHZGV0YWlscworCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2lvbhIEGgJ2MQoPCgd2ZXJzaW9uEgQaAnYxChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAolCgROQU1FEh0aG2RldGFpbHMtdjEtNmM5ZjhiY2JjYi1qOGs1OQoXCglOQU1FU1BBQ0USChoIYm9va2luZm8KTwoFT1dORVISRhpEa3ViZXJuZXRlczovL2FwaXMvYXBwcy92MS9uYW1lc3BhY2VzL2Jvb2tpbmZvL2RlcGxveW1lbnRzL2RldGFpbHMtdjEKJQoPU0VSVklDRV9BQ0NPVU5UEhIaEGJvb2tpbmZvLWRldGFpbHMKHQoNV09SS0xPQURfTkFNRRIMGgpkZXRhaWxzLXYx
x-envoy-peer-metadata-id: sidecar~10.244.0.11~details-v1-6c9f8bcbcb-j8k59.bookinfo~bookinfo.svc.cluster.local
x-envoy-decorator-operation: details.bookinfo.svc.cluster.local:9080/*
ここまでで Istio のセットアップは完了したので、以下のコマンドで Istio の Envoy に Wasm Filter をデプロイします。ちなみに v0.0.20
時点でサポートされているデプロイ対象のプラットフォームは Istio と Gloo のみです。
wasme deploy istio webassemblyhub.io/ryysud/custom-header:v0.1 \
--id=custom-header \
--namespace bookinfo
実行結果は以下となります。
$ wasme deploy istio webassemblyhub.io/ryysud/custom-header:v0.1 \
--id=custom-header \
--namespace bookinfo
INFO[0000] cache namespace created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0000] cache configmap created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0000] cache service account created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0000] cache role created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0000] cache rolebinding created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0000] cache daemonset created cache=wasme-cache.wasme image="quay.io/solo-io/wasme:0.0.20"
INFO[0004] added image to cache config... cache="{wasme-cache wasme}" image="webassemblyhub.io/ryysud/custom-header:v0.1"
INFO[0004] waiting for event with timeout 1m0s
INFO[0005] cleaning up cache events for image webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0005] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=details-v1
INFO[0006] created Istio EnvoyFilter resource envoy_filter_resource=details-v1-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=details-v1
INFO[0006] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=productpage-v1
INFO[0006] created Istio EnvoyFilter resource envoy_filter_resource=productpage-v1-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=productpage-v1
INFO[0006] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=ratings-v1
INFO[0008] created Istio EnvoyFilter resource envoy_filter_resource=ratings-v1-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=ratings-v1
INFO[0008] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v1
INFO[0008] created Istio EnvoyFilter resource envoy_filter_resource=reviews-v1-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v1
INFO[0008] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v2
INFO[0009] created Istio EnvoyFilter resource envoy_filter_resource=reviews-v2-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v2
INFO[0009] updated workload sidecar annotations filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v3
INFO[0010] created Istio EnvoyFilter resource envoy_filter_resource=reviews-v3-custom-header.bookinfo filter="id:\"custom-header\" image:\"webassemblyhub.io/ryysud/custom-header:v0.1\" rootID:\"add_header\" " workload=reviews-v3
実行結果のログからわかるように、まず初めに wasme
ネームスペースに Wasm Filter をキャッシュするための DaemonSet などがデプロイされていることがわかります。DaemonSet として実行されるコンテナは wasme cache container と呼ばれるもので、cache container でプルとキャッシュされた Wasm イメージを hostPath で Pod 内の Envoy が利用する形となります。
以下は wasme
ネームスペース内のリソースとなります。
$ kubectl get all -n wasme
NAME READY STATUS RESTARTS AGE
pod/wasme-cache-qnp89 1/1 Running 0 51s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/wasme-cache 1 1 1 1 1 <none> 52s
更に実行結果のログから、Wasm Filter の設定を変換して Istio の EnvoyFilter リソースとして作成することで Istio の Envoy に Wasm Filter をデプロイしていることがわかります。
以下は実際に作成された EnvoyFilter
リソースの例となります。リソースから envoy.wasm.runtime.v8
ランタイムで /var/local/lib/wasme-cache/a515a5d244b021c753f2e36c744e03a109cff6f5988e34714dbe725c904fa917
という Wasm Filter が Envoy に設定されることがわかるかと思います。
$ kubectl get envoyfilters.networking.istio.io -n bookinfo
NAME AGE
details-v1-custom-header 63s
productpage-v1-custom-header 63s
ratings-v1-custom-header 63s
reviews-v1-custom-header 61s
reviews-v2-custom-header 61s
reviews-v3-custom-header 60s
# 余計な情報は削除しています
$ kubectl get envoyfilters.networking.istio.io -n bookinfo productpage-v1-custom-header -o yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: productpage-v1-custom-header
namespace: bookinfo
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: envoy.http_connection_manager
subFilter:
name: envoy.router
patch:
operation: INSERT_BEFORE
value:
config:
config:
name: custom-header
rootId: add_header
vmConfig:
code:
local:
filename: /var/local/lib/wasme-cache/a515a5d244b021c753f2e36c744e03a109cff6f5988e34714dbe725c904fa917
runtime: envoy.wasm.runtime.v8
vmId: custom-header
name: envoy.filters.http.wasm
workloadSelector:
labels:
app: productpage
version: v1
Wasm Filter がデプロイできたので動作確認を行います。
$ kubectl exec -ti -n bookinfo deploy/productpage-v1 -c istio-proxy -- curl -I http://details.bookinfo:9080/details/123
HTTP/1.1 200 OK
content-type: application/json
server: istio-envoy
date: Thu, 18 Jun 2020 05:04:18 GMT
content-length: 180
x-envoy-upstream-service-time: 1
hello: world! # <----- Wasm Filter によって `hello` ヘッダーが追加されている
x-envoy-peer-metadata: CjcKDElOU1RBTkNFX0lQUxInGiUxMC4yNDQuMC4xOSxmZTgwOjpmY2FhOjU4ZmY6ZmUwZjo1ZDU2CtQBCgZMQUJFTFMSyQEqxgEKEAoDYXBwEgkaB2RldGFpbHMKIAoRcG9kLXRlbXBsYXRlLWhhc2gSCxoJYjhiNmM2OTdiCiQKGXNlY3VyaXR5LmlzdGlvLmlvL3Rsc01vZGUSBxoFaXN0aW8KLAofc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtbmFtZRIJGgdkZXRhaWxzCisKI3NlcnZpY2UuaXN0aW8uaW8vY2Fub25pY2FsLXJldmlzaW9uEgQaAnYxCg8KB3ZlcnNpb24SBBoCdjEKGgoHTUVTSF9JRBIPGg1jbHVzdGVyLmxvY2FsCiQKBE5BTUUSHBoaZGV0YWlscy12MS1iOGI2YzY5N2ItcWxsdm0KFwoJTkFNRVNQQUNFEgoaCGJvb2tpbmZvCk8KBU9XTkVSEkYaRGt1YmVybmV0ZXM6Ly9hcGlzL2FwcHMvdjEvbmFtZXNwYWNlcy9ib29raW5mby9kZXBsb3ltZW50cy9kZXRhaWxzLXYxCiUKD1NFUlZJQ0VfQUNDT1VOVBISGhBib29raW5mby1kZXRhaWxzCh0KDVdPUktMT0FEX05BTUUSDBoKZGV0YWlscy12MQ==
x-envoy-peer-metadata-id: sidecar~10.244.0.19~details-v1-b8b6c697b-qllvm.bookinfo~bookinfo.svc.cluster.local
x-envoy-decorator-operation: details.bookinfo.svc.cluster.local:9080/*
レスポンスを見てみると hello
ヘッダーが追加されていることから、Wasm Filter が正常にデプロイできたことが確認できました。以上が、自ら開発した Wasm Filter を Istio により Pod 内に展開された Envoy にデプロイするまでの流れとなります。
おまけ: Wasm Filter を手軽に試す方法
今回は手元で動作確認を行わずに Istio の Envoy に対して Wasm Filter をデプロイしましたが、wasme deploy envoy <Wasmイメージ>
を利用することでローカルに Docker コンテナとして Wasm Filter を適用した Envoy を起動して動作確認を行えるので、開発時に手軽に挙動を確認したい場合はこのコマンドを利用するのが良いかと思います。
$ wasme pull webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0000] Pulling image webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0003] Image: webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0003] Digest: sha256:a515a5d244b021c753f2e36c744e03a109cff6f5988e34714dbe725c904fa917
$ wasme deploy envoy webassemblyhub.io/ryysud/custom-header:v0.1
INFO[0000] mounting filter file at /Users/ryysud/.wasme/store/2006ead26dd93ad64b2a4cf14278550d/filter.wasm
INFO[0000] running envoy-in-docker container_name=add_header envoy_image="quay.io/solo-io/gloo-envoy-wasm-wrapper:1.3.5" filter_image="webassemblyhub.io/ryysud/custom-header:v0.1"
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e6a673f8fa7 quay.io/solo-io/gloo-envoy-wasm-wrapper:1.3.5 "envoy --disable-hot…" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, 0.0.0.0:19000->19000/tcp add_header
$ curl -s -I localhost:8080 | grep hello
hello: world!
さいごに
今回は wasme を利用して、自ら開発した Wasm Filter を Istio により Pod 内に展開された Envoy にデプロイするまでの流れを紹介しました。非常に簡単に Wasm Filter のデプロイを行えるので、興味がある方は早速触ってみるのも良いのかと思います。
次回は Wasme Operator(WebAssembly Hub Operator)を使用して CRD ベースで Istio の Envoy に Wasm Filter をデプロイする方法を調査していきたいと思います。