💍

Onyx 入門3: ウェブサーバを書いて Wasmer Edge にデプロイする

2023/12/27に公開

https://zenn.dev/hatappo/articles/09ff4f8546f805 の続きでもあるんですが、できるだけ単独の記事としても読める&試せるようにしています。


公式のこれを参考に進めます。

https://onyxlang.io/docs/guides/http-server

Wasmer Edge の WCGI を使います。Wasmer Edge は Wasmer 社が提供するエッジコンピューティング・プラットフォームです。WCGI は wasm に CGI 的に使えるようにした仕組みです。

https://wasmer.io/posts/announcing-wcgi

事前準備

onyx の CLI を入れておいてください

それと Wasmer の CLI を入れておいてください

  • brew install wasmer (マックの場合のインストール例)

プロジェクト

作業フォルダを掘って、 onyx プロジェクトとしてセットアップ。

$ mkdir -p my-http-server && cd $_

$ onyx package init
Creating new project manifest in ./onyx-pkg.kdl.

Package name:
Package description:
Package url:
Package author:
Package version (0.0.1):

init コマンドによって onyx-pkg.kdl という設定ファイルが作成されています。 npm でいう package.json のようなもので、依存ライブラリなどが書かれます。

http-server パッケージの依存を追加してダウンロード。onyx package sync は npm でいう npm install 相当。

$ onyx package add http-server
       Added  'http-server' version 0.2.24

$ onyx package sync
       Fetch  http://github.com/onyx-lang/pkg-http-server  0.2.24

$ onyx package show
Package name        :
Package description :
Package url         :
Package author      :
Package version     : 0.0.1

Dependencies:
    http-server | Dependency { name = "http-server", version = 0.2.24, source = Git("http://github.com/onyx-lang/pkg-http-server") }

最小限のコード

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
}

main :: () {
    router := http.server.router();

    // 'GET /' に対するハンドラを登録
    router->get("/", (req: &Req, res: &Res) {
        res->html("HTTP Server in Onyx!");
        res->status(200);
        res->end();
    });

    app := http.server.tcp(&router);
    app->serve(8000);
}
  • #load "./lib/packages" によって onyx package sync で入ってきたライブラリを全体的にロードすることができます。 ./lib/packages というパスは ./lib/packages.onyx を指すように解決されます。 ./lib/packages.onyx ファイルには onyx package sync で入ってきた各パッケージを #load する処理の列挙が onyx package コマンドによって作成されているはずです。
    • .lib 直下には packages.onyx しか存在しないため #load_all "./lib" としても OKです[1]
  • use している中で Req :: Request のようにしているのは関数に別名を付けたうえで取り込んでいるだけで別名を付けずにそのまま扱っても以降のコードで扱われるシンボルが対応していれば問題ありません。

それ以降のコードは比較的分かりやすい、ウェブアプリケーションに典型的な書き方ではないかと思います。ルータを作成して、ルーティングを1つ追加して、それをインプットにhttpサーバを作って、 ポート8000でListenさせる、という流れです。

ローカルでの動作確認その1

onyx run すればOKです。

$ onyx run main.onyx
[Info][HTTP-Server] Serving on port 8000

さらに http://localhost:8000/ をブラウザで開くなどで HTTP Response を確認します。

$ curl http://localhost:8000
HTTP Server in Onyx!%

ローカルでの動作確認その2

今度は Wasmer と WASIX で動かします。

$ onyx build main.onyx -r wasi -DWASIX -o main.wasm

$ wasmer run --net main.wasm
[Info][HTTP-Server] Serving on port 8000

同じく localhost:8000 でレスポンスを確認できます。

$ curl http://localhost:8000
HTTP Server in Onyx!%
  • onyx build
    • -r オプションは runtime を指定しています。デフォルトが onyx でそれ以外に wasijscustom が指定可能です。
    • -D オプションについてはドキュメントに記載がないですがこの場合は WASIX の拡張を含めるようにする指定のようです[2]
    • -o オプションは生成する wasm バイナリ名です。指定しないと out.wasm になります。
  • wasmer run--net オプションはホストコンピュータのネットワーク機能を有効化する指示です。明示的に指定しないとネットワーク機能が使えないのは wasm の仕様の反映でしょうか。

