Digital Developer Conference: Hybrid Cloud 2021. On Sep 21, gain free hybrid cloud skills from experts and partners. Register now

Deploy fast, serverless Rust functions

Introduction

This tutorial demonstrates two methods for configuring and deploying serverless Rust actions in IBM Cloud Functions. Rust code for two IBM Cloud Functions actions is provided for demonstration purposes: the insert action for adding new todo tasks into an IBM Cloudant database, and the fetch_all function for retrieving all previously added todo tasks.

Rust is a multi-paradigm programming language that is designed for performance and safety, particularly safe concurrency. Syntactically similar to C++, Rust can provide memory safety without an automated garbage collection system by using a borrow checker to validate reference values. Rust also has an ownership system that enforces an implicit readers-writer lock and checks if all references are valid at compile time, which produces reliable programs with the speed of C or C++.

Sadly, IBM Cloud Functions currently doesn’t support a Rust runtime. Although it’s still possible to create serverless functions that are written in Rust by using some IBM Cloud Functions features and the built-in Docker SDK.

The first method that is demonstrated in this tutorial creates a custom IBM Cloud Functions runtime with the Docker SDK. You prepare a container image with all of the function’s dependencies and host it in a public image repository (such as Docker Hub). IBM Cloud Functions then uses the image that contains your code as the runtime for your action. Because of the underlying container support from IBM Cloud Functions, you not only can create serverless functions in Rust by using this method, but you can also create them in virtually any programming language, whether interpreted or compiled. You also take advantage of some Docker and Rust features to create the smallest container image possible, squeezing milliseconds in start and execution times.

You also learn a second approach to create a Rust serverless function in IBM Cloud Functions. Apache OpenWhisk, which powers IBM Cloud Functions, supports the execution of bash scripts or compiled binaries as “native” serverless functions. In comparison to the first method, this approach comes with some restrictions. However, you can create an action with shorter cold starts and faster execution. The performance gains mainly come from the fact that Rust is able to interoperate with C and take advantage of musl, an extremely clean and efficient standards-conformant libc implementation. The Rust compiler with the musl toolchain is then able to produce a statically linked binary with real-time quality robustness that can be executed in a bare-bones container (you need only a Linux kernel). Because your action doesn’t need any runtime or dynamically linked libraries, IBM Cloud Functions spins it up and executes it really fast!

Prerequisites

  • You must have an active IBM Cloud account. Register if you don’t have one yet.
  • You must have the IBM Cloud CLI installed on your machine. Check the instructions for installing the IBM Cloud CLI for Linux, MacOS, or Windows.
  • You must have Docker installed on your machine and access to Docker Hub. Get Docker for Linux, MacOS, or Windows, and create a Docker Hub account.

Estimated time

It should take you about one hour to complete this tutorial.

Steps

The steps of this tutorial are as follows:

1. Review code requirements

To create valid, serverless functions for IBM Cloud Functions, ensure that the following rules are true for your Rust code:

  1. The program entry point must be called main.
  2. Your function should accept only a single command-line argument as input, and it must be a JSON object encoded as a string.
  3. Your function must return a JSON object as a JSON formatted string sent to stdout (it must be the final log line before the program completes).
  4. Functions that are created from binaries must be self-contained and target x86_64 Linux architecture. In other words, all dependencies and required system libraries must be statically linked in a single executable file.
  5. The produced binary file must be called exec.

The sample code provided for this tutorial already respects these requirements. You can read more about actions that are created from binaries in the official IBM Cloud Functions documentation.

The sample code repository contains the following source directory:

.
├── actions
│   ├── fetch_all.zip
│   └── insert.zip
├── docs
│   └── ...
├── src
│   ├── fetch_all.rs
│   └── insert.rs
├── .gitignore
├── Cargo.toml
├── Dockerfile
└── README.md

The actions folder contains zipped, precompiled actions that you can use to deploy “native” actions in IBM Cloud Functions (if you don’t want to compile the Rust code from scratch).

The Cargo.toml file contains configuration (packages and metadata) for the two Rust programs that are used in this tutorial. The implementation of each function is contained in the /src/fetch_all.rs and /src/insert.rs files. Both functions parse command-line arguments into JSON objects, authenticate with IBM Cloud Identity and Access Management (IAM), interact with an IBM Cloudant database, and then output a JSON string to stdout.

