roche

Roche is a commandline tool for building rust http and event services using containers.

To see how it works it's recommended to walk through the simple tutorial.

motivation

Services built with Rust have some fantastic runtime qualities for serverless/container scenarios:

  • low resource footprint

  • quick startup time

  • zero garbage collection

However these things come with a trade off as build times are not ideal for rapid application development.

roche addresses this short coming by providing a function as a service pattern for tide that reduces build times to seconds and enables developers to focus on business logic allowing for the speedy delivery of blazing fast and energy efficient software.

It leverages the nesting feature of tide so all that is required to be developed is a handler while the application infrastructure is provided by prebuilt docker containers.

Once the base images are downloaded build times are around 5s for debug and 30s for Release.

Roche is intended to target knative environments but it can also be used to build standard docker containers.

comparisons

There are other opensource tools in this space that provide similar functionality. As they mature converging may be appropriate but currently they have differing constraints and target audiences.

Nametag linelangnotes
roche Roche is a commandline tool for building rust http and event services using containers. rust Targeted Rust use-cases.
Smaller developer pool
http-rs specific
odo odo is a fast, iterative, and straightforward CLI tool for developers who write, build, and deploy applications on Kubernetes and OpenShift. go Focus on multilanguage and enterprise scenarios but no official rust support.
Devfiles specification is still being defined.
boson-project Function as a Service CLI and Client Libray for Knative-enabled Clusters. go Multilanguage but no rust yet
includes knative management
Lacks granular build management

Command Line Tool

roche can be used either as a command line tool or a Rust crate. We will look at prebuilt releases first and then the different options for building from source.

Install From Binaries

Precompiled binaries will be provided on a best-effort basis. Visit the releases page to download the appropriate version for your platform.

Install From Source

roche can also be installed from source

Pre-requisite

roche is written in Rust and therefore needs to be compiled with Cargo. If you haven't already installed Rust, please go ahead and install it now.

If you are new to rust we strongly recommend taking the rustup option at the top of that link.

Install Crates.io version

Installing roche is relatively easy if you already have Rust and Cargo installed. You just have to type this snippet in your terminal:

cargo install roche

This will fetch the source code for the latest release from Crates.io and compile it. If you didn't choose the rustup option you may have to add Cargo's bin directory to your PATH.

Run roche help in your terminal to verify if it works. Congratulations, you have installed roche!

Install Git version

The git version contains all the latest bug-fixes and features, that will be released in the next version on Crates.io, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.

git clone --depth=1 https://github.com/roche-rs/roche.git
cd roche
cargo build --release

The executable roche will be in the ./target/release folder, this should be added to the path.

The init command

The init command is used to generate the initial boilerplate which can range from a simple function file to a predefined template or a full custom rust project. This documentation will step through each one of those scenarios to highlight how to use the init subcommand and options.

To see the complete list of options are available from the commandline by using

roche init --help

simple

If you a looking for a quick end to end guide to get up and running then please refer to the simple tutorial. This section aims to outline some of the detail behind that tutorial.

To generate a simple function the init command is used like this:

roche init

When using the init command without any parameters, a function file will be created for you in the current directory:

.
├── functions.rs

The functions.rs file is the bear minimum required to develop a running application and contains the following code:


#![allow(unused)]
fn main() {
pub fn handler() -> tide::Server<()> {    
    let mut api = tide::new();
    api.at("/").get(|_| async { Ok("Hello, World!") });
    api
}
}

This code can be modified as required and the base image has the following libraries imported as defined in the default repository.

async-std = { version = "1.6.5", features = ["attributes"] }
tide = "0.15.0"
serde = "1.0.117"
serde_urlencoded = "0.7.0"
surf = "2.1.0"
dotenv = "0.15.0"
uuid = { version = "0.8", features = ["v4"] }

To see how these libraries can be used to larger functionality try the functions available in the example library.

When you're ready you can move onto the build step.

predefined template

roche init default
roche-test/
├── Cargo.toml
├── LICENSE
├── README.md
└── src
    ├── functions.rs
    └── lib.rs
  • The src directory contains the functions.rs that is used to build the service. It also contains a lib.rs file which provides a sample implementation that can be used to test the service.

  • The Cargo.toml is for the test library. It is not used by the service.

  • The README.md contains specific instructions for the template

