ย 

Optimized Lambda functions
with Rust (Workshop) ๐Ÿ”จ

Luciano Mammino (@loige)

2024-12-12

๐Ÿ‘‹ I'm Lucianoย (๐Ÿ‡ฎ๐Ÿ‡น๐Ÿ•๐Ÿ๐ŸคŒ)

๐Ÿ‘จโ€๐Ÿ’ป Senior Architect @ fourTheorem

๐Ÿ“” Co-Author of Node.js Design Patterns ย ๐Ÿ‘‰

Let's connect!

linktr.ee/loige

$ ~ whoami

๐Ÿ‘‹ I'm Lucianoย (๐Ÿ‡ฎ๐Ÿ‡น๐Ÿ•๐Ÿ๐ŸคŒ)

๐Ÿ‘จโ€๐Ÿ’ป Senior Architect @ fourTheorem

๐Ÿ“” Co-Author of Crafting Lambda Functions in Rust ย ๐Ÿ‘‰

Let's connect!

linktr.ee/loige

$ ~ whoami

Early-access available at

40% discount! ๐Ÿค‘

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

Grab the slides

What is RUST?

Grab the slides

What is RUST?

Grab the slides

OK... Why do I like RUST?

Grab the slides

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

BTW, I am not the only one who likes Rust... ๐Ÿ˜

๐Ÿ˜ผ Repo

๐Ÿ’ฌ Shared Chat

Let's sink our teeth

into some rustyย goodness!

cargo new hello-rust
cargo new hello-rust
cd hello-rust
cargo new hello-rust
cd hello-rust
cargo run

Hello, World!

cargo new hello-rust
cd hello-rust
cargo run

Hello, World!

Handling missing data & errors

null... does not exist in Rust! ๐Ÿ˜ต

you can use ()ย to represent no data

Unit type (empty tuple)

fn some_function() {
	// ... do something
}
fn some_function() -> () {
    // ... do something
}

Implicit

But if you want to represent data that might be set or not, you should use Option

fn main() {
    let items: Vec<u32> = vec![];
    let first_item = items.first();
}
Option<u32>

Some(u32)

Presence of value

None

๐Ÿฅบ Absence of value

fn main() {
    let items: Vec<u32> = vec![];
    let first_item = items.first(); // None
}
fn main() {
    let items: Vec<u32> = vec![1,2,3];
    let first_item = items.first(); // Some(1)
}
fn main() {
    let items: Vec<u32> = vec![1, 2, 3];
    let first_item = items.first();
    match first_item {
        Some(value) => println!("{}", value),
        None => println!("No items"),
    }
}

๐Ÿ˜€ Happy path

๐Ÿฅบ Sad path

fn main() {
    let items: Vec<u32> = vec![1, 2, 3];
    let first_item = items.first();
    println!("{:?}", first_item.unwrap()); // 1
}

If you cannot get the value, panic!

fn main() {
    let items: Vec<u32> = vec![];
    let first_item = items.first();
    println!("{:?}", first_item.unwrap());
}

If you cannot get the value, panic!

thread 'main' panicked at examples/option-unwrap.rs:4:33:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fn main() {
    let items: Vec<u32> = vec![];
    let first_item = items.first();
    println!("{:?}", first_item.expect("No first value"));
}

If you cannot get the value, panic!

thread 'main' panicked at examples/option-expect.rs:4:33:
No first value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fn main() {
    let items: Vec<u32> = vec![];
    let first_item = items.first();
    println!("{:?}", first_item.unwrap_or(&1)); // 1
}

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

use std::env;

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





Result<String, VarError>

๐Ÿ˜€ 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"),
    }
}

๐Ÿ˜€ Happy path

๐Ÿฅบ Sad path

use std::env;

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




String

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.

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

๐Ÿค‘ FREE Resources for learning Rust

my favourite ones!

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 ๐Ÿž

Where do we start?

Let's try to create a new function

RUST?!

RUST?!

EASY PEASY... we justย need a custom runtime! ๐Ÿค—

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

Poll for events

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

๐Ÿƒโ€โ™‚๏ธ Lambda execution model

