Vicuna: Serverless rust made simple

At Curology we use the Serverless Framework to build and deploy a myriad of AWS Lambda services that support everything from real-time data ingress to NLP topic labeling. Some of these services are written in Rust and recently we open sourced a piece of this infrastructure as a very small library called Vicuna.

Vicuna is intended to make Rust Serverless applications simple by creating a small middleware API that compliments the serverless-rust plugin. Middleware are just functions. But importantly, they are capable of taking a request or a response and then handling them however you might like. They can also be composed and tested with ease.

Middleware in practice

To illustrate how the library works, let’s look at an example application. Now, this isn’t intended to be a particularly useful middleware, but it will hopefully give you a taste of what’s possible and get you thinking about how you might use the library.

Let’s say we’d like to echo the request back to the HTTP client. To do so, we can write a simple middleware.

        use vicuna::{lambda_http::Response, Handler};

fn echo_request(_handler: Handler) -> Handler {
    Box::new(move |request, _context| {
        Ok(Response::builder().body(request.into_body()).unwrap()))
    })
}

      

Here our echo_request function is a middleware. We can tell it’s a middleware because it’s a function that takes Handler and returns Handler; this is how we define middleware in Vicuna. Our middleware grabs the body from the request and creates a new response with that.

Note that the body of our function is a closure: this is the handler, i.e. the Handler type. The handler function takes in a request and it’s within this scope that we can do all our magic. In more complex middleware, we might alter the request or response, for instance.

Using middleware

Middleware alone are not particularly useful and as we can see they expect this handler parameter. The idea is that we’ll produce some kind of handler which is passed into the middleware. Of course we can supply this ourselves, but fortunately Vicuna provides a starting point for us and we’ll use that here to demonstrate how we tie middleware together.

        use vicuna::{default_handler, Handler, WrappingHandler};use vicuna::lambda_http::{lambda, Response};

fn echo_request(_handler: Handler) -> Handler {
    Box::new(move |request, _context| {
	Ok(Response::builder().body(request.into_body()).unwrap()))
    })
}

fn main() {    lambda!(default_handler().wrap_with(echo_request).handler())}
      

Here we’ve updated our application to use the default_handler function, which as the name suggests provides a handler that’s been initialized to a reasonable default state. We’ve also made use of the WrappingHandler trait which provides the wrap_with method; this is a convenient way of wrapping middleware around handlers.

Vicuna in anger

While this is a contrived example meant to show how the library can be used, it’s worth pointing out that we use Vicuna extensively throughout our data-oriented Serverless applications. These applications have benefited from the flexibility offered by the middleware pattern and are used for filtering, rewriting, and persisting events throughout our systems; they also make for excellent telemetry planes.

Moving our serverless applications into this abstraction has also helped us improve test coverage and overall increased our confidence that our systems are doing what intend. In part this is due to the strongly-typed nature of Rust but it would be fair to say that the simple, intuitive nature of middleware has helped as well.

We hope others will find it useful too.

Join us

We believe great skin should be a fact of life, not a luxury

View open positions