custom template with options

roche init https://github.com/roche-rs/default.git --branch main --name roche-sample

Roche supports all the options of a cargo generate project. This enables you to enable you to create your own templates without having to modify the main roche project. That said if you would like your project to be a predefined template then PRs are welcome.

The branch option allows you to select which branch to choose the default is main if it isn't supplied.

The name option autocompletes the name prompt to enable automation.

When complete the project will look like this.

roche-sample/
├── Cargo.toml
├── LICENSE
├── README.md
└── src
    ├── functions.rs
    └── lib.rs
  • The src directory contains the functions.rs that is used to build the service. It also contains a lib.rs file which provides a sample implementation that can be used to test the service.

  • The Cargo.toml is for the test library. It is not used by the service.

  • The README.md contains specific instructions for the template

The build command

The build command is the key part of the roche development workflow.

It's specifically designed to compress rust build times to seconds rather than minutes.

That said it's intention is to be easy to operate from a developer point of view.

See the complete list of options for build by executing the following

roche build --help

simple

If you a looking for a quick end to end guide to get up and running then please refer to the simple tutorial. This section aims to outline some of the detail behind that tutorial.

In the default scenario build can be executed from either the project root or the same folder with the function file in.

roche build

docker username config By default roche will try and obtain your current docker or podman user to generate the build tag.

Unfortunately older versions of docker on non-linux environments don't output complete docker info.

This can be resolved by either supplying a --tag option or setting a DOCKER_USERNAME environment variable.

details

Behind the scenes there are 4 things going on.

  1. A Dockerfile is created in memory and piped to the docker command on the local machine.

  2. The Dockerfile contains the build system for the function that has been compiled previously so dependencies are ready made.

  3. The final build artefact is then passed to the runtime image to ensure a clean build is achieved.

  4. Docker then applies the tag to the built image.

This is a sample Dockerfile that drives this flow.

FROM quay.io/roche/dev-default:1.1.0 as builder
COPY functions.rs /app-build/src/app
RUN cargo build --release
FROM quay.io/roche/alpine:3.12
RUN apk add --no-cache libgcc
RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser
WORKDIR "/app"
COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/debug/roche-service INCLUDE_ENV ./
USER rocheuser
ENV PORT 8080
EXPOSE 8080

The options allow you to override the two images used in the from clauses as well as the output container tag

roche build \
--buildimage quay.io/roche/dev-default:1.1.0 \
--runtime quay.io/roche/alpine:3.12 \
--tag namespace/buildname:version

environment files

Roche supports environment files. Simply create a .env file in the same location as the functions.rs and it will be included in the development build. See the env example for more details.

The release command

The release command is similar to build but as rust release builds take longer it's not intended to be used as part of the development flow.

It's designed to enable developers to confirm what the released artifact will contain.

See the complete list of options for release by executing the following

roche release --help

simple

If you a looking for a quick end to end guide to get up and running then please refer to the simple tutorial. This section aims to outline some of the detail behind that tutorial.

In the default scenario release can be executed from either the project root or the same folder with the function file in.

roche release

you must ensure that you are logged into docker or podman if you don't provide a --tag option

details

Behind the scenes there are 4 things going on.

  1. A Dockerfile is created in memory and piped to the docker command on the local machine.

  2. The Dockerfile contains the build system for the function that has been compiled previously so dependencies are ready made.

  3. The final build artefact is then passed to the runtime image to ensure a clean build is achieved.

  4. Docker then applies the tag to the built image.

This is a sample Dockerfile that drives this flow.

FROM quay.io/roche/default:1.0.0 as builder
COPY functions.rs /app-build/src/app
RUN cargo build --release
FROM quay.io/roche/alpine:3.12
RUN apk add --no-cache libgcc
RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser
WORKDIR "/app"
COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/release/roche-service ./
USER rocheuser
ENV PORT 8080
EXPOSE 8080

The options allow you to override the two images used in the from clauses as well as the output container tag