You use the provided Dockerfile to deploy a serverless action with the IBM Cloud Functions Docker SDK.

1.1. The insert function

The insert function is able to insert JSON documents into an IBM Cloudant database. The function first authenticates with IBM Cloud IAM, and then connects to a Cloudant instance and inserts a new record into the tasks database.

The function input is a JSON payload of the record to be inserted, similar to the following example.

{
    "_id": "213635e4-3338-46b3-8252-d085e6b5106c",
    "task": "Get newspaper",
    "done": false
}

1.2. The fetch_all function

The fetch_all function first authenticates with IBM Cloud IAM, then connects to a Cloudant instance and returns a JSON object with metadata from all stored records in the tasks database.

This function does not have any input parameters.

2. Set up your IBM Cloudant database

In this tutorial, you create a Cloudant instance that is accessed through the serverless functions to be deployed. The insert action inserts new JSON documents into a database and the fetch_all action retrieves a list with all of the objects previously stored.

Cloudant is a fully managed and distributed JSON document database, making it a great choice for web and mobile applications. Cloudant APIs and replication protocols are fully compatible with Apache CouchDB.

2.1 Create a Cloudant instance in IBM Cloud

Log in to your IBM Cloud account.

Go to the Cloudant service within the IBM Cloud catalog and create your Cloudant Lite instance with the following options:

  • Select the Multitenant environment.
  • Choose the region that is closest to you from the Available regions list.
  • Enter a name for your Cloudant instance in the Instance name field.
  • Select one of your existing IBM Cloud resource groups from the Resource group list.
  • Enter any tags that you want for your Cloudant instance in the optional Tags field.
  • Choose IAM from the Authentication method list.
  • Confirm that Lite is selected as the Plan.
  • Click Create.

It takes some time for the service to provision. After you wait a few minutes, go to your Resource List and expand Services to view the status of your Cloudant instance. When your instance is ready, Active will display in the Status column, as demonstrated in the following image.

Screen capture of IBM Cloud Resource list

2.2 Create a new database

When your Cloudant instance is active, open it by clicking its name.

On your Cloudant instance management page, click the Dashboard tab. In the Databases section of the page, click Launch to open the Databases dashboard, as indicated by the following image.

Screen capture of a Cloudant instance management page with the Databases segment highlighted

If you are prompted for login information, select Sign in with IBMid.

On the Databases dashboard, click Create Database.

In the Database name field, enter tasks. Then select Non-partitioned and click Create, as indicated in the following picture.

Screen capture of the Create Database pane

2.3 Generate IAM credentials

To communicate with your Cloudant database, you must first authenticate with the IBM Cloud Identity and Access Management (IAM) service. This is done via an HTTP request passing a valid apikey as an input parameter. IBM Cloud IAM will then provide you with a Bearer Token that must be passed on the next API calls to Cloudant for authentication.

After creating the tasks database, return to the Service Details – IBM Cloud web page tab in your browser (your Cloudant instance management page) and click Service credentials on the side panel.

On the Service credentials page, click New credential to create a new credential, as shown in the following screen capture image.

Generating IAM credentials

Within the Create credential dialog, keep the default values that appear in the Name and Role fields, and click Add.

Back on the Service credentials page, click the Copy to clipboard icon that now appears next to your new credential, as shown in the following screen capture image.

Generating IAM credentials

Take note of the generated credentials and keep them safe because they are necessary to complete the tutorial (especially the apikey and url variables). Following is an example of what the IBM Cloudant generated credentials look like.

{
    "apikey": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "host": "xxxxx-xxxxx-xxxx-xxxx-xxxxx-bluemix.cloudantnosqldb.appdomain.cloud",
    "iam_apikey_description": "Auto-generated for key xxxxx-xxxxx-xxxx-xxxx-xxxxx",
    "iam_apikey_name": "cloudant-db",
    "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Writer",
    "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::a/xxxxxxxx",
    "url": "https://xxxxx-xxxxx-xxxx-xxxx-xxxxx-bluemix.cloudantnosqldb.appdomain.cloud",
    "username": "xxxxx-xxxxx-xxxx-xxxx-xxxxx-bluemix"
}

