Veltioblog

FAAS in Python, WASM, WASI and Go

Python で簡単な FaaS サーバーを作り、Wasm, WASI, Go でプラグインを作成して動かしてみます。


FAAS in Go with WASM, WASI and Rust - Eli Bendersky's website という投稿を読んで、Python で似たようなことをやるにはどうすれば良いか知るために試してみました。

この記事の内容は以下の通りです。

WASI を使う Wasm の作成方法などは、ここでは説明しません(コードは載せておきます)。

Table of contents

Design

アプリケーションの基本デザインは FAAS in Go with WASM, WASI and Rust - Eli Bendersky's website と同じです。

以下のステップで動作します。

  1. Python web server が HTTP GET リクエストを受け取る。
  2. Python web server が HTTP GET リクエストのパスを解析し、対応する Wasm モジュールをロードし、実行する。
  3. Wasm モジュールは、Python web server から環境変数でデータを受け取って、処理結果を stdout に出力する。
  4. Python web server は、Wasm プラグインの処理結果を stdout から読み取って、HTTP レスポンスとして返す。

受け渡すデータについて

今回は Python と Wasm 間のデータのやり取りを JSON で行います。

サーバー実装

Python (FastAPI) でサーバーを実装します(全体のコードはこちら)。

app = FastAPI()

logging.basicConfig(level="INFO")
logger = logging.getLogger(__name__)


class Item(BaseModel):
    """Item."""

    name: str
    age: int
    description: str


@app.get("/{funcname}")
async def execute(funcname: str) -> Item:
    """Execute wasm function."""
    # refs: https://gist.github.com/pims/711549577759ad1341f1a90860f1f3a5

    wasm_path = f"functions/{funcname}.wasm"
    # なんらかの処理をして Wasm モジュールに渡すデータを作成
    # これはテスト用のデータ
    d = {"name": "zztkm", "age": 20}

    try:
        item_dict = await invoke_wasm_module(funcname, wasm_path, d)
        return Item(**item_dict)
    except Exception:
        logger.exception("error")
        raise HTTPException(status_code=400, detail="Bad Request") from None

このサーバーは、HTTP GET リクエストを受け取り、パスパラメーターを解析し、対応する Wasm モジュールパスを invoke_wasm_module 関数に渡します。 そして、その返り値を Item オブジェクトに詰め替えて HTTP レスポンスとして返します。

Wasm を実行する関数の実装

サーバーの実装ができたので、次に Wasm モジュールを実行する関数を実装します。

async def invoke_wasm_module(modname: str, wasm_path: str, input_data: dict[str, Any]) -> dict[str, Any]:
    """Invoke wasm module."""
    # refs: https://gist.github.com/pims/711549577759ad1341f1a90860f1f3a5

    engine = Engine()
    linker = Linker(engine)
    linker.define_wasi()

    # functions/funcname.wasm が存在するか確認
    try:
        module = Module.from_file(linker.engine, wasm_path)
    except Exception:
        logger.exception("module not found %s", wasm_path)
        raise

    config = WasiConfig()

    # generate tenmporary file for stdout_file
    async with tempfile.NamedTemporaryFile() as f:
        config.stdout_file = f.name
        config.env = [["DATA", json.dumps(input_data)]]

        store = Store(linker.engine)
        store.set_wasi(config)

        instance = linker.instantiate(store, module)
        start = instance.exports(store)["_start"]

        logger.info("start wasm module %s", modname)
        try:
            start(store)
            out = await f.read()
            return json.loads(out.decode(encoding="utf-8"))
        except WasmtimeError as e:
            logger.debug(e)
            out = await f.read()
            return json.loads(out.decode(encoding="utf-8"))

関数の説明

Wasm モジュールの実装

main.go

package main

import (
	"encoding/json"
	"os"
)

type Input struct {
	Name string `json:"name"`
	Age int `json:"age"`
}

type Output struct {
	Name string `json:"name"`
	Age int `json:"age"`
	Description string `json:"description"`
}

func main() {
	d := os.Getenv("DATA")
	var input Input
	json.Unmarshal([]byte(d), &input)

	output := Output{
		Name: input.Name,
		Age: input.Age,
		Description: "This is a description",
	}

	b, _ := json.Marshal(output)
	os.Stdout.Write(b)
}

そして、以下のようにビルドします。

GOOS=wasip1 GOARCH=wasm go build -o gojson.wasm main.go

Go の WASI サポートについては、公式ブログ を読んでみてください。

ここで生成した gojson.wasm を functions ディレクトリに配置して、サーバーを起動します。 起動したら curl で叩いてみます。

$ curl "http://127.0.0.1:8000/gojson"
{"name":"zztkm","age":20,"description":"This is a description"}

上記のような出力が得られたら成功です 😄

今回は、Wasm モジュールをローカルファイルシステムから取得しましたが、S3R2 などのオブジェクトストレージから読み込んでみると、サーバーを停止することなく、機能拡張をすることができるようになります(実際には毎回取得するわけにはいかないのでキャッシュするなど工夫が必要)!

まとめ