roche build \
--buildimage quay.io/roche/default:1.0.0 \
--runtime quay.io/roche/alpine:3.12 \
--tag namespace/buildname:version

environment files

While Roche supports environment files for dev builds they are not included in the release build. This is to prevent sensitive information being managed incorrectly in production environments.

The gen command

The gen command is used to generate a Dockerfile that can be shipped with your function. This enables CI/CD scenarios where the preference is not to allow the roche cli to proliferate outside of development environments.

See the complete list of options for gen by executing the following

roche gen --help

simple

To take the default settings run

roche gen

This will create the following Dockerfile which is identical to the one created during the release command.

FROM quay.io/roche/default:1.0.0 as builder
COPY functions.rs /app-build/src/app
RUN cargo build --release
FROM quay.io/roche/alpine:3.12
RUN apk add --no-cache libgcc
RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser
WORKDIR "/app"
COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/release/roche-service ./
USER rocheuser
ENV PORT 8080
EXPOSE 8080

For more information on how this file see the release documentation.

If you are using a custom baseimage or a custom runtime then these can be overridden using the appropriate options.

roche gen --buildimage quay.io/roche/default:1.0.0 --runtime quay.io/roche/alpine:3.12

By default the Dockerfile is configured to expect the functions.rs file to be in the same folder.

This of course is modifiable by editing the location of the functions.rs in the COPY line. For example you may wish to have the Dockerfile in the root of your project so the line could be edited as follows

COPY src/functions.rs /app-build/src/app

test

The test command copies the functions.rs and lib.rs from the local project to a docker image and executes cargo test --lib in the running container with the output of the test is displayed on the commandline.

See the complete list of options for test by executing the following

roche test --help

simple

If you are looking for guidance in a wider development context then please see the project tutorial.

In the folder with a functions.rs and lib.rs files run the following:

roche test

This will build a testimage and run it. The output will be available on the console.

If something doesn't work as expext the an image is created you can inspect it using the standard docker run -it nameofimage /bin/bash

provide test container and tag

It's unlikely that you will need these options as the test image tag is generated automatically and the test image should be avaiable from the template project.

However options to overide them are provided for usage consistency.

roche test -l quay.io/roche/dev-default:1.4.0 -t mytestimage

Notes If you create a default project a sample integration test is provided.

Once built the project can be tested using the following steps:

  1. Run the container
docker run -p 8080:8080 -t nameofyourbuild
  1. From the template project folder run the cargo test against the container.
cargo test --test '*'

The --test '*' flag is designed to ignore the tests in the lib and only run the tests in the tests folder.

Tutorials

As the capabilities of roche grows these tutorials will provide help on using the features.

  • simple - This is a fast start to go from zero to deployed code in minutes.

  • project - Create a rust project from a template with prebuilt tests.

  • project - Create a project based on mongodb using wither an Document Object Mapper

simple

Before your begin install the roche cli

1. make an empty folder and generate a function template

mkdir tide-faas
cd tide-faas
roche init

This creates a single function file that you can add functionality into.


#![allow(unused)]
fn main() {
pub fn handler() -> tide::Server<()> {    
    let mut api = tide::new();
    api.at("/").get(|_| async { Ok("Hello, World!") });
    api
}
}

2. Login to docker and build the function image.

docker login 
roche build

optionally if you aren't logged or using an imcompatible docker version you can set a DOCKER_USERNAME environment variable

export DOCKER_USERNAME=yourusername
roche build

3. If you would like to run the image use the standard docker commands

docker run -p 8080:8080 registry/namespace/devimagename:version

4. For a release build run the following

$ roche release -t registry/namespace/imagename:version

These take slightly longer as they are compiled with the --release flag

5. Deploy to your favourite container based FaaS platform

First push your container to a container registry

$ docker push registry/namespace/imagename:version

Then deploy into the container service of your choice

knative

kn service create roche-function --image registry/namespace/imagename:version

ibmcloud code engine

ibmcloud ce app create -n roche-function --image registry/namespace/imagename:version

google cloud run

gcloud run deploy roche-function --image registry/namespace/imagename:version

aws fargate

fargate service deploy roche-function --image registry/namespace/imagename:version

project