3. Install the cloud-functions plug-in for the IBM Cloud CLI

Open the IBM Cloud CLI installed on your machine.

After you log in to your IBM Cloud account with the CLI and target your chosen region, resource group, and Cloud Foundry org and space, you can list all of the plugins available in the standard repositories by using the following command:

ibmcloud plugin repo-plugins

To work with IBM Cloud Functions, you must install the cloud-functions/wsk/functions/fn plugin. You can do this by using the following command:

ibmcloud plugin install cloud-functions

After the process is complete, you can check if the cloud-functions plugin was installed correctly by executing the following command:

ibmcloud plugin list

Example output:

Listing installed plug-ins...

Plugin Name                                 Version   Status   Private endpoints supported   
cloud-functions/wsk/functions/fn            1.0.49             false   
...<other installed plugins>

To test if the cloud-functions plugin is installed correctly, execute a simple invocation:

ibmcloud fn action invoke /whisk.system/utils/echo -p message hello --result

Example output:

{
    "message": "hello"
}

4. Method 1: Create a Rust action for IBM Cloud Functions with the Docker SDK

This step demonstrates how to deploy the insert function by using the Docker SDK method.

4.1. Prepare a Dockerfile for the custom IBM Cloud Functions runtime

With IBM Cloud Functions, you can write your serverless actions in any language and pack it as a Docker image. Be advised that the container images must be from a public repository. If you want to use private images, check the official IBM Cloud Functions documentation for work-arounds.

The first step is to choose an image, or produce a custom Dockerfile for your Rust runtime in IBM Cloud Functions. Instead of using an already prepared image, this tutorial follows a different approach and demonstrates how to build your own image with a custom Dockerfile.

By inspecting the sample Dockerfile provided for this tutorial, you can see that it is based on two stages (build and runtime):

# --- Build stage
FROM ekidd/rust-musl-builder AS builder
ADD . ./
RUN sudo chown -R rust:rust /home/rust && \
    cargo build
    # add the --release option in `cargo build` for enabling
    # compiler optimizations (it'll cause a much longer compile-time).

# --- Runtime stage
FROM openwhisk/dockerskeleton
ENV FLASK_PROXY_PORT 8080
RUN apk --no-cache add ca-certificates
COPY --from=builder \
    /home/rust/src/target/x86_64-unknown-linux-musl/release/exec \
    /action/exec
CMD ["/bin/bash", "-c", "cd actionProxy && python -u actionproxy.py"]

For the runtime stage, you use the official openwhisk/dockerskeleton image. This is a minimalistic image (197 MB) based on Alpine Linux, and it expects that you provide a single binary called exec with the function code. For the build stage, you use the ekidd/rust-musl-builder image that is based on Ubuntu. This image contains a fully configured Rust development environment and saves you from all the work of configuring a local environment capable of compiling binaries targeting the Alpine Linux architecture.

Your task now is to build and push the final container image with your Rust action into a public image registry. This tutorial demonstrates how to do that by using Docker and Docker Hub. Alternatively, you can build and push the image using Podman and Quay.

First, log in to Docker Hub with the login command and follow the prompt instructions:

docker login --username <your_dockerhub_username>

Clone the sample code repository to your machine by using Git:

git clone https://github.com/IBM/fast-rust-serverless-functions.git

Navigate to the root directory of the cloned repository (which contains the provided Dockerfile) and run the docker build command. Notice that the custom Dockerfile build stage depends on pulling the Ubuntu image (~1.8 GB), so this step may take some time to complete the first time, based on your network and system specs.

docker build -t <your_dockerhub_username>/insert_action:v1 ./

The previous command will build the insert_action:v1 image in your machine’s local registry. To list all of the images in your local Docker registry, execute the following comand:

docker images

Example output:

REPOSITORY                                  TAG     IMAGE ID        CREATED           SIZE
<your_dockerhub_username>/insert_action     v1      a852061613b2    12 seconds ago    212MB

To push the built image to Docker Hub, execute the following command:

docker push <your_dockerhub_username>/insert_action:v1

