Onyx 入門3: ウェブサーバを書いて Wasmer Edge にデプロイする
https://zenn.dev/hatappo/articles/09ff4f8546f805 の続きでもあるんですが、できるだけ単独の記事としても読める&試せるようにしています。
公式のこれを参考に進めます。
Wasmer Edge の WCGI を使います。Wasmer Edge は Wasmer 社が提供するエッジコンピューティング・プラットフォームです。WCGI は wasm に CGI 的に使えるようにした仕組みです。
事前準備
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
でそれ以外にwasi
とjs
とcustom
が指定可能です。 -
-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 type
でHTTP 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 モードのローカル起動ってどうすんだろ?というあたりとかはサーバーレス開発っぽいなとは感じました(ちゃんと調べてない)。
-
#load_all
ディレクティブは指定したディレクトリのすべての*.onyx
ファイルをロードします。サブディレクトリを再帰的に読み込むことはしません。 ↩︎ -
コードとしてはこのあたり → https://github.com/onyx-lang/onyx/blob/31d7afa31165c104224949ed44949a43796f8021/compiler/src/onyx.c#L280-L291 ↩︎
-
collect_routes がどのようにルーティングを収集するかの実装コードはこちらです。 route 関数が使われているタグを同一ファイル内を超えてグローバルにスキャンするようですが、route にも coolect_route にもオプショナルな引数
group
があり、それで範囲を絞ることが可能です。 ↩︎ -
ドキュメント → https://docs.wasmer.io/edge/configuration/app-configuration ↩︎
Discussion