Rust, Serverless & AWS
Luciano Mammino (@loige)
Rust Dublin Meetup 2023-08-22
Writing Lambda functions in Rust!
👋 I'm Luciano (🇮🇹🍕🍝🤌)
👨💻 Senior Architect @ fourTheorem
📔 Co-Author of Node.js Design Patterns 👉
Let's connect!
Grab the slides
$ ~ 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
We host a weekly podcast about AWS
⚡️Serverless
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
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!
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
Some use cases 🛠️
- HTTP APIs backend with API Gateway
- Custom logic step in a Step Function
- Process new S3 objects (Create picture thumbnails)
- Scrape/Synchronise data on a schedule (Import data from an FTP)
- Process jobs from a queue (Generate PDF invoices and send them)
- Execute code when a business event happens (Send welcome email)
- Define the logic to rotate secrets
- Analyse logs and react to suspicious activities
- etc ...
(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! 🔫
AWS Lambda Pricing 💸
Cost = Allocated Memory 𝒙 time
AWS Lambda Pricing 💸
Cost = Allocated Memory 𝒙 time
512 MB = $0.0000000083/ms
Executing a lambda for 15 mins...
0.0000000083 * 900000 = 0.007 $
AWS Lambda... what about CPU? 🙄
You don't explicitly configure it:
CPU scales based on memory
AWS Lambda... what about CPU? 🙄
You don't explicitly configure it:
CPU scales based on memory
Memory | vCPUs |
---|---|
128 - 3008 MB | 2 |
3009 - 5307 MB | 3 |
5308 - 7076 MB | 4 |
7077 - 8845 MB | 5 |
8846+ MB | 6 |
🏃♂️ 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
🏃♂️ Lambda execution model
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
Poll for events
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
Poll for events
event (JSON)
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
in detail
🏃♂️ Lambda execution model
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
response (JSON)
or error
in detail
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 🐞
Our first Lambda
in Node.js (sorry 😜)
export const handler = async(event, context) => {
// ... get data from event
// ... run business logic
// ... return a result
}
export const handler = async(event, context) => {
// ... get data from event
const { url } = event
// ... run business logic
const res = await fetch(url)
console.info(`Status of ${url} : ${res.status}`)
// ... return a result
return res.status
}
Supported Lambda runtimes
-
Node.js
-
Python
-
Java
-
.NET
-
Go
-
Ruby
-
Custom
Supported Lambda runtimes
-
Node.js
-
Python
-
Java
-
.NET
-
Go
-
Ruby
-
Custom
RUST?!
Rust Runtime for Lambda
OK, Where do we start?
install cargo-lambda
Cargo Lambda
- A third-party command for Cargo that makes it easier to author, test and deploy Lambdas in Rust
- Mostly built by an AWS employee (@calavera)
- It can cross-compile for Linux ARM (on Win/Mac/Linux)
- Integrates well with SAM and CDK for IaC
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_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.without_time()
.init();
run(service_fn(function_handler)).await
}
Event & Context
async fn function_handler(event: LambdaEvent<S3Event>)
-> Result<(), Error> {
// let event = event.payload;
let (event, ctx) = event.into_parts();
println!(
"This execution will expire at {}",
ctx.deadline
);
for record in event.records {
// ...
}
Ok(())
}
Request & Response types
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.10.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.0.183"
serde_json = "1.0.104"
// ...
#[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 }))
}
// ...
HTTP-based lambdas
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)
}
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.
Building & Deploying
cargo lambda build --release && cargo lambda deploy
NO TRIGGER CONFIGURED! 🙄
SAM
Serverless Application Model
IaC with...
AWS SAM
- YAML-based Infrastructure as code (IaC) tool focused on serverless apps
- Great when you have to go beyond just one lambda
- ... or when you need more advanced integrations
(e.g. API Gateway) - It supports everything that is natively supported with CloudFormation, but with a slightly simpler syntax
- Deploys through CloudFormation!
# template.yaml
AWSTemplateFormatVersion : '2010-09-09'
Transform:
- AWS::Serverless-2016-10-31
Description: |
A sample Serverless project triggered from S3 CreateObject events
Resources:
ExampleFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs18.x
Handler: index.handler
Events:
S3CreateObject:
Type: S3
Properties:
Bucket: !Ref MyPhotoBucket
Events: s3:ObjectCreated:*
MyPhotoBucket:
Type: AWS::S3::Bucket
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.al2
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
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!
BONUS: SAM + Cargo Lambda
a complete example
Cover Photo by Francesco Ungaro on Unsplash
Thanks to @gbinside, @conzy_m, @eoins, and @micktwomey for kindly reviewing this talk!
THANKS!
Grab these slides!