Unleashing Serverless Performance

Writing Optimized AWS Lambda Functions with Rust

Luciano Mammino (@loige)

2024-10-23

๐Ÿ‘‹ 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

50% 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

๐•ย loige

๐•ย loige

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

  • We pay (only) for what we use

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

๐•ย loige

Serverless... with benefits ๐ŸŽ

  • More focus on the business logic (generally)

  • Increased team agility (mostly)

  • Automatic scalability (sorta)

  • Not a universal solution, but it can work well in many situations!

๐•ย loige

AWS Lambda

Serverless FaaS offering in AWS

Can be triggered by different kinds of events

  • HTTP Requests
  • New files in S3
  • Jobs in a Queue
  • Orchestrated by Step Functions
  • On a schedule
  • Manually invoked

๐•ย loige

(some) Limitations ๐Ÿ˜–

  • Maximum execution time is 15 minutes...
  • Payload size (request/response) is limited
  • Doesn't have a GPU option (yet)

... so again, it's not a silver bullet for all your compute problems! ๐Ÿ”ซ

๐•ย loige

AWS Lambda Pricing ๐Ÿ’ธ

Cost = Allocated Memory ๐’™ time

๐•ย loige

ย  ย  ย  ย  ย ๐Ÿ’ฐย  ย  ย  ย  ย  ย  ย  ย  ย  ย  ๐Ÿ‹๏ธโ€โ™‚๏ธย  ย  ย  ย  ย  ย  ย  ย  ย  ย โฑ๏ธ

AWS Lambda Pricing ๐Ÿ’ธ

Cost = Allocated Memory ๐’™ time

๐•ย 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

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

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

๐•ย loige

๐•ย loige

Load the handler code

Infinite loop

๐•ย 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

๐•ย loige

# Docker
docker version
# (...)

# Rust
cargo --version
# -> cargo 1.82.0 (8f40fc59f 2024-08-21)

# Zig (for cross-compiling lambda binaries)
zig version
# -> 0.13.0

# AWS
aws --version
# -> aws-cli/2.17.0 Python/3.11.8 Darwin/23.6.0 exe/x86_64

# AWS login
# you might need to run extra commands to get temporary credentials if you use AWS organizations
# details on how to configure your CLI here: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html
aws sts get-caller-identity
# ->
# {
#     "UserId": "AROATBJTMBXWT2ZAVHYOW:luciano",
#     "Account": "208950529517",
#     "Arn": "arn:aws:sts::208950529517:assumed-role/AWSReservedSSO_AdministratorAccess_d0f4d19d5ba1f39f/luciano"
# }

# Cargo Lambda
cargo lambda --version
# -> cargo-lambda 1.4.0 (e3bd536 2024-09-07Z)

# SAM
sam --version
# -> SAM CLI, version 1.126.0

๐•ย loige

cargo lambda new itsalive

๐•ย loige

๐•ย loige

๐•ย loige

// src/main.rs

use aws_lambda_events::event::eventbridge::EventBridgeEvent;
use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent};

async fn function_handler(event: LambdaEvent<EventBridgeEvent>) 
  -> Result<(), Error> {
    dbg!(event);
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing::init_default_subscriber();

    run(service_fn(function_handler)).await
}

๐•ย loige

{
  "version": "0",
  "id": "53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2015-10-08T16:53:06Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule"
  ],
  "detail": {}
}

Sample event

๐•ย loige

cargo lambda watch
cargo lambda invoke --data-file "events/eventbridge.json"

Or use one of the official example events:

cargo lambda invoke --data-example eventbridge-schedule

๐•ย loige

๐•ย loige

cargo lambda build --arm64 --release
cargo lambda release

๐•ย loige

Problems with this approach ๐Ÿ˜ญ

  • It doesn't use IaC (Infrastructure as code)
  • You cannot customise the create resources
  • You cannot create additional resources as part of your application stack
    (e.g. DynamoDB tables, S3 buckets, etc)
  • Hard to do incremental changes consistently
  • If you need to delete the entire project, you have to delete every single resource manually!
    (Lambda, log group, IAM role)

๐•ย loige

Let's use SAM
Serverless Application Model

