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

Rust, Serverless, and AWS Writing Lambdas in Rust - Rust Nation UK 2024

By Luciano Mammino

Rust, Serverless, and AWS Writing Lambdas in Rust - Rust Nation UK 2024

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! In this talk we will cover: What is Serverless and why it's cool, What is AWS Lambda, use cases and limitations, Lambda pricing model, Lambda and CPU allocation, Lambda execution model, Why using Rust with Lambda is a good idea, Cargo-Lambda and how to integrate it with SAM for Infrastructure as Code, Writing our first lambda, testing it locally and deploying it. In addition to all of this, we will deep dive on the Lambda interface and how we can fine tune the Lambda request and response types to our needs. By the end of this talk, you should be ready to write and deploy your first Lambda written in Rust!

  • 1,283