iTranslated by AI
Onyx Introduction 3: Writing a Web Server and Deploying to Wasmer Edge
This is a continuation of https://zenn.dev/hatappo/articles/09ff4f8546f805, but I've designed it so that it can be read and tried as a standalone article as much as possible.
I will proceed by referring to the official guide here:
I will use WCGI on Wasmer Edge. Wasmer Edge is an edge computing platform provided by Wasmer. WCGI is a mechanism that allows Wasm to be used like CGI.
Preparation
Please install the Onyx CLI:
Also, install the Wasmer CLI:
-
brew install wasmer(Example installation for Mac)
Project
Create a working directory and set it up as an Onyx project.
$ 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):
The init command creates a configuration file named onyx-pkg.kdl. It's similar to package.json in npm, where dependency libraries and other information are defined.
Add the dependency for the http-server package and download it. onyx package sync is equivalent to npm install in npm.
$ 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") }
Minimal Code
#load "./lib/packages"
use http
use http.server {
Req :: Request,
Res :: Response,
}
main :: () {
router := http.server.router();
// Register a handler for '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"allows you to globally load libraries brought in byonyx package sync. The path./lib/packagesis resolved to point to./lib/packages.onyx. The./lib/packages.onyxfile should contain a list of#loadstatements for each package brought in byonyx package sync, generated by theonyx packagecommand.- Since only
packages.onyxexists directly under.lib, using#load_all "./lib"is also OK[1].
- Since only
- In the
usestatements,Req :: Requestis simply aliasing the function while importing it. You can use it without an alias as long as the symbols used in the subsequent code match.
The code that follows should be relatively easy to understand, as it follows a typical pattern for web applications. The flow is: create a router, add a routing rule, create an HTTP server using that as input, and listen on port 8000.
Local Verification Part 1
Just run onyx run.
$ onyx run main.onyx
[Info][HTTP-Server] Serving on port 8000
Furthermore, check the HTTP response by opening http://localhost:8000/ in a browser or similar.
$ curl http://localhost:8000
HTTP Server in Onyx!%
Local Verification Part 2
Next, we will run it with Wasmer and WASIX.
$ onyx build main.onyx -r wasi -DWASIX -o main.wasm
$ wasmer run --net main.wasm
[Info][HTTP-Server] Serving on port 8000
You can also check the response at localhost:8000.
$ curl http://localhost:8000
HTTP Server in Onyx!%
-
onyx build- The
-roption specifies the runtime. The default isonyx, and you can also specifywasi,js, orcustom. - Although the
-Doption is not mentioned in the documentation, in this case, it seems to specify the inclusion of WASIX extensions[2]. - The
-ooption specifies the name of the generated Wasm binary. It defaults toout.wasmif not specified.
- The
- The
--netoption forwasmer runis an instruction to enable the network features of the host computer. The fact that network features cannot be used unless explicitly specified might be a reflection of the Wasm specification.
Refactoring
We will move the routing that was written inline in the main function out into its own function. By using the #tag directive, the routing function can be discovered.
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): Added a new statement to load theroutefunction. -
(2): Passing theroutefunction to the#tagdirective. It feels like decorators in JS or Python, or annotations in Java. -
(3): Thecollect_routesfunction collects the routes defined with the#tagdirective and sets them in the router. This makes the routing configuration pluggable[3].
Check the behavior as before.
$ onyx run main2.onyx
[Info][Http-Server] Serving on port 8000
$ curl http://localhost:8000
HTTP Server in Onyx!
Deploying to 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) Switching from http.server.tcp to http.server.cgi
}
-
(1):http.server.tcp(&router)was changed tohttp.server.cgi(&router), and the code for starting the server withapp->serve(8000)that followed it has been removed. Note also that since the return value ofhttp.server.tcpis no longer used, receiving it in a variable has been removed as well. Everything else is identical tomain2.onyx.
Build the project.
$ onyx build main3.onyx -r wasi -o my-http-server.wasm
Add a configuration file named wasmer.toml [4]. This is a manifest file for publishing to the Wasmer Registry, containing dependency packages, metadata, and commands to be executed. If writing from scratch, you can use the wasmer init command.
[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"
- Change
package.nameto suit your environment or preference.
$ 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
If you don't have an account, you can create one in the browser.
Also, payment settings are required, so go to https://wasmer.io/payment/setup beforehand to register your card information. Please note the warnings that it is free during the Beta phase [5] and that registering payment information won't lead to unexpected usage charges without your permission.
Create a deployment configuration file.
$ 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'
- Select
HTTP serverforApp type. The rest can mostly be left as defaults.
Deploy the application.
$ 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→ A URL specific to the deployed application. -
Versioned URL: https://*.id.wasmer.app→ A unique URL for each deployment version. -
Admin dashboard: https://wasmer.io/apps/*→ The URL for the application's management dashboard.
Success if "HTTP Server in Onyx!" is displayed when you access the Versioned URL or App URL! 🎉
Supplement - app.yaml
A deployment configuration file named app.yaml should have been generated by wasmer app create [6].
---
kind: wasmer.io/App.v0
name: xxxxx-my-http-server
package: hatappo/my-http-server
debug: false
-
kindspecifies the type and version of this configuration file, but currently, it is fixed towasmer.io/App.v0, andnameis an arbitrary name, so essentially it's not setting much of anything here.
Supplement - File structure at the time of deployment completion
The file structure at this point should look like the following. The files/directories marked with ★ are required for deployment to Wasmer Edge.
$ tree -L 1
.
├── app.yaml # ★
├── lib
├── main.onyx
├── main2.onyx
├── main3.onyx
├── my-http-server.wasm # ★
├── onyx-pkg.kdl # ★
└── wasmer.toml # ★
Supplement - Debugging the deployment
If the deployment doesn't go well, please check your code and try wasmer deploy again.
In my case, I had written:
http.server.cgi(&router);
as
app = http.server.cgi(&router);
and it initially failed with a 400 error, likely due to a compilation error on the Wasmer side.
Supplement - Updating various files by wasmer deploy
- The
wasmer deploycommand may automatically rewrite package version numbers orapp_idin the localapp.yamlandwasmer.toml. It was a bit hard to imagine how to handle this in a deployment pipeline. It might be okay to ignore it, though.
Summary
I created a simple web application using the pkg-http-server library and deployed it to Wasmer Edge. pkg-http-server feels similar to Express in Node.js, and routing seems straightforward to write.
Deploying to Wasmer Edge is quite fast, taking only a few seconds. The command-line tools are well-developed and easy to use. However, investigating errors when they occur might be a bit difficult. Since it's still in Beta, we can definitely look forward to future improvements.
I'm also happy that local execution, including the Onyx CLI, is easy. However, I felt it was very "serverless-development-like" in that I wasn't sure how to start it locally in CGI mode (I haven't looked into it properly yet).
-
The
#load_alldirective loads all*.onyxfiles in the specified directory. It does not recursively read subdirectories. ↩︎ -
Implementation code is around here -> https://github.com/onyx-lang/onyx/blob/31d7afa31165c104224949ed44949a43796f8021/compiler/src/onyx.c#L280-L291 ↩︎
-
The implementation code for how
collect_routesgathers routing can be found here. It seems to scan globally for tags where theroutefunction is used, even across different files, but bothrouteandcollect_routeshave an optionalgroupargument that allows you to narrow the scope. ↩︎ -
Documentation -> https://docs.wasmer.io/registry/manifest ↩︎
-
Documentation -> https://docs.wasmer.io/edge/configuration/app-configuration ↩︎
Discussion