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
✉️ 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
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
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!
Serverless FaaS offering in AWS
Can be triggered by different kinds of events
... so again, it's not a silver bullet for all your compute problems! 🔫
Cost = Allocated Memory 𝒙 time
Cost = Allocated Memory 𝒙 time
512 MB = $0.0000000083/ms
Executing a lambda for 15 mins...
0.0000000083 * 900000 = 0.007 $
You don't explicitly configure it:
CPU scales based on memory
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 |
Runtime
Handler (logic)
Runtime
Handler (logic)
Poll for events
Runtime
Handler (logic)
Poll for events
event (JSON)
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
response (JSON)
or error
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
}
Node.js
Python
Java
.NET
Go
Ruby
Custom
Node.js
Python
Java
.NET
Go
Ruby
Custom
RUST?!
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
}
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(())
}
async fn function_handler(event: LambdaEvent<S3Event>)
-> Result<(), Error> {
// ...
Ok(())
}
Request
Response
What if we want to use different types? 🤨
use type definitions in the aws_lambda_events crate
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,
})
}
// ...
Create custom request and response types
# 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 })
}
// ...
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 }))
}
// ...
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.
cargo lambda build --release && cargo lambda deploy
NO TRIGGER CONFIGURED! 🙄
# 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
SAM Works with Cargo Lambda (beta feature):
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
sam build sam local start-api sam deploy
Cover Photo by Francesco Ungaro on Unsplash
Thanks to @gbinside, @conzy_m, @eoins, and @micktwomey for kindly reviewing this talk!
THANKS!
Grab these slides!