๐•ย loige

# template.yml

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

Resources:
  
  HealthCheckLambda:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: rust-cargolambda
    Properties:
      CodeUri: .
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - arm64
      MemorySize: 256
      Timeout: 70
      Events:
        ScheduledExecution:
          Type: Schedule
          Properties:
            Schedule: rate(30 minutes)

๐•ย loige

๐•ย loige

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

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

cargo add reqwest \
  --no-default-features \
  --features "rustls-tls,http2"

๐•ย loige

// src/main.rs

async fn function_handler(_event: LambdaEvent<EventBridgeEvent<Value>>) -> Result<(), Error> {
    let start = Instant::now();
    let resp = reqwest::get("https://loige.co").await;
    let duration = start.elapsed();
    match resp {
        Ok(resp) => {
            let status = resp.status().as_u16();
            let success = resp.status().is_success();
            dbg!(status);
            dbg!(success);
            dbg!(duration);
        }
        Err(e) => {
            eprintln!("The request failed: {}", e);
        }
    }

    Ok(())
}

๐•ย loige

# template.yml

Resources:
# ...
  HealthChecksTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: "Id"
          KeyType: "HASH"
        - AttributeName: "Timestamp"
          KeyType: "RANGE"
      AttributeDefinitions:
        - AttributeName: "Id"
          AttributeType: "S"
        - AttributeName: "Timestamp"
          AttributeType: "S"

๐•ย loige

# template.yml

Resources:
# ...
  HealthCheckLambda:
    Type: AWS::Serverless::Function
    # ...
    Properties:
      # ...
      Environment:
        Variables:
          TABLE_NAME: !Ref HealthChecksTable
      Policies:
        - DynamoDBWritePolicy:
            TableName: !Ref HealthChecksTable

๐•ย loige

cargo add aws-config aws-sdk-dynamodb

๐•ย loige

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

๐•ย loige

let success = match resp {
  Ok(resp) => {
    let status = resp.status().as_u16();
    item.insert("Status".to_string(), AttributeValue::N(status.to_string()));
    item.insert(
    	"Duration".to_string(),
    	AttributeValue::N(duration.as_millis().to_string()),
  	);
  	resp.status().is_success()
  }
  Err(e) => {
  	item.insert("Error".to_string(), AttributeValue::S(e.to_string()));
  	false
  }
};

item.insert("Success".to_string(), AttributeValue::Bool(success));

๐•ย loige

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

๐•ย loige

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

๐•ย loige

๐•ย loige

๐•ย loige

๐•ย loige

sam delete

Clean up! ๐Ÿงน

๐•ย loige

We took some shortcuts... ๐Ÿ˜…

ย 

Check out the repo for a better and more complete implementation!

ย 

github.com/lmammino/rust-lambda-workshop

Closing notes

  • Lambda is great (most of the time)
  • 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)
  • Go, have fun, share your learnings!

๐•ย loige

BONUS: SAM + Cargo Lambda
a complete example

๐•ย loige

BONUS 2:ย another complete example

๐•ย loige

BONUS 3:ย MOAR examples! ๐Ÿค—

๐•ย loige

BONUS 4:ย MOAR examples! ๐Ÿค—

๐•ย loige

Early-access available at

50% discount! ๐Ÿค‘

THANKS!

Grab these slides!

๐•ย loige

Early-access available at

50% discount! ๐Ÿค‘

Unleashing Serverless Performance: Writing Optimized AWS Lambda Functions with Rust

By Luciano Mammino

Unleashing Serverless Performance: Writing Optimized AWS Lambda Functions with Rust

Take your serverless functions to new heights by pairing AWS Lambda with the high-performance powers of Rust. In this hands-on workshop, you'll discover how to write optimized, scalable, and maintainable serverless code using Rust as your runtime. Learn: Why Rust + Lambda is a match made in heaven The tooling ecosystem for seamless development Best practices for handling events and responses Integration with Infrastructure as Code tools By the end of this session, you'll be equipped to harness Rust's performance benefits and revolutionize your serverless workflows. Prerequisites: basic AWS & Lambda knowledge, some Rust experience (or a willingness to learn).

  • 339