After the push process is complete, copy the image public repository address. If you pushed it to Docker Hub, the address will be something similar to docker.io/<your_dockerhub_username>/insert_action:v1.

4.2. Register the insert action in IBM Cloud Functions with the Docker SDK

With a custom container image stored in a public repository, you are now able to create a new IBM Cloud Functions action with the IBM Cloud CLI.

The Rust code for the insert function is written to be what’s called a raw web action. A web action is an action that can be invoked without authentication and can be used to implement HTTP handlers that respond with headers, status code, and body content of different types. The “raw” option allows you to manually handle the incoming HTTP request (including headers, instead of only the POST JSON body). For more information, check the official IBM Cloud Functions documentation.

To register a raw web action based on a Docker image at IBM Cloud Functions, you execute the fn action create command passing the --docker option, as shown in the following code block. Do not forget to add the Cloudant credentials that you saved previously (apikey and url) as parameters.

ibmcloud fn action create insert --param iam_apikey <your_cloudant_apikey> --param db_url <your_cloudant_url> --param database tasks --docker docker.io/<your_dockerhub_username>/insert_action:v1 --web raw

Notice that you pass the Cloudant credentials to IBM Cloud Functions with the --param option. This option tells IBM Cloud Functions to append the declared variables as headers in all requests to the created action, so it will not be necessary to hardcode or resend the sensitive information for each request.

If no errors appear after you execute the fn action create command, your serverless action is now ready to be activated.

4.3. Test the insert action

To test the insert action, you must make an HTTP POST request to the web action endpoint. To fetch the generated endpoint, execute the following command:

ibmcloud fn action get insert --url

You can now use an HTTP client to test the action. Using cURL, the command is:

curl -H "Content-Type: application/json" --request POST \
    --data '{"_id":"newspaper-task-id-xyz1234","task":"Get newspaper","done":false}' \
    <your_web_action_endpoint>.json

Example output:

{
    "body": {
        "err": false,
        "inserted_record": {
            "id": "newspaper-task-id-xyz1234",
            "ok": true,
            "rev": "1-add9f181f8f72aeb5a694b751558bb12"
        },
        "msg": "insert execution complete!"
    },
    "statusCode": "200 OK"
}⏎

The action response body contains the inserted document metadata from Cloudant (id, ok, and rev). The rev variable is a revision identifier used by the Cloudant replication protocol. You can read more about Cloudant documents in the product documentation.

5. Method 2: Create a Rust action for IBM Cloud Functions with a static binary

This step demonstrates how to deploy the fetch_all function by using a binary file.

5.1. Set up dependencies and the Rust development environment

To compile a self-contained binary file targeting the correct architecture (x86_64 Linux Musl to be precise), you need several tools installed in your system. For developers using Windows or MacOS, setting up the necessary development environment can be a cumbersome and convoluted process. To assist you in this step, you can use the ekidd/rust-musl-builder image to virtualize a complete Rust development environment.

If you already have a Rust development environment on your machine and don’t want to use the containerized environment, you can leverage the target feature from Rustup to install the x86_64-unknown-linux-musl architecture support for the Rust compiler as follows:

rustup target add x86_64-unknown-linux-musl

Be advised that several common Rust packages depend on system libraries, such as OpenSSL. If you want to use your local Rust tools, you must have all of these libraries installed in your system. Otherwise, the compilation process will fail. This is the main reason why setting up the development environment is a complex process and we highly recommended using the containerized environment. (That already has everything set up for us!)

This tutorial demonstrates the use of the “Dockerized” Rust development environment instead of relying on the local host system tools.

5.2. Compile a static binary targeting the appropriate architecture

First, pull the docker.io/ekidd/rust-musl-builder image if you haven’t already done it while deploying the insert action:

docker pull ekidd/rust-musl-builder

To compile a Rust binary by using the rust-musl-builder image, you must run a container in interactive mode (passing the -it option) and mount a volume in the same directory that contains your Rust code (the Cargo.toml file, /src directory, and so on):

docker run -it -v <path_to_directory_containing_the_rust_code>:/home/rust/src ekidd/rust-musl-builder cargo build

The previous command will take some time to run while Rust compiles your function. After it’s complete, the compiled exec binary will be located in the ./target/x86_64-unknown-linux-musl/debug/fetch_all directory.