Runtime

Handler (logic)

Poll for events

event (JSON)

execute

response or
error

response (JSON)
or error

Load the handler code

Infinite loop

AWS made a Custom Rust Runtime for Lambda!

Let's make it simple!

install cargo-lambda

cargo lambda new hello-lambda
cargo lambda new hello-lambda

# Choose HTTP integration!
use lambda_http::{run, service_fn, tracing, Body, Error, Request, RequestExt, Response};

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing::init_default_subscriber();
    run(service_fn(function_handler)).await
}

async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    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");

    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.

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

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing::init_default_subscriber();
    run(service_fn(function_handler)).await
}

async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    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");

    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;

    Ok(resp)
}

Lambda-Proxy integration
Example Request (HTTP Event) ๐Ÿง

{
  "resource": "/my/path",
  "path": "/my/path",
  "httpMethod": "GET",
  "headers": {
    "header1": "value1",
    "header2": "value1,value2"
  },
  "multiValueHeaders": {
    "header1": [
      "value1"
    ],
    "header2": [
      "value1",
      "value2"
    ]
  },
  "queryStringParameters": {
    "parameter1": "value1,value2",
    "parameter2": "value"
  },
  "multiValueQueryStringParameters": {
    "parameter1": [
      "value1",
      "value2"
    ],
    "parameter2": [
      "value"
    ]
  },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "id",
    "authorizer": {
      "claims": null,
      "scopes": null
    },
    "domainName": "id.execute-api.us-east-1.amazonaws.com",
    "domainPrefix": "id",
    "extendedRequestId": "request-id",
    "httpMethod": "GET",
    "identity": {
      "accessKey": null,
      "accountId": null,
      "caller": null,
      "cognitoAuthenticationProvider": null,
      "cognitoAuthenticationType": null,
      "cognitoIdentityId": null,
      "cognitoIdentityPoolId": null,
      "principalOrgId": null,
      "sourceIp": "IP",
      "user": null,
      "userAgent": "user-agent",
      "userArn": null,
      "clientCert": {
        "clientCertPem": "CERT_CONTENT",
        "subjectDN": "www.example.com",
        "issuerDN": "Example issuer",
        "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
        "validity": {
          "notBefore": "May 28 12:30:02 2019 GMT",
          "notAfter": "Aug  5 09:36:04 2021 GMT"
        }
      }
    },
    "path": "/my/path",
    "protocol": "HTTP/1.1",
    "requestId": "id=",
    "requestTime": "04/Mar/2020:19:15:17 +0000",
    "requestTimeEpoch": 1583349317135,
    "resourceId": null,
    "resourcePath": "/my/path",
    "stage": "$default"
  },
  "pathParameters": null,
  "stageVariables": null,
  "body": "Hello from Lambda!",
  "isBase64Encoded": false
}

Lambda-Proxy integration
Example Response ๐Ÿง

{
    "isBase64Encoded": false,
    "statusCode": 200,
    "headers": { 
      "Content-Type": "text/plain"
    },
    "body": "Hello, World"
}

Local Testing

cargo lambda watch
cargo lambda invoke
# 1. example event
# list of supported example events
# https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-events/src/fixtures

cargo lambda invoke --data-example apigw-request


# 2. custom JSON event file

cargo lambda invoke --data-file event.json


# 3. inline

cargo lambda invoke --data-ascii '{"name": "Loige"}'

Building & Deploying

cargo lambda build --release --arm64 && cargo lambda deploy

๐Ÿ”ฅ HOT TIP

cargo lambda deploy --enable-function-url

โœ๏ธ Exercise #1

The SCREAM endpoint!

Write a Lambda that implements an HTTP API that receives a message via query string parameters and responds with that messages capitalized (in the response body).

ย 

Bonus: if no message is provided in the request, return a help message in the response body.

GET https://...?message=Hello
HTTP/1.1
Content-Type: text/plain
Content-Length: 5

HELLO

Different types of
events and responses

async fn function_handler(event: LambdaEvent<EventType>) 
  -> Result<ResponseType, Error> {
    
    let (event, context) = event.into_parts();
    
    // ...
}