リファクタリング

main 関数にインラインで書かれていたルーティングを関数として外に出します。 #tag ディレクティブを使うことでルーティング関数を見つけられるようにしています。

main2.onyx

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
    route // (1)
}

#tag route.{ .GET, "/" } // (2)
index :: (req: &Req, res: &Res) {
    res->html("HTTP Server in Onyx!");
    res->status(200);
    res->end();
}

main :: () {
    router := http.server.router();
    router->collect_routes(); // (3)
    app := http.server.tcp(&router);
    app->serve(8000);
}
  • (1): route 関数のロードを新たに追加しています。
  • (2): #tag ディレクティブに route 関数を渡しています。 JS や Python のデコレータ、 Java のアノテーションに近いようなイメージの書き方ですね。
  • (3): collect_routes 関数が #tag ディレクティブで定義されたルーティングを収集しルータに設定します。これで、ルーティングの設定がプラガブルになりました[3]

さっきと同じように動作確認します。

$ onyx run main2.onyx
[Info][Http-Server] Serving on port 8000

$ curl http://localhost:8000
HTTP Server in Onyx!

Wasmer Edge にデプロイする

main3.onyx

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
    route
}

#tag route.{ .GET, "/" }
index :: (req: &Req, res: &Res) {
    res->html("HTTP Server in Onyx!");
    res->status(200);
    res->end();
}

main :: () {
    router := http.server.router();
    router->collect_routes();
    http.server.cgi(&router); // (1) http.server.tcp から http.server.cgi に切り替え
}
  • (1): http.server.tcp(&router)http.server.cgi(&router) に変更し、その次の行にあった app->serve(8000) でサーバを起ち上げる処理を消しています。http.server.tcp の返り値も使わないため変数で受け取っていたのを削除している点にも注意してください。他は main2.onyx とまったく同じです。

ビルドします。

$ onyx build main3.onyx -r wasi -o my-http-server.wasm 

wasmer.toml という設定ファイルを追加します[4]。 Wasmer Registry にパブリッシュするためのマニフェストファイルで、依存パッケージや、メタデータ、実行するコマンドなどを記載します。0から書く場合は wasmer init コマンドを使うといいです。

[package]
name = "hatappo/my-http-server"
version = "0.1.0"
description = "My first HTTP server"
license = "MIT"

[[module]]
name = "server"
source = "my-http-server.wasm"
abi = "wasi"

[[command]]
name = "server"
module = "server"
runner = "https://webc.org/runner/wcgi"

[command.annotations.wasi]
env = ["SCRIPT_NAME=rust_wcgi"]

[command.annotations.wcgi]
dialect = "rfc-3875"
  • package.name は自分の環境や好みに書き換えてください。
$ wasmer login
Opening auth link in your default browser: https://wasmer.io/auth/cli?nonce_id=xxxxx&secret=xxxxx
Waiting for session... Done!
✅ Login for Wasmer user "xxxxx" saved

アカウントがない場合はそのままブラウザ上でアカウント作成します。

また、支払いの設定も必要となるので先に https://wasmer.io/payment/setup に遷移しカード情報の登録などを行います。 Beta の間は無料である[5]こと、決済情報を登録しても許可なく勝手に従量課金されたりはしないこと、が注意書きとして出てきます。

デプロイ用の設定ファイルを作成します。

$ wasmer app create
App type: HTTP server
Who should own this package?: hatappo
Found local package: 'hatappo/my-http-server@0.1.1'
Use package 'hatappo/my-http-server' yes
What should be the name of the app? <NAME>.wasmer.app: xxxxx-my-http-server
Would you like to publish the app now? no
Writing app config to '/xxxxx/lang-onyx/my-http-server/app.yaml'
To (re)deploy your app, run 'wasmer deploy'
  • App typeHTTP server を選んでください。あとはだいたいそのままで。

デプロイします。

$ wasmer deploy                                       
Loaded app from: /xxxxx/my-http-server/app.yaml

Publish new version of package 'hatappo/my-http-server'? yes
Publishing package...

[1/2] ⬆️   Uploading...
[2/2] 📦  Publishing...
Successfully published package `hatappo/my-http-server@0.1.7`
Waiting for package to become available.......
Package 'hatappo/my-http-server@0.1.7' published successfully!