Rename the fetch_all file to exec and zip it into the fetch_all.zip file by using Bash:

cp ./target/x86_64-unknown-linux-musl/debug/fetch_all exec && zip fetch_all exec

With the fetch_all.zip file prepared, you can finally register the fetch_all action in IBM Cloud Functions.

5.3. Register the fetch_all action in IBM Cloud Functions with a binary file

Similar to the insert function, the Rust code for the fetch_all function is written to be what’s called a “raw web action”.

To register a raw web action based on a zipped binary executable in IBM Cloud Functions, you must pass the --native option to the fn action create command, as the following command demonstrates. (Notice that the Cloudant credentials are also required for the fetch_all action.)

ibmcloud fn action create fetch_all fetch_all.zip --param iam_apikey <your_cloudant_apikey> --param db_url <your_cloudant_url> --param database tasks --native --web raw

Using --native instead of the --docker approach has some advantages. In particular, you don’t need to push a new image to Docker Hub every time you want to update your action, which saves a lot of time and network resources, especially when you are developing and testing. If you actually have the entire Rust development environment set up on your machine, you don’t need Docker at all to deploy a Rust function into IBM Cloud Functions!

If no errors appear after you execute the fn action create command, the fetch_all serverless action is now ready to be activated.

5.4. Test the fetch_all action

To test the fetch_all action, you must make an HTTP GET request to the web action endpoint. To fetch the generated endpoint for the fetch_all action, execute the following command:

ibmcloud fn action get fetch_all --url

You can then use an HTTP client to test the action. Using cURL, that command is:

curl -H "Content-Type: application/json" --request GET <your_web_action_endpoint>.json

Example output:

{
    "body": {
    "data": {
        "offset": 0,
        "rows": [
            {
                "id": "newspaper-task-id-xyz1234",
                "key": "newspaper-task-id-xyz1234",
                "value": {
                    "rev": "1-add9f181f8f72aeb5a694b751558bb12"
                }
            }
        ],
       "total_rows": 1
    },
    "err": false,
    "msg": "fetch_all execution complete!"
    },
    "statusCode": "200 OK"
}⏎

You can now make more calls with the insert action and then check the new records with the fetch_all function. You can also use the Cloudant Dashboard to validate the results.

6. Analyze activations

Using the fn activation list command, you can view a summary of all the function activations with some details:

ibmcloud fn activation list

Example output:

Datetime            Activation ID                    Kind     Start Duration Status  Entity
2021-03-22 14:35:12 07905a5563c94f3f905a5563c98f3f77 blackbox warm  145ms    success .../fetch_all:0.0.1
2021-03-22 14:35:07 fa852c76f3e548a4852c76f3e5b8a469 blackbox cold  1.457s   success .../fetch_all:0.0.1
2021-03-22 14:06:50 4174351608cd46fdb4351608cdb6fd4f blackbox warm  1.635s   success .../insert:0.0.1
2021-03-22 14:05:22 1375388ff50b4788b5388ff50b8788d0 blackbox cold  3.628s   success .../insert:0.0.1

Note that the cold start activation for the insert action is approximately 3.6 seconds. This includes the creation of the ~212MB insert_action:v1 container, and the execution of the entire function code. The code includes two internal HTTPS requests (IAM authentication and Cloudant query) that considerably add to the execution time. Nevertheless, with a simple test you can roughly estimate that IBM Cloud Functions takes 2 seconds to produce the custom containerized runtime for your Rust code, and 1 second to create the native runtime for the fetch_all action.

You can also check the individual logs from each activation with the following command:

ibmcloud fn activation get <activation_id>

Summary

This tutorial explained how to deploy Rust code as a serverless, raw web action on IBM Cloud Functions by using the Docker SDK and the native approach supported by the underlying OpenWhisk stack, as well as how to monitor your actions with the IBM Cloud CLI cloud-functions plugin. You also learned how to create and operate a NoSQL Cloudant database on IBM Cloud, using REST APIs for interacting with the database by using Rust.

Extend your knowledge by learning about packages and triggers for IBM Cloud Functions actions. You can also learn how to create partitioned databases with Cloudant for even better performance in distributed web and mobile applications.