This tutorial will walk you through modifiying the default project from the default surf request to return some JSON instead. It is intended to get you orientated with the more rust-like developer flow for roche.

Before your begin install the roche cli

1. Initialise a project using the default tempate and provide a name option

roche init default --name sample-project

For more details on this command see the init documentation

2. Move into the project folder and inspect the contents

cd sample-project
tree
.
├── Cargo.toml
├── LICENSE
├── README.md
├── src
│   ├── Dockerfile
│   ├── functions.rs
│   └── lib.rs
└── tests
    └── integration.rs

3. Modify the api to return some JSON.

We are going to replace the contents of the src/functions.rs with the following code:


#![allow(unused)]
fn main() {
use tide::prelude::*; // Pulls in the json! macro.

pub fn handler() -> tide::Server<()> {    
    let mut api = tide::new();
    api.at("/animals").get(|_| async {
        Ok(json!({
            "meta": { "count": 2 },
            "animals": [
                { "type": "cat", "name": "chashu" },
                { "type": "cat", "name": "nori" }
            ]
        }))
    });
    api
}
}

3. Run the test

Now run the tests with roche test to see if we have broken anything.

roche test
...
running 1 test
test run_lib ... FAILED

failures:

---- run_lib stdout ----
thread 'run_lib' panicked at 'assertion failed: resp_string.contains("httpbin.org")', src/lib.rs:11:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    run_lib

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

As expected the test fails.

4. Redefine test

