Faster, greener, cheaper
Lambda functions with Rust

Luciano Mammino (@loige)

2024-08-20

 

Harder, better, faster, stronger
Lambda functions with Rust

Luciano Mammino (@loige)

2024-08-20

 

Harder, better, faster, stronger
Lambda functions with Rust

Luciano Mammino (@loige)

2024-08-20

This guy has the best title ideas!

Grab the slides

𝕏 loige

Grab the slides

What is RUST?

𝕏 loige

Grab the slides

What is RUST?

𝕏 loige

Grab the slides

OK... Why do I like RUST?

𝕏 loige

Why do I like Rust? ❤️

  • Zero-cost abstractions

  • 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

BTW, I am not the only one who likes Rust... 😏

𝕏 loige

Let's sink our teeth

into some rusty goodness!

𝕏 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!

Why RUST + Lambda?

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

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

👨‍💻 Senior Architect @ fourTheorem

📔 Co-Author of Node.js Design Patterns  👉

Let's connect!

linktr.ee/loige

$ ~ whoami

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

Where do we start?

𝕏 loige

Let's try to create a new function

𝕏 loige

𝕏 loige

𝕏 loige

RUST?!

𝕏 loige

RUST?!

𝕏 loige

EASY PEASY... we just need a custom runtime! 🤗

𝕏 loige

🏃‍♂️ Lambda execution model

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

𝕏 loige

🏃‍♂️ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

response (JSON)
or error

𝕏 loige

𝕏 loige

AWS made a Custom Rust Runtime for Lambda!

𝕏 loige

OK, Where do we start?

install Rust

𝕏 loige

What
next?

install cargo-lambda

𝕏 loige

𝕏 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::init_default_subscriber();

    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!? 😱

🔥 HOT TIP

cargo lambda deploy --enable-function-url

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 and the DX 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! 🚀

Extra 10% discount with the code "BELIEVE" 🤑🤑

Early-access available at

50% discount! 🤑

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

THANKS!

Grab these slides!

𝕏 loige

Harder, better, faster, stronger Lambda functions with Rust - Believe in Serverless

By Luciano Mammino

Harder, better, faster, stronger Lambda functions with Rust - Believe in Serverless

Join AWS serverless hero Luciano Mammino as he gives you the practical details to getting started with Rust in Lambda. Bite-sized functions are the perfect way to learn about this blazing fast programming language that is taking the tech industry by storm. Get ready as Luciano gets rusty and shares the how, what, where, and why you should try it out yourself.

  • 477