Rust, Serverless, and AWS
Writing Lambdas in Rust

Luciano Mammino (@loige)

2024-03-28

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

Learning Rust - Live! ๐Ÿฅต

youtube.com/@loige

twitch.tv/loige

๐•ย loige

๐•ย loige

<self-promotion/>

sorry... ๐Ÿ˜‡

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

๐Ÿƒโ€โ™‚๏ธ 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

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

Rust Runtime for Lambda

๐•ย loige

OK, Let's do this!

๐•ย loige

๐•ย loige

๐•ย loige

# Docker
docker version
# (...)

# Rust
cargo --version
# -> cargo 1.76.0 (c84b36747 2024-01-18)

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

# AWS
aws --version
# -> aws-cli/2.15.28 Python/3.11.8 Darwin/23.3.0 exe/x86_64 prompt/off

# 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.1.0 (e918363 2024-02-19Z)

# SAM
sam --version
# -> SAM CLI, version 1.111.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};
use serde_json::Value;

async fn function_handler(event: LambdaEvent<EventBridgeEvent<Value>>) 
  -> 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"

๐•ย 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 region_provider = RegionProviderChain::default_provider();
let config = aws_config::defaults(BehaviorVersion::latest())
  .region(region_provider)
  .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

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

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

THANKS!

Grab these slides!

๐•ย loige