We have broken the tests the response string no longer contains httpbin.org :(

So let's update the src/lib.rs file so that the test pass.

As we are now returning JSON we can use a more formal type to test against.


#![allow(unused)]
fn main() {
pub mod functions;

#[cfg(test)]
#[async_std::test]
async fn run_lib() {
    use tide::prelude::*;
    use tide_testing::TideTestingExt;
    let app = functions::handler();
    let response_json : serde_json::value::Value = app.get("/animals").recv_json().await.unwrap();

    assert_eq!(
        response_json,
        json!({
            "meta": { "count": 2 },
            "animals": [
                { "type": "cat", "name": "chashu" },
                { "type": "cat", "name": "nori" }
            ]
        })
    );
}
}

The TideTestingExt library gives some nice utility features that allow us to exercise routes without running a full http server.

As the container is now so pre-fetched retests will be a lot faster.

Note that the integration tests are ignored. This is by design as they exercise the http endpoint.

The integration tests where the environment where the app is deployed.

There will be a further tutorial on this when we look at an end to end knative workflow.

5. Prepare for CI/CD

Roche supports building releases from the desktop with the release option. However it might be preferable to integrate the code in a more formal CI/CD pipeline.

As we don't want to introduce the roche cli into the CI/CD pipeline it's possible to generate the Dockerfile and include it with your project. This can then be picked up easily by the CI tool.

Just cd into the src folder and run roche gen.

cd src
roche gen

This generates the following document that can be modified to suit the CI requirements.

FROM quay.io/roche/default:1.4.0 as builder
COPY functions.rs /app-build/src
COPY lib.rs /app-build/src
RUN cargo build --release && cargo test --lib --release
FROM quay.io/roche/alpine-libgcc:3.12
RUN addgroup -S rocheuser && adduser -S rocheuser -G rocheuser
WORKDIR "/app"
COPY --from=builder --chown=rocheuser /app-build/run.sh /app-build/Cargo.toml /app-build/target/release/roche-service ./
USER rocheuser
ENV PORT 8080
EXPOSE 8080
CMD ["./run.sh"]

6. That's it!

Hopefully you now have a feel for how roche development with tide can work in a more rust project manner.

mongodb

This tutorial will walk you creating a tide http service using mongodb as a data store in a docker container.

It will use the mongodb template project and outline some of the features that are bundled in the template.

The template is a pre-release build and colaboration on final features and functions would be highly appreciated.

Please take or raise an issue.

Before your begin install the roche cli

1. Initialise a named mongo project

Projects can be initialised using templates or a remote git repo.

Here we are using a predefined template and providing a project name to the prompt.

roche init mongodb --name mongodb-sample

2. Move into the project and inspect the structure

cd mongodb-sample
ls -R
.
├── .rocherc
├── Cargo.toml
├── LICENSE
├── README.md
└── src
    ├── .env
    ├── functions.rs
    └── lib.rs

.rocherc .rocherc contains the images that are used to build the services.

These images store the intialised builds that reduce build times and provide an initial implementation to get up and running with.

Cargo.toml

This Cargo.toml is not used to run the builds but is supplied so that code completion and external integrations tests can be developed.

README.md

The description of the project and how to use it.

src/.env

The .env file is used by the build and test commands to ensure the environent is configured correctly. The values in here can be modified to suite your local development environment.

But the .env file is not shipped in a release build (See 12 Factor Apps) but is passed to the build stages so that tests can run.

src/functions.rs

This contains a some sample code to create a City struct to illustrate creating an Object Document Mapper with wither.

Note the use of the model key word in the struct definintion.


#![allow(unused)]
fn main() {
#[derive(Debug, Model, Serialize, Deserialize)]
pub struct City {
}

The handler function then creates a tide app with the state object defined in the tempate.


#![allow(unused)]
fn main() {
let mut api = tide::with_state(state);
}

A get route is then attached to the app object and the db client is extracted from the state and a call is made to the database.

src/lib.rs

This file contains a complete unit test that will exercise the route that you have defined in functions.rs.


#![allow(unused)]
fn main() {
use tide_testing::TideTestingExt;
let app = functions::handler(engine);
let response_json: serde_json::value::Value = app.get("/").recv_json().await.unwrap();
}

3. run a local mongodb

Before we run the unit test we should start a mongodb instance. Here is a quick command using docker

docker run -p 27017:27017 -v ~/data:/data mongo:4.2

4. run the test

Once the docker instance is up and running you can run the tests with

roche test

Running roche test copies the lib.rs and the function.rs to the test_build_image specified in .rocherc.

Roche then runs cargo test --lib in the container and the output is provided on the console.

roche test
DOCKER_USERNAME environment variable not found trying to docker cli
No tag provided using roche/test-mongodb-sample
Roche: Sent file to builder for -troche/test-mongodb-sample
 Downloading crates ...
  Downloaded tide-testing v0.1.2
   Compiling tide-testing v0.1.2
   Compiling roche-mongodb-service v0.1.0 (/app-build)
    Finished test [unoptimized + debuginfo] target(s) in 11.94s
     Running target/debug/deps/roche_mongodb_service-372fa7f25cd4aa41
Roche: Build complete for -troche/test-mongodb-sample
STEP 1: FROM quay.io/roche/dev-mongodb:1.0.0
STEP 2: COPY . /app-build/src/
--> c27648fb9c7
STEP 3: RUN cargo test --lib 

running 1 test
test run_lib ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

STEP 4: COMMIT roche/test-mongodb-sample
--> 2e6d1366404
2e6d13664048224fa41a024430dd2c513b15c331c294de28cbdf673e090c08d8

Notice that the DOCKER_USERNAME is resolved by roche. If you wish to override this behaviour that a DOCKER_USERNAME=yourusername can be added to .rocherc.

The test also generates a docker image called roche/test-mongodb-sample just incase it fails and more investigation of the build is required.

5. Generate a Dockerfile for CI/CD

In the same folder as the .rocherc run in order to create a Dockerfile.

roche gen

You will notice that the file refers to the release image quay.io/roche/mongodb:1.0.0 as builder.

It also builds and tests the code in release mode and if sucessful it will copy the exe into a minimum configuration image to keep the size of the final artifact to a minimum.

6. Test the Dockerfile

You can run roche release to mimic this functionality but here we are going to test the Dockerfile explicitly.

In the base of the project folder generate the docker file.

roche gen

Now move the docker file into the src folder, then run a docker build with a custom tag.

mv Dockerfile src
cd src
docker build -t mongo-sample-local .

Once the build completes we can run it

docker run --network host \
-e MONGODB_CONNECTION_STRING=mongodb://127.0.0.1:27017 \
-e TIDE_SECRET=tobegeneratedtobegeneratedtobegenerated mongo-sample-local

7. Next Steps

Congratulations on running the mongodb template on your local machine! Next we will look at publishing it into a knative environment.