OpenWhisk and Rust Lang
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.
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
.
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'
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 --docker
ok: created action rust_test
Invoking Action
Test the action from the command-line to verify it works.
$ wsk action invoke rust_test --blocking --result
{ "msg": "Hello, Stranger!" }
$ wsk action invoke rust_test --blocking --result --param name James
{ "msg": "Hello, James!" }
Success 😎.