OpenWhisk and Rust

This blog post is one of a series looking at using Docker Actions in OpenWhisk to support extra runtimes.

Let’s look at writing serverless functions for OpenWhisk using Rust.

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.

Rust has been growing in popularity since it launched in 2010. Rust is a popular language for writing microservices due to the focus on the attention to safety and strong concurrency support.

None of the major serverless platform natively support Rust at the moment. OpenWhisk does not include this as a default runtime. However, recent updates to OpenWhisk provide a path for writing serverless functions with Rust.

Let’s re-write the example from the previous post in Rust and see how to get it running using this new approach…

Have you seen this post explaining how Docker-based Actions work? This post assumes you have already read that first.

Rust Language Actions

Rust has a build system that supports creating static binaries. These binaries contain the application source code and dependent libraries.

Using the same approach as the Go-based example, bundling this binary into a zip file allows us to overwrite the runtime stub prior to invocation.

Runtime binaries will be executed by the Python-based invoker for each invocation. Request parameters will be passed as a JSON string using the first command-line argument. The invoker expects the Action result to be written to standard output as a JSON string.

Action Source Code

Here’s a simple Rust function that returns a greeting string from an input parameter. It parses the JSON string provided on the command-line to look for a name parameter. If this isn’t present, it defaults to stranger. It returns a JSON object with the greeting string (msg) by writing to the console.

extern crate rustc_serialize;
use rustc_serialize::json;
use rustc_serialize::json::Json;
use std::env;

#[derive(RustcDecodable, RustcEncodable)]
pub struct Greeting {
    message: String
}

fn main() {
    let mut name = "stranger".to_string();

    // first arg contains JSON parameters
    if let Some(arg1) = env::args().nth(1) {
        // parse JSON and extract 'name' field
        let params = Json::from_str(&arg1).unwrap();
        if let Some(params_obj) = params.as_object() {
            if let Some(params_name) = params_obj.get("name") {
                name = params_name.as_string().unwrap().to_string();
            }
        }
    };

    let greeting = Greeting {
        message: format!("Hello, {}!", name),
    };

    println!("{}", json::encode(&greeting).unwrap());
}

Set Up Project

Using Rust’s package management tool, create a new project for our serverless function.

Add the source code above into the src/main.rs file.

$ cargo new action; cd action
     Created library `action` project
$ mv src/lib.rs src/main.rs
$ vim src/main.rs
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

This function uses the rustc-serialize crate to handle parsing and producing JSON.

Add this identifier to the project’s dependencies listed in Cargo.toml.

[package]
name = "action"
version = "0.1.0"
authors = ["Me <me@email.com>"]

[dependencies]
rustc-serialize = "0.3"

Build and run the binary to test it works as expected.

$ cargo run
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///private/tmp/test/action)
    Finished debug [unoptimized + debuginfo] target(s) in 7.0 secs
     Running `target/debug/action`
{"message":"Hello, stranger!"}
$ cargo run '{"name": "James"}'
    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/action {\"name\":\ \"James\"}`
{"message":"Hello, James!"}

Before we can deploy this binary to OpenWhisk, it must be compiled for the platform architecture.

Cross-Compiling Locally

Rust’s compiler uses LLVM under the covers, making it possible to generate machine code for different architectures. Cross-compiling for different platforms requires having the correct compiler, linker and libraries for that architecture installed.

Rust recently released a toolchain manager to simplify this process.

Install the Rust toolchain for the x86_64-unknown-linux-musl runtime.

$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

Install musl-based GCC cross-compilers.

$ brew install filosottile/musl-cross/musl-cross

Add the configuration file to set the correct linker for the runtime.

$ cat .cargo/config
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

We can now cross-compile the binary for the correct environment.

$ cargo build --target=x86_64-unknown-linux-musl --release
   Compiling rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///Users/james/code/bluemix/openwhisk-languages/rust/action)
    Finished release [optimized] target(s) in 9.30 secs

Checking the file type demonstrates we have built a static binary for the Linux x86_64 platform.

$ file target/x86_64-unknown-linux-musl/release/action
target/x86_64-unknown-linux-musl/release/action: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, not stripped

Cross-Compiling Using Docker

If you don’t want to install the Rust development toolchain, Docker can be used to start a container with the environment set up.

$ docker pull ekidd/rust-musl-builder
$ docker run -it -v $(pwd):/home/rust/src ekidd/rust-musl-builder cargo build --release
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///home/rust/src)
    Finished release [optimized] target(s) in 1.80 secs
$ file target/x86_64-unknown-linux-musl/release/action
target/x86_64-unknown-linux-musl/release/action: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, not stripped

Create & Deploy Archive

Add the binary to a zip file, ensuring the file is named exec in the archive.

Use the wsk command-line to create a new Docker Action using this archive.

$ cp target/x86_64-unknown-linux-musl/release/action exec
$ zip action.zip exec
  adding: exec (deflated 64%)
$ wsk action create rust_test action.zip --native
ok: created action rust_test

Invoking Action

Test the action from the command-line to verify it works.

$ wsk action invoke rust_test --result
{
    "msg": "Hello, Stranger!"
} 
$ wsk action invoke rust_test --result --param name James
{
    "msg": "Hello, James!"
}

Success 😎.