Deploying app xxxxx-my-http-server...

 ✅ App hatappo/xxxxx-my-http-server was successfully deployed!

> App URL: https://xxxxx-my-http-server.wasmer.app
> Versioned URL: https://xxxxx.id.wasmer.app
> Admin dashboard: https://wasmer.io/apps/xxxxx-my-http-server

Waiting for new deployment to become available...
(You can safely stop waiting now with CTRL-C)
..
New version is now reachable at https://xxxxx-my-http-server.wasmer.app
Deployment complete
  • App URL: https://*.wasmer.app → デプロイされたアプリケーション固有のURLです。
  • Versioned URL: https://*.id.wasmer.app → さらにデプロイバージョンごとに固有のURLです。
  • Admin dashboard: https://wasmer.io/apps/* → そのアプリケーションの管理画面のURLです。

Versioned URL か App URL にアクセスして「HTTP Server in Onyx!」が表示されたら成功です🎉

補足 - app.yaml

wasmer app create によって app.yaml というデプロイ設定のファイルが生成されているはずです[6]

---
kind: wasmer.io/App.v0
name: xxxxx-my-http-server
package: hatappo/my-http-server
debug: false
  • kind はこの設定ファイルの種類とバージョンを指定していますが、現在は wasmer.io/App.v0 で固定なのと name は任意の名称なので、実質的にここではたいして何も設定していないようなものです。

補足 - デプロイ完了時点のファイル構成

ファイル構成としてはこの時点で以下のようになっているはずです。 Wasmer Edge へのデプロイに必要なのは のファイル/ディレクトリです。

$ tree -L 1
.
├── app.yaml             # ★
├── lib
├── main.onyx
├── main2.onyx
├── main3.onyx
├── my-http-server.wasm  # ★
├── onyx-pkg.kdl         # ★
└── wasmer.toml          # ★

補足 - デプロイのデバッグ

デプロイがうまくいかなかったらコードを点検するなどして再度 wasmer deploy してみてください。
自分の場合は

http.server.cgi(&router);

app = http.server.cgi(&router);

としてしまっていて、たぶん Wasmer 側でコンパイルエラーになって  400 エラーが見える、という形で最初失敗しました。

補足 - wasmer deploy による各種ファイルの更新

  • wasmer deploy コマンドはローカルの app.yaml と wasmer.toml 内のパッケージバージョン番号や app_id などを自動で書き換えることがあります。 デプロイメントパイプライン上でどう扱うか少しイメージしにくかったです。無視してもいいのかもですが。

まとめ

pkg-http-server ライブラリを使って簡易なウェブアプリケーションを作成し Wasmer Edge にデプロイしました。 pkg-http-server は NodeJS の Express ライクな感じでルーティングは素直に書けそうです。

Wasmer Edge のデプロイは数秒とかなり速いです。コマンドラインツールも充実しており使いやすい。ただエラーになった場合の調査はちょっと難しいのかも。ここらへんはまだ β なのでぜんぜん今後に期待でいいと思います。

onyx の CLI を含めてローカルでの実行が容易なのも嬉しいです。が、結局 cgi モードのローカル起動ってどうすんだろ?というあたりとかはサーバーレス開発っぽいなとは感じました(ちゃんと調べてない)。

脚注
  1. #load_all ディレクティブは指定したディレクトリのすべての *.onyx ファイルをロードします。サブディレクトリを再帰的に読み込むことはしません。 ↩︎

  2. コードとしてはこのあたり → https://github.com/onyx-lang/onyx/blob/31d7afa31165c104224949ed44949a43796f8021/compiler/src/onyx.c#L280-L291 ↩︎

  3. collect_routes がどのようにルーティングを収集するかの実装コードはこちらです。 route 関数が使われているタグを同一ファイル内を超えてグローバルにスキャンするようですが、route にも coolect_route にもオプショナルな引数 group があり、それで範囲を絞ることが可能です。 ↩︎

  4. ドキュメント → https://docs.wasmer.io/registry/manifest ↩︎

  5. https://wasmer.io/posts/wasmer-edge-beta-is-ga ↩︎

  6. ドキュメント → https://docs.wasmer.io/edge/configuration/app-configuration ↩︎

Discussion