WasmEdge: WebAssembly runtimes are coming for the edge

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 
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(

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

echo "adaltas" | nc localhost 8080


Received 8 bytes



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 
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 using crun
  • 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:


vagrant scp ./ansible/files/echo_server.yml /tmp/echo_server.yml
echo_server.yml                    100%  922   905.3KB/s   00:00
vagrant ssh

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

kubectl apply -f /tmp/echo_server.yml
deployment.apps/echo-server-deployment created
service/echo-server-service created

kubectl wait deployment echo-server-deployment --for condition=Available
deployment.apps/echo-server-deployment condition met

echo "adaltas" | nc localhost 31808
adaltas

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.

Luis Robinson

Next Post

Ingresses and Load Balancers in Kubernetes with MetalLB and nginx-ingress

Wed Dec 28 , 2022
When it comes to exposing services from a Kubernetes cluster and making it accessible from outside the cluster, the recommended option is to use a load-balancer type service to redirect incoming traffic to the right pod. In a bare-metal cluster, using this type of service leaves the EXTERNAL-IP field of […]
Ingresses and Load Balancers in Kubernetes with MetalLB and nginx-ingress

You May Like