Request

Response

Canonical Handler signature

async fn function_handler(event: LambdaEvent<EventType>) 
  -> Result<ResponseType, Error> {
    
    let (event, context) = event.into_parts();
    
    // ...
}

Generic Event Wrapper

Specific Response Type (Success)

Canonical Handler signature

Specific Event Type

async fn function_handler(event: LambdaEvent<EventType>) 
  -> Result<ResponseType, Error> {
    
    let (event, context) = event.into_parts();
    
    // ...
}

Execution context

Canonical Handler signature

Specific Event

(extracted)

use aws_lambda_events::event::s3::S3Event;

async fn function_handler(event: LambdaEvent<S3Event>)
  -> Result<(), Error> {
    let (event, context) = event.into_parts();
    println!("{}", context.request_id);
    
    for record in event.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(())
}

Specific Example (S3 event)

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

Request

Response

What if we want to use different types? ๐Ÿคจ

Option 1

use type definitions in the aws_lambda_events crate

Processing jobs from SQS

Example

Jobs

# Cargo.toml

[dependencies]
aws_lambda_events = { 
  version = "0.15.0",
  default-features = false,
  features = [
    "sqs",
  ]
}
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,
    })
}

// ...

Option 2

Create custom request and response types

Custom logic in Step Function

Example

# Cargo.toml

[dependencies]
serde = "1"
serde_json = "1"
// ...

#[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 })
}

// ...

Option 3

Use arbitrary JSON values!

// ...

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 }))
}

// ...

โœ๏ธ Exercise #2

S3 processing

Write a Lambda that processes S3 events. For every object in the event, print the bucket name, the type of operation, and the object name.

S3 Event

MyBucket - ObjectCreated:Put "Happy Face.jpg"

LOGS

MyBucket - ObjectCreated:Put "lolz.gif"

MyBucket - ObjectCreated:Put "secrets/passwords.txt"

Problems with the current deployment approach...

NO TRIGGER CONFIGURED! ๐Ÿ™„

WUT!? ๐Ÿ˜ฑ

... more problems

  • Configuring our Lambda Function (Memory, timeout, environment variables, permissions, etc.)
  • Creating various resources for our app (S3 buckets, SQS queues, etc.)
  • Wiring different resources together (e.g. pass a DynamoDB table name to a Lambda function)

SAM

Serverless Application Model

IaC with...

What is SAM?

  • A tool that allows you to do IaC (Infrastructure as Code)
  • You can define all your infrastructure in a declarative configuration file (YAML or JSON)
  • SAM will read that file and deploy everything consistently
  • SAM is based on CloudFormation but it offers a more concise syntax for common serverless use cases

SAM speed run ๐Ÿƒ

  • File structure (header, parameters, resources, outputs)
  • References
  • Functions

File structure

# template.yaml
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: nodejs22.x
    Timeout: 180

Description: "This is a cool serverless project"

Parameters:
  ParameterLogicalID:
    Description: Information about the parameter
    Type: string
    Default: somevalue
    AllowedValues:
      - value1
      - value2

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        Variables:
          MESSAGE: "Hello From SAM"

Outputs:
  OutputLogicalID:
    Description: Information about the value
    Value: Value to return
    Export:
      Name: Name of resource to export

๐Ÿ”ฅ HOT TIP AWS extension for VSCode

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)

# 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

Building, Local testing & Deploying

ย 

sam build
sam local start-api
sam deploy

๐Ÿ”ฅ HOT (annoying) TIP

sam validate --lint && sam build --beta-features && sam deploy

You always need to build before deploying
(if you changed something)

Passing configuration to the handler

  • You should load the configuration and initialize clients in the init phase

  • Then, you can pass those to the handler
  • This way you are doing the heavy work only once (per lambda instance)
  • To support this pattern, we need to make our handler "configurable" (i.e. it should receive a config value at every invocation)

๐Ÿ”ฅ Performance HOT TIP

"init once, reuse many"

struct HandlerConfig {
    table_arn: String
}

