Your Lambdas,

in Rust!

Luciano Mammino (@loige)

AWS COMMUNITY DAY NORDICS

2024-05-07

Grab the slides

👋 I'm Luciano (🇮🇹🍕🍝🤌)

👨‍💻 Senior Architect @ fourTheorem

📔 Co-Author of Node.js Design Patterns  👉

Let's connect!

linktr.ee/loige

$ ~ whoami

Grab the slides

Always re-imagining

We are a pioneering technology consultancy focused on AWS and serverless

✉️ Reach out to us at  hello@fourTheorem.com

😇 We are always looking for talent: fth.link/careers

We can help with:

Cloud Migrations

Training & Cloud enablement

Building high-performance serverless applications

Cutting cloud costs

𝕏 loige

𝕏 loige

Why do I like Rust? ❤️

  • Performant and memory-safe language

  • Strongly typed with a really good type-system

  • Takes inspiration from Haskell, C++, OCaml, JavaScript, Ruby

  • Great (built-in) package manager (Cargo)

  • Great ecosystem of libraries

  • Pattern matching, no null, Option & Result types

𝕏 loige

use std::env;

fn main() {
    let region = env::var("AWS_REGION");
}





Result<String, VarError>

𝕏 loige

😀 Happy path

🥺 Sad path

use std::env;

fn main() {
    let region = env::var("AWS_REGION");
    
    match region {
        Ok(region) => println!("Selected region: {}", region),
        Err(_) => println!("Error: AWS_REGION not set"),
    }
}

𝕏 loige

😀 Happy path

🥺 Sad path

use std::env;

fn main() {
    let region = env::var("AWS_REGION")
        .expect("AWS_REGION environment variable not set");
}




String

𝕏 loige

If you cannot get the value, panic!

use std::env;

fn main() {
    let region = env::var("AWS_REGION")
        .unwrap_or_else(|_| "eu-west-1".to_string());
}




Rust makes it very hard for you to ignore possible errors or the absence of values.

𝕏 loige

if you cannot get the value, use a default value!

Serverless, in a nutshell 🥜

  • A way of running applications in the cloud

  • Of course, there are servers... we just don't have to manage them

  • Small units of compute (functions), triggered by events

  • We pay (only) for what we use

𝕏 loige

AWS Lambda Pricing 💸

Cost = Allocated Memory 𝒙 time

𝕏 loige

         💰                    🏋️‍♂️                   ⏱️

AWS Lambda Pricing 💸

Cost = Allocated Memory 𝒙 time

𝕏 loige

         💰                    🏋️‍♂️                   ⏱️

Why Rust + Lambda = ❤️

  • Performance + Efficient memory-wise = COST SAVING 🤑
  • Very fast cold starts! (proof) ⚡️
  • Multi-thread safety 💪
  • No null types + Great error primitives = fewer bugs 🐞

𝕏 loige

Supported Lambda runtimes

  • Node.js

  • Python

  • Java

  • .NET

  • Go

  • Ruby

  • Custom

𝕏 loige

Supported Lambda runtimes

  • Node.js

  • Python

  • Java

  • .NET

  • Go

  • Ruby

  • Custom

RUST?!

𝕏 loige

AWS made a Custom Runtime for Lambda!

𝕏 loige

𝕏 loige

🏃‍♂️ Lambda execution model

  • It's serverless: it should run only when needed
  • Lambda code is stored in S3
  • event-based: an event can trigger a lambda execution
  • if no instance is available, one is created on the fly (cold-start)
  • if an instance is available and ready, use that one
  • if an instance is inactive for a while, it gets destroyed

𝕏 loige

🏃‍♂️ Lambda execution model

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

response (JSON)
or error

in detail

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

response (JSON)
or error

in detail

𝕏 loige

This part here is implemented by
aws-lambda-rust-runtime!

OK, Where do we start?

install cargo-lambda

𝕏 loige

𝕏 loige

use aws_lambda_events::event::s3::S3Event;
use lambda_runtime::{run, service_fn, Error, LambdaEvent};

async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> {
    for record in event.payload.records {
        tracing::info!(
            "[{}] Bucket={} Key={}",
            record.event_name.unwrap_or_default(),
            record.s3.bucket.name.unwrap_or_default(),
            record.s3.object.key.unwrap_or_default()
        );
    }
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .with_target(false)
        .without_time()
        .init();

    run(service_fn(function_handler)).await
}

𝕏 loige

𝕏 loige

Request & Response types

𝕏 loige

async fn function_handler(event: LambdaEvent<S3Event>) 
  -> Result<(), Error> {
    // ...
    Ok(())
}

Request

Response

What if we want to use different types? 🤨

𝕏 loige

Option 1

use type definitions in the aws_lambda_events crate

𝕏 loige

𝕏 loige

Processing jobs from SQS

Example

Jobs

𝕏 loige

# Cargo.toml

[dependencies]
aws_lambda_events = { 
  version = "0.15.0",
  default-features = false,
  features = [
    "sqs",
  ]
}

𝕏 loige

use aws_lambda_events::event::sqs::{BatchItemFailure, SqsBatchResponse, SqsEvent};
// ...

