Serverless Rust-ing with Oracle Cloud Functions

Zeroing in on Serverless

Serverless computing is a cloud computing execution model in which the cloud provider allocates machine resources on demand, taking care of the servers on behalf of their customers (Wikipedia). Serverless should not be confused with a common misinterpretation “lack of a server”. Serverless is a way of deploying and running cloud applications where the cloud service provider takes the responsibility of creating VMs, CI/CD, and even running servers, etc., from the user and does not “reserve” resources beforehand. There are multiple components of serverless computing, this story talks about one of the core components — Functions.

Function - The cornerstone of every modular and beautifully designed piece of software.

The era of cloud-native computing has influenced the way we write and ship applications. These can be web applications, batch processing pipelines, data processing pipelines, event handling jobs; the list goes on. One of the rapidly adopted strategies is to write Functions — just the Functions! The idea of writing Functions is not new to developers. We all have been writing Functions one way or the other, but they need some driver code, typically a Server, Cron job, etc. Here’s where managed services like Oracle Cloud Functions come into picture. Essentially developer gives a function to the service and tells it when to invoke it. The service takes it from there — starting a server, adding an event handlers, managing IO and type conversion, etc.

Why Rust and running Rust Functions?

Glad you thought so, I love the way you think. Rust is a secure systems programming language which is blazing fast and memory efficient. It gives ridiculously high control over resources, similar to languages like C and C++, but with the guarantee of secure software resulting in significantly less runtime bugs. Rust’s popularity has been growing in community because of its safety, speed, and resource utilization. Rust functions can result in significantly more performance and better resource utilization.

Overview of Oracle Cloud Functions

Oracle provides a managed service for running functions in Cloud. I will be using the shorthand Oracle Fn for Oracle Cloud Functions from here on. At the time of writing this story, Oracle Fn supports 6 languages officially. These are Go, Java, Kotlin, Python, Node.js, and Ruby. The key thing to notice is that all the code written in these languages gets built and placed in a docker image. This is interesting to the developer because a docker based deployment is supported by the project which means as long as you are following the contract, you can run any language of your choice! Once the docker image is created, it is pushed to Oracle Container Registry. There are a few steps a developer needs to take in order to interact with Oracle Cloud which are explained in detail here. For brevity, we are going to keep things locally and use a development server.

Prerequisites

There are 2 prerequisites for running Functions locally.

  • Latest version of Fn CLI. Fnproject is the base project of Oracle Fn and is maintained by the core team at the time of writing this story.

Steps

  • Using the init image to generate project and boilerplate code.
  • Create a Rust project.
  • Create a Dockerfile for deploying our code.
  • Running Fnproject locally.
  • Create an app and function using Fn CLI.
  • Build and deploy our function.
  • 10,000 feet testing via cURL.

Using the init image

Fn project has a construct called init-images which lets the user run any project using docker as the runtime and generated boilerplate code. The init image takes care of setting up the project, adding the func.yaml file, and creating a Dockerfile. If you use the init image method, feel free to move directly to adding code in main.rs (code present in next section) and then to “Running Fnproject locally.” section.

Running init-image: metamemelord/fdk-rust:init
Executing docker command: run --rm -e FN_FUNCTION_NAME=simple-calc metamemelord/fdk-rust:init
func.yaml created.
.
├── Cargo.toml
├── Dockerfile
├── func.yaml
└── src
└── main.rs
1 directory, 4 files

Create a Rust project

We are going to build a RESTful API based ultra complicated calculator which can Add, Subtract, Multiply, and Divide two numbers. Get it? It’s more complicated than a Hello World program. Okay sorry, let’s keep it dumb for brevity. Please be informed that a function can do far more interesting things than doing arithmetic operations on 2 numbers. Let’s begin:

  • Serde — For serialization and deserialization.
  • FDK — We will be using git directly, alternatively you can use a dependency from crates.io.
#[derive(serde::Deserialize)]
enum Operation {
Add,
Sub,
Mul,
Div,
}
#[derive(serde::Deserialize)]
struct CalculatorInput {
operand1: f64,
operand2: f64,
operation: Operation,
}
#[derive(Serialize)]
struct CalculatorResult {
result: f64,
}
impl CalculatorResult {
fn new(result: f64) -> Self {
Self { result }
}
}
fn simple_calc(input: CalculatorInput) -> CalculatorResult {
match input.operation {
Operation::Add => CalculatorResult::new(input.operand1 + input.operand2),
Operation::Sub => CalculatorResult::new(input.operand1 - input.operand2),
Operation::Mul => CalculatorResult::new(input.operand1 * input.operand2),
Operation::Div => CalculatorResult::new(input.operand1 / input.operand2),
}
}
fn simple_calc(
_: &mut fdk::RuntimeContext,
input: CalculatorInput,
) -> fdk::Result<CalculatorResult> {
Ok(match input.operation {
Operation::Add => CalculatorResult::new(input.operand1 + input.operand2),
Operation::Sub => CalculatorResult::new(input.operand1 - input.operand2),
Operation::Mul => CalculatorResult::new(input.operand1 * input.operand2),
Operation::Div => CalculatorResult::new(input.operand1 / input.operand2),
})
}
#[tokio::main]
async fn main() {
let function = Function::run(simple_calc);
if let Err(e) = function.await {
eprintln!("{}", e);
};
}

Create a Dockerfile

I will be using Rust 1.53 on Alpine for this project

Running Fnproject locally

Fn CLI is shipped with built in capability to do this. We just need to run the command:

2021/05/29 23:26:13 ¡¡¡ 'fn start' should NOT be used for PRODUCTION !!! see https://github.com/fnproject/fn-helm/
...
... // And a few more lines
CONTAINER ID   IMAGE                       COMMAND        CREATED         STATUS         PORTS                                                 NAMES
249baaec435c fnproject/fnserver:latest "./fnserver" 2 minutes ago Up 2 minutes 2375/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp fnserver

Create an app and function using Fn CLI.

The first class citizens of Oracle Functions are apps. An app can have any number of functions. Let’s create an app:

Successfully created app:  calculators
Dockerfile found. Using runtime 'docker'.
func.yaml created.

Build and deploy our function

The Fn CLI can build the function code using fn build command, but we will use the fn deploy command which builds and deploys the function. One thing to note here is that we need to pass the app name as an argument using the --app flag. Additionally, we will also tell the CLI that we are working locally by supplying the --local flag.

Deploying simple-calc to app: calculators
Bumped to version 0.0.2
Building image simple-calc:0.0.2 ..............................................................................................................................
Updating function simple-calc using image simple-calc:0.0.2...
Successfully created function: simple-calc with simple-calc:0.0.2

10,000 feet testing via cURL

To start testing our function, we need an endpoint to invoke. We can obtain the endpoint from the Fn CLI via the fn inspect command.

{
"annotations": {
"fnproject.io/fn/invokeEndpoint": "http://localhost:8080/invoke/<Function ID>"
},
"id": "<Function ID>",
// ... some metadata
}
curl --location --request POST 'http://localhost:8080/invoke/<Function ID>' \
--header 'Content-Type: application/json' \
--data-raw '{"operand1": 1.12,"operand2": 23.5,"operation": "Add"}'
curl --location --request POST 'http://localhost:8080/invoke/<Function ID>' \
--header 'Content-Type: application/xml' \
--data-raw '<CalculatorInput><operand1>2.23</operand1><operand2>1.21</operand2><operation>Sub</operation></CalculatorInput>'
curl --location --request POST 'http://localhost:8080/invoke/<Function ID>' \
--header 'Accept: application/xml' \
--header 'Content-Type: application/json' \
--data-raw '{"operand1": 1.1,"operand2": 23.5,"operation": "Mul"}'
curl --location --request POST 'http://localhost:8080/invoke/<Function ID>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'operand1=10' \
--data-urlencode 'operand2=4' \
--data-urlencode 'operation=Div'

Further reading

Further exploration:

SMTS • Web Services • Rust/Go/Py/JS • Cloud & Distributed Systems