async fn function_handler(
    config: &HandlerConfig, // <- now we can receive a reference to the config
    _event: LambdaEvent<EventBridgeEvent<Value>>,
) -> Result<(), Error> {
  // ...
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let table_arn = env::var("TABLE_ARN")
          .expect("TABLE_ARN environment variable is not set");
    
    let config = &HandlerConfig {
        table_arn,
    };

    run(service_fn(move |event| async move {
        function_handler(config, event).await
    }))
    .await
}

You can reference other resources (or properties of other resources) in your template using !Refย and !GetAtt

๐Ÿ”ฅ HOT TIP: References in SAM

โœ๏ธ Exercise #3

S3 processing with SAM

Let's update our previous exercise (process files from S3) and let's use SAM.

Use a SAM template to define the S3 bucket and the Lambda function. Pass the bucket name to the lambda function using an environment variable.

Using the AWS SDK for Rust!

cargo add aws-config aws-sdk-dynamodb

List of available libraries

awslabs.github.io/aws-sdk-rust

You will always need the aws-configย crate (credentials and config management)

You will need to install the specific client library for the services you want to us

let table_name = env::var("TABLE_NAME").expect("TABLE_NAME not set");

let config = aws_config::defaults(BehaviorVersion::latest())
  .load()
  .await;
let dynamodb_client = aws_sdk_dynamodb::Client::new(&config);

let timestamp = event
  .payload
  .time
  .unwrap()
  .format("%+")
  .to_string();

let mut item = HashMap::new();
item.insert(
	"Id".to_string(),
	AttributeValue::S(format!("https://loige.co#{}", timestamp)),
);
item.insert("Timestamp".to_string(), AttributeValue::S(timestamp));

let insert_result = dynamodb_client
  .put_item()
  .table_name(table_name.as_str())
  .set_item(Some(item))
  .send()
  .await?;

tracing::info!("Insert result: {:?}", insert_result);

โœ๏ธ Exercise #4

S3 image resizing

Let's update our previous exercise (process files from S3) and let's resize image files.

You have to:

  • Check if the file is an image (you can match on the extension)
  • Read the content of the file using the S3 SDK
  • Load the image in memory and resize it (imageย crate)
  • Save the resulting image to S3 using the S3 SDK

ย 

Note: remember to give your Lambda permissions to read and write to the S3 bucket!

Note: Try to create the S3 client on the init phase and pass it as configuration!

โœ๏ธ Final project

Website Health-check App

TODO - Add examples for

- How to trigger a lambda on a schedule

- How to make an HTTP request using reqwest (+ Lambda config)

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!

BONUS: SAM + Cargo Lambda
a complete example

BONUS 2:ย another complete example

Benchmark vs ๐Ÿ

๐Ÿš€ 16x faster cold starts

โšก๏ธ 3.5x less memory

๐Ÿค‘ 3x cheaper

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

THANKS!

Grab these slides!

Optimized Lambda functions with Rust (Workshop)

By Luciano Mammino

Optimized Lambda functions with Rust (Workshop)

Abstract You're already familiar with AWS Lambda and its potential for scalability and cost savings. Now, imagine harnessing the performance and efficiency of the Rust programming language to take your serverless functions to the next level. This workshop is designed for developers who are comfortable with AWS and Lambda, but want to explore the benefits of using Rust as their serverless runtime. With a focus on practical implementation, we'll cover everything you need to know to write efficient, scalable, and maintainable AWS Lambda functions in Rust. What You'll Learn Why the combination of Rust and AWS Lambda is a perfect match for optimized performance The tooling ecosystem, including the Rust runtime for Lambda and cargo lambda CLI utility How to bootstrap, test locally, compile, deploy, and remotely test your first Lambda function in Rust Best practices for handling different event types (HTTP, SQS, EventBridge, custom) and responses Integration with Infrastructure as Code tools like AWS SAM Dependency management, code organization, and testing strategies for Rust serverless applications Takeaways By the end of this workshop, you'll have a solid understanding of how to leverage Rust's performance benefits in your AWS Lambda functions. Whether you're looking to reduce costs, improve response times, or simplify development workflows, this session will provide you with the

  • 77