async fn function_handler(event: LambdaEvent<SqsEvent>)
  -> Result<SqsBatchResponse, Error> {
    let mut failed_jobs = Vec::with_capacity(event.payload.records.len());

    for record in event.payload.records {
        // process the job
        // ...
        // if the job failed, add it to the failed_jobs list
        failed_jobs.push(BatchItemFailure {
            item_identifier: record.message_id.unwrap_or_default(),
        });
    }

    Ok(SqsBatchResponse {
        batch_item_failures: failed_jobs,
    })
}

// ...

𝕏 loige

Option 2

Create custom request and response types

𝕏 loige

Custom logic in Step Function

Example

𝕏 loige

# Cargo.toml

[dependencies]
serde = "1.0.200"
serde_json = "1.0.116"

𝕏 loige

// ...

#[derive(serde::Deserialize)]
struct Request {
    url: String,
}

#[derive(serde::Serialize)]
struct Response {
    issue_number: u32,
}

async fn function_handler(event: LambdaEvent<Request>) 
  -> Result<Response, Error> {
    println!("I am going to scrape {}", event.payload.url);
    // TODO: actual scraping logic here
    Ok(Response { issue_number: 333 })
}

// ...

𝕏 loige

𝕏 loige

Option 3

Use arbitrary JSON values!

𝕏 loige

// ...

async fn function_handler(
    event: LambdaEvent<serde_json::Value>,
) -> Result<serde_json::Value, Error> {
    let url = event
        .payload
        .as_object()
        .unwrap()
        .get("url")
        .unwrap()
        .as_str()
        .unwrap(); // 🤮
    println!("I am going to scrape {}", url);
    // TODO: actual scraping logic here
    Ok(serde_json::json!({ "issue_number": 333 }))
}

// ...

𝕏 loige

HTTP-based lambdas

𝕏 loige

𝕏 loige

use lambda_http::{run, service_fn, Body, Error, Request, RequestExt, Response};

async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    // Extract some useful information from the request
    let who = event
        .query_string_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    
    let message = format!("Hello {who}, this is an AWS Lambda HTTP request");

    // Return something that implements IntoResponse.
    // It will be serialized to the right response event 
    // automatically by the runtime
    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;
    Ok(resp)
}

𝕏 loige

use lambda_http::{run, service_fn, Body, Error, Request, RequestExt, Response};

async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    // Extract some useful information from the request
    let who = event
        .query_string_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    
    let message = format!("Hello {who}, this is an AWS Lambda HTTP request");

    // Return something that implements IntoResponse.
    // It will be serialized to the right response event 
    // automatically by the runtime
    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;
    Ok(resp)
}

These are just abstractions! 🧐

Lambda is still using JSON behind the scenes.

For HTTP you generally use the
Lambda-Proxy integration.

𝕏 loige

Building & Deploying

cargo lambda build --release && cargo lambda deploy

𝕏 loige

𝕏 loige

NO TRIGGER CONFIGURED! 🙄

WUT!? 😱

SAM

Serverless Application Model

IaC with...

𝕏 loige

AWS SAM + Cargo Lambda

SAM Works with Cargo Lambda (beta feature):

  • Define IaC with the full power of SAM
  • Build and run your Rust lambdas with Cargo Lambda
  • Can simulate API Gateway locally!

Note: Cargo Lambda also works with CDK
(github.com/cargo-lambda/cargo-lambda-cdk)

𝕏 loige

# template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform:
  - AWS::Serverless-2016-10-31

Resources:
  ExampleHttpLambda:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: rust-cargolambda
    Properties:
      CodeUri: .
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - arm64
      Events:
        HttpPost:
          Type: Api
          Properties:
            Path: /
            Method: get
# samconfig.toml

version = 0.1

[default]
[default.global]
[default.global.parameters]
stack_name = "rust-http-lambda"

[default.build.parameters]
beta_features = true
[default.sync.parameters]
beta_features = true

Tells SAM to build using Cargo Lambda

Selects a "custom runtime"

Defines an HTTP trigger
(API Gateway)

Enables SAM beta features

𝕏 loige

Building, Local testing & Deploying

 

sam build
sam local start-api
sam deploy

𝕏 loige

𝕏 loige

𝕏 loige

Closing notes

  • Lambda is great (but you knew that already 😉)
  • Writing Lambdas in Rust is fun and it can be very cost-efficient
  • Still not very common to write Lambdas in Rust, but the tooling is already quite good (Cargo Lambda + SAM / CDK)
  • Go, have fun, share your learnings!

𝕏 loige

BONUS: SAM + Cargo Lambda
a complete example

𝕏 loige

BONUS 2: another complete example

𝕏 loige

Benchmark vs 🐍

🚀 16x faster cold starts

⚡️ 3.5x less memory

🤑 3x cheaper

Do you want more?! 🤔

𝕏 loige

James and I are working on a book! 🚀

 

Do you want more?! 🤔

𝕏 loige

James and I are working on a book! 🚀

Early-access available at

60% discount! 🤑

Do you like this little guy?

Come and ask me for some stickers!

Thanks to @gbinside, @conzy_m, @eoins, and @micktwomey for kindly reviewing this talk!

THANKS!

Grab these slides!

𝕏 loige

Your Lambdas, in Rust - AWS Community Day Nordics

By Luciano Mammino

Your Lambdas, in Rust - AWS Community Day Nordics

Rust is taking the software engineering world by storm, but how does it affect serverless? In AWS it’s not even a supported runtime, so how can we even use it… and should we even try to do that? Spoiler: yes we should and it’s actually quite easy to get started with it!

  • 177