WasmEdge: WebAssembly runtimes are coming for the edge
Sep 29, 2022
Never miss our publications about Open Source, big data and distributed systems, low frequency of one email every two months.
With many security challenges solved by design in its core conception, lots of projects benefit from using WebAssembly.
WasmEdge runtime is an efficient Virtual Machine optimized for edge computing. Its main use cases are:
- Jamstack apps, through a static front end with a serverless backend (FaaS)
- Automobiles
- IoT and Stream processing
It is an embeddable virtual machine that can be used as a process, in a process, or orchestrated as a native OCI container (providing an OCI compliant interface).
What is WebAssembly?
WebAssembly (Wasm) is a byte-code meant to be run alongside JavaScript, it is a successor to a few projects designed to speed-up code running in a browser.
Its most notable predecessors are asm.js and Google Native Client (GNI).
Asm.js and GNI had two different philosophies. The former uses a subset of JavaScript, therefore you only need a JavaScript engine to execute it. While the latter is a fully-fledged sandbox meant to be integrated into web browsers.
EmScripten is a tool allowing you to convert LLVM based byte-code (C, C++, Rust) to asm.js.
WebAssembly features are:
- Portable
- Secured
- Performant
- Reduced binary size
The WebAssembly specification defines two languages, the binary (compiled one), and the text one, made to be human-readable.
Here are examples of a code written in Rust and how it translates in WAT (WebAssembly Text).
Rust:
pub fn add_two(a: i32, b: i32) -> i32 {
a + b
}
WAT (WebAssembly Text):
(module
(type $t0 (func (param i32 i32) (result i32)))
(func $addTwo (export "add_two") (type $t0) (param $p0 i32) (param $p1 i32) (result i32)
(i32.add
(local.get $p0)
(local.get $p1))))
WebAssembly extensions
WasmEdge proposes multiple extensions that are either feature or proposed feature of WebAssembly such as WASI.
WASI for example is an extension allowing for interfacing with the system using sockets (TCP/UDP communication) or even accessing the file system with a POSIX-like file I/O.
Here is a list of the current extensions proposed:
- WASI: WebAssembly System Interface
- Reference Types
- Bulk Memory Operations
- SIMD: Single Instruction Multiple Data
Where does WasmEdge come in?
WasmEdge is one of the different runtimes allowing WebAssembly execution on the server-side with a focus on edge computing.
Rust is a first-class citizen of the WebAssembly ecosystem, and as such, is a primary target for WasmEdge.
Here are the highlighted points on their website:
- Performant
- Ahead Of Time (AOT) compilation
- Small footprint
- Portable
- Embeddable
- Orchestrable
- Blockchain
- Extensible
WasmEdge Extensions
WasmEdge proposes a set of extensions allowing more advanced use cases, such as:
WasmEdge Extensions documentation
- TensorFlow, ONNX
- Image Processing
- Key-Value Storage
- Network Sockets
- Command Interface
- Ethereum
- Substrate
Hands-on tutorial
The following section explains how to write your first WasmEdge application, and how to run it in a containarized environment.
Here’s a github repository containing ansible playbooks, and the source code to run every steps shown in this tutorial.
First step: Setting up your environment
git clone https://github.com/adaltas/wasmedge-hands-on-tutorial
cd wasmedge-hands-on-tutorial
Python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
ansible-galaxy collection install -r requirements.yml
vagrant up
./hosts.sh # generates an hosts file with the vm configuration
Inventory:
master ansible_host=192.168.121.117 ansible_port=22 ansible_user=vagrant ansible_private_key_file=/path/to/key
TL;DR
You can run every playbooks by running ansible-playbook ansible/00_all.yml
.
This will perform every action, such as building the project to wasm, run inside a container with and without kubernetes.
Second step: install WasmEdge
To install WasmEdge automatically, run the playbook ansible/01_install_wasmedge.yml
:
ansible-playbook ansible/01_install_wasmedge.yml
PLAY [master] *********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************************************************************
ok: [master]
TASK [Install packages] ***********************************************************************************************************************************************************************************
changed: [master]
TASK [Get wasmedge install script] ************************************************************************************************************************************************************************
changed: [master]
TASK [Install WasmEdge globally] **************************************************************************************************************************************************************************
changed: [master]
PLAY RECAP ************************************************************************************************************************************************************************************************
master : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Third step: Install rust toolchain
Rust is a first class citizen in the WASM ecosystem, during this tutorial, we’ll be using a Rust project.
To install the rust toolchain, run the playbook ansible-playbook ansible/02_install_rust.yml
ansible-playbook ansible/02_install_rust.yml
PLAY [master] *********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************************************************************
ok: [master]
TASK [Check if cargo is installed] ************************************************************************************************************************************************************************
fatal: [master]: FAILED! => changed=true
cmd: command -v /home/vagrant/.cargo/bin/cargo
delta: '0:00:00.001685'
end: '2022-06-12 18:49:36.987147'
msg: non-zero return code
rc: 127
start: '2022-06-12 18:49:36.985462'
stderr: ''
stderr_lines: <omitted>
stdout: ''
stdout_lines: <omitted>
...ignoring
TASK [Download Installer] *********************************************************************************************************************************************************************************
changed: [master]
TASK [Install rust/cargo] *********************************************************************************************************************************************************************************
changed: [master]
TASK [Install wasm32-wasi toolchain] **********************************************************************************************************************************************************************
changed: [master]
PLAY RECAP ************************************************************************************************************************************************************************************************
master : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
Fourth step: Upload and build project
In this step, we’re going to upload and build the project with the following playbook: ansible-playbook ansible/03_build_echo_server.yml
ansible-playbook ansible/03_build_echo_server.yml
PLAY [master] *********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************************************************************
ok: [master]
TASK [Upload echo server project] *************************************************************************************************************************************************************************
changed: [master]
TASK [Build project] **************************************************************************************************************************************************************************************
changed: [master]
PLAY RECAP ************************************************************************************************************************************************************************************************
master : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We are going to compile a sample project written in Rust. It’s a simple TCP server echoing back any data sent.
You can find the source code at: /opt/echo_server/src/main.rs
.
fn main() {
let port = env::var("PORT").unwrap_or_else(|_| String::from("8080"));
let host_address = format!("0.0.0.0:{}", &port);
let listener = TcpListener::bind(&host_address, false).expect("Failed to bind on address");
println!("Listening on {}", host_address);
loop {
let stream = listener.accept(0).unwrap().0;
if let Err(error) = echo(stream) {
println!("Error: {:#?}", error);
};
}
}
fn echo(mut stream: TcpStream) -> Result<()> {
let mut buff = [0u8; 1024];
let mut data = Vec::new();
loop {
let n = stream.read(&mut buff)?;
data.extend_from_slice(&buff[0..n]);
if n < 1024 {
break;
}
}
println!("Received {} bytes", data.len());
stream.write_all(&data)?;
stream.shutdown(Shutdown::Both)?;
Ok(())
}
Before going in the next step, we’re going to step inside the virtual machine to play around with WasmEdge.
vagrant ssh
cd /opt/echo_server
cargo clean
cargo build --release --target wasm32-wasi
Compiling libc v0.2.126
Compiling wasmedge_wasi_socket v0.3.3
Compiling echo_server v0.1.0 (/opt/echo_server)
Finished release [optimized] target(s) in 1.59s
wasmedge target/wasm32-wasi/release/echo_server.wasm
Listening on 0.0.0.0:8080
# Open another terminal, and ssh into the vm
echo "adaltas" | nc localhost 8080
# you can close this terminal
# On the first terminal, a new line should appear
Received 8 bytes
# ctrl + c to stop the server
# the following command will compile to a native library the webassembly
# you get better performances !
wasmedgec target/wasm32-wasi/release/echo_server.wasm echo_server
[2022-06-12 19:02:00.209] [info] compile start
[2022-06-12 19:02:00.222] [info] verify start
[2022-06-12 19:02:00.234] [info] optimize start
[2022-06-12 19:02:01.317] [info] codegen start
[2022-06-12 19:02:02.237] [info] output start
[2022-06-12 19:02:02.239] [info] compile done
[2022-06-12 19:02:02.240] [info] output start
wasmedge echo_server
Listening on 0.0.0.0:8080
Fifth step: Let’s containarize
In this step, we’re going to run our project echo_server
inside a container.
As you know, current container technologies are based on a container runtime that will interface with your operating system kernel. These runtimes are used to run C (a.k.a native) applications. If you run a Python container, in reality, your container runtime will run the Python interpreter (a compiled program -> native) that will run the Python script!
We’re going to use a container runtime that can speak natively with wasm bytecode!
If you look inside the playbook ansible/04_install_container_runtime.yml
, we’ll build a container runtime called crun (an alternative to runc).
N.B: using crun is a personnal choice, you can run WasmEdge application with runc
as well
ansible-playbook ansible/04_install_container_runtime.yml
PLAY [master] *********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************************************************************
ok: [master]
TASK [Install packages to build crun] *********************************************************************************************************************************************************************
changed: [master]
TASK [Clone crun repository] ******************************************************************************************************************************************************************************
changed: [master]
TASK [Generate build configuration] ***********************************************************************************************************************************************************************
changed: [master]
TASK [Build crun] *****************************************************************************************************************************************************************************************
changed: [master]
TASK [Install crun] ***************************************************************************************************************************************************************************************
changed: [master]
PLAY RECAP ************************************************************************************************************************************************************************************************
master : ok=6 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Next, with ansible/05_install_container_tools.yml
, we’re installing buildah (a container building tool) and podman (a rootless container running tool), and configuring podman
to use our custom crun
.
N.B: using buildah
and podman
is a personnal choice, you can get by using only docker
!
ansible-playbook ansible/05_install_container_tools.yml
PLAY [master] *********************************************************************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************************************************************
ok: [master]
TASK [Add OpenSuse apt key] *******************************************************************************************************************************************************************************
changed: [master]
TASK [Add OpenSuse repository] ****************************************************************************************************************************************************************************
changed: [master]
TASK [Install Buildah and Podman] *************************************************************************************************************************************************************************
changed: [master]
TASK [Copy default containers configuration] **************************************************************************************************************************************************************
changed: [master]
TASK [Restart podman] *************************************************************************************************************************************************************************************
changed: [master]
PLAY RECAP ************************************************************************************************************************************************************************************************
master : ok=6 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Finally, our container environment is ready, let’s build a container, and execute some code!
At the root of the echo_server
project, you can find a Containerfile
that will create an image from scratch
(scratch is a noop, there’s no base distribution !) and copy the Wasm bytecode into the image.
FROM scratch
COPY target/wasm32-wasi/release/echo_server.wasm /
CMD ["/echo_server.wasm"]
You can find every steps performed inside the playbook ansible/06_run_in_container.yml
.
Let’s step back inside the virtual machine, and try to build this container.
vagrant ssh
cd /opt/echo_server
buildah bud --annotation "module.wasm.image/variant=compat" -t echo_server:latest
STEP 1/3: FROM scratch
STEP 2/3: COPY target/wasm32-wasi/release/echo_server.wasm /
STEP 3/3: CMD ["/echo_server.wasm"]
COMMIT echo_server:latest
Getting image source signatures
Copying blob 74c551b5129a done
Copying config 8fe3b74406 done
Writing manifest to image destination
Storing signatures
--> 8fe3b74406f
Successfully tagged localhost/echo_server:latest
8fe3b74406fe14983be2d0b579f4a6f68441e49841b2d35a5f9c505724f37ae1
podman image ls
localhost/echo_server latest 8fe3b74406fe 1 minutes ago 2.01 MB
In this step, you can see we annotate the image with module.wasm.image/variant=compat
.
This annotation will be stored inside the image’s manifest. It will allow the container runtime to know this is image is a webassembly program.
As you can see, the image only contains the .wasm
, and therefore is really lightweight.
Then, let’s run the container:
podman run --name echo_server --publish 8080:8080 --detach localhost/echo_server:latest
356b31a98a942b841eaf1eecc861d0b5e94053d76ff498e4d5ad546b18d35b24 # this is different on your vm
echo "adaltas" | nc localhost 8080
adaltas
podman logs echo_server
Listening on 0.0.0.0:8080
Received 8 bytes
Sixth step: Let’s try with Kubernetes
Now that we’re able to run containers speaking webassembly, let’s deploy pods inside Kubernetes.
You will need to deploy the following playbooks before being able to deploy on kubernetes:
ansible/07_install_kubernetes.yml
: Deploy a single kubernetes cluster and setup Containerd as container runtime usingcrun
ansible/08_private_registry.yml
: Configure a private registry to store our container images (OCI format).
Let’s step back inside the virtual machine for the last time:
You can find every steps performed inside the playbook ansible/09_deploy_on_k8s.yml
in the following block:
# install vagrant plugin scp: vagrant plugin install vagrant-scp
vagrant scp ./ansible/files/echo_server.yml /tmp/echo_server.yml
echo_server.yml 100% 922 905.3KB/s 00:00
vagrant ssh
# tagging the image with the private registry we created, then we push the image
podman image tag localhost/echo_server:latest localhost:5000/echo_server:latest
podman push --tls-verify=false localhost:5000/echo_server:latest
Getting image source signatures
Copying blob 74c551b5129a done
Copying config 8fe3b74406 done
Writing manifest to image destination
Storing signatures
# applying deployment
kubectl apply -f /tmp/echo_server.yml
deployment.apps/echo-server-deployment created
service/echo-server-service created
# wait for deployment
kubectl wait deployment echo-server-deployment --for condition=Available
deployment.apps/echo-server-deployment condition met
# sending payload
echo "adaltas" | nc localhost 31808
adaltas
# fetching logs
kubectl logs deployment/echo-server-deployment
Listening on 0.0.0.0:8080
Received 8 bytes
Conclusion
By following the tutorial, you have seen the power of WebAssembly. By bringing a new kind of bytecode, optimised for the edge that can be run everywhere, from the browser to a linux container, WebAssembly brought a new paradigm upon us.
WebAssembly is a powerfull idea that can lead to better performance, better observability by developping instrumenting technologies able to target Wasm bytecode and better operability by allowing to deploy a single technology instead of many.
What’s next for WasmEdge?
Here’s the Roadmap.