Luciano Mammino PRO
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com
Luciano Mammino (@loige)
2024-12-12
๐ I'm Lucianoย (๐ฎ๐น๐๐๐ค)
๐จโ๐ป Senior Architect @ fourTheorem
๐ Co-Author of Node.js Design Patterns ย ๐
Let's connect!
๐ I'm Lucianoย (๐ฎ๐น๐๐๐ค)
๐จโ๐ป Senior Architect @ fourTheorem
๐ Co-Author of Crafting Lambda Functions in Rust ย ๐
Let's connect!
Early-access available at
40% discount! ๐ค
โ๏ธ 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.co
loige.co
loige.co
loige.co
loige.co
loige.co
Zero-cost abstractions
Strongly typed with a really good type-system
Takes inspiration from Haskell, C++, OCaml, JavaScript, Ruby
Great (built-in) package manager (Cargo)
Great ecosystem of libraries
Pattern matching, no null, Option & Result types
loige.co
loige.co
loige.co
loige.co
loige.co
cargo new hello-rust
loige.co
cargo new hello-rust
cd hello-rust
loige.co
cargo new hello-rust
cd hello-rust
cargo run
Hello, World!
loige.co
cargo new hello-rust
cd hello-rust
cargo run
Hello, World!
loige.co
Handling missing data & errors
loige.co
null... does not exist in Rust! ๐ต
loige.co
you can use ()ย to represent no data
Unit type (empty tuple)
loige.co
fn some_function() {
// ... do something
}
loige.co
fn some_function() -> () {
// ... do something
}
Implicit
loige.co
But if you want to represent data that might be set or not, you should use Option
loige.co
fn main() {
let items: Vec<u32> = vec![];
let first_item = items.first();
}
Option<u32>
Some(u32)
Presence of value
None
๐ฅบ Absence of value
loige.co
fn main() {
let items: Vec<u32> = vec![];
let first_item = items.first(); // None
}
loige.co
fn main() {
let items: Vec<u32> = vec![1,2,3];
let first_item = items.first(); // Some(1)
}
loige.co
fn main() {
let items: Vec<u32> = vec![1, 2, 3];
let first_item = items.first();
match first_item {
Some(value) => println!("{}", value),
None => println!("No items"),
}
}
๐ Happy path
๐ฅบ Sad path
loige.co
fn main() {
let items: Vec<u32> = vec![1, 2, 3];
let first_item = items.first();
println!("{:?}", first_item.unwrap()); // 1
}
If you cannot get the value, panic!
loige.co
fn main() {
let items: Vec<u32> = vec![];
let first_item = items.first();
println!("{:?}", first_item.unwrap());
}
If you cannot get the value, panic!
thread 'main' panicked at examples/option-unwrap.rs:4:33: called `Option::unwrap()` on a `None` value note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
loige.co
fn main() {
let items: Vec<u32> = vec![];
let first_item = items.first();
println!("{:?}", first_item.expect("No first value"));
}
If you cannot get the value, panic!
thread 'main' panicked at examples/option-expect.rs:4:33: No first value note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
loige.co
fn main() {
let items: Vec<u32> = vec![];
let first_item = items.first();
println!("{:?}", first_item.unwrap_or(&1)); // 1
}
if you cannot get the value, use a default value!
loige.co
use std::env;
fn main() {
let region = env::var("AWS_REGION");
}
Result<String, VarError>
๐ Happy path
๐ฅบ Sad path
loige.co
use std::env;
fn main() {
let region = env::var("AWS_REGION");
match region {
Ok(region) => println!("Selected region: {}", region),
Err(_) => println!("Error: AWS_REGION not set"),
}
}
๐ Happy path
๐ฅบ Sad path
loige.co
use std::env;
fn main() {
let region = env::var("AWS_REGION")
.expect("AWS_REGION environment variable not set");
}
String
If you cannot get the value, panic!
loige.co
use std::env;
fn main() {
let region = env::var("AWS_REGION")
.unwrap_or_else(|_| "eu-west-1".to_string());
}
Rust makes it very hard for you to ignore possible errors or the absence of values.
if you cannot get the value, use a default value!
loige.co
loige.co
my favourite ones!
loige.co
loige.co
loige.co
loige.co
loige.co
RUST?!
loige.co
RUST?!
loige.co
EASY PEASY... we justย need a custom runtime! ๐ค
loige.co
loige.co
loige.co
Runtime
Handler (logic)
loige.co
Runtime
Handler (logic)
Poll for events
loige.co
Runtime
Handler (logic)
Poll for events
event (JSON)
loige.co
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
loige.co
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
loige.co
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
response (JSON)
or error
loige.co
loige.co
loige.co
Load the handler code
Infinite loop
loige.co
loige.co
loige.co
loige.co
loige.co
loige.co
cargo lambda new hello-lambda
loige.co
cargo lambda new hello-lambda
# Choose HTTP integration!
loige.co
loige.co
use lambda_http::{run, service_fn, tracing, Body, Error, Request, RequestExt, Response};
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
run(service_fn(function_handler)).await
}
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
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");
let resp = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(message.into())
.map_err(Box::new)?;
Ok(resp)
}
loige.co
These are just abstractions! ๐ง
Lambda is still using JSON behind the scenes.
For HTTP you generally use theย Lambda-Proxy integration.
use lambda_http::{run, service_fn, tracing, Body, Error, Request, RequestExt, Response};
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
run(service_fn(function_handler)).await
}
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
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");
let resp = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(message.into())
.map_err(Box::new)?;
Ok(resp)
}
loige.co
Lambda-Proxy integration
Example Request (HTTP Event) ๐ง
{
"resource": "/my/path",
"path": "/my/path",
"httpMethod": "GET",
"headers": {
"header1": "value1",
"header2": "value1,value2"
},
"multiValueHeaders": {
"header1": [
"value1"
],
"header2": [
"value1",
"value2"
]
},
"queryStringParameters": {
"parameter1": "value1,value2",
"parameter2": "value"
},
"multiValueQueryStringParameters": {
"parameter1": [
"value1",
"value2"
],
"parameter2": [
"value"
]
},
"requestContext": {
"accountId": "123456789012",
"apiId": "id",
"authorizer": {
"claims": null,
"scopes": null
},
"domainName": "id.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "id",
"extendedRequestId": "request-id",
"httpMethod": "GET",
"identity": {
"accessKey": null,
"accountId": null,
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"principalOrgId": null,
"sourceIp": "IP",
"user": null,
"userAgent": "user-agent",
"userArn": null,
"clientCert": {
"clientCertPem": "CERT_CONTENT",
"subjectDN": "www.example.com",
"issuerDN": "Example issuer",
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
"validity": {
"notBefore": "May 28 12:30:02 2019 GMT",
"notAfter": "Aug 5 09:36:04 2021 GMT"
}
}
},
"path": "/my/path",
"protocol": "HTTP/1.1",
"requestId": "id=",
"requestTime": "04/Mar/2020:19:15:17 +0000",
"requestTimeEpoch": 1583349317135,
"resourceId": null,
"resourcePath": "/my/path",
"stage": "$default"
},
"pathParameters": null,
"stageVariables": null,
"body": "Hello from Lambda!",
"isBase64Encoded": false
}
Lambda-Proxy integration
Example Response ๐ง
{
"isBase64Encoded": false,
"statusCode": 200,
"headers": {
"Content-Type": "text/plain"
},
"body": "Hello, World"
}
loige.co
Local Testing
loige.co
cargo lambda watch
loige.co
cargo lambda invoke
# 1. example event
# list of supported example events
# https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-events/src/fixtures
cargo lambda invoke --data-example apigw-request
# 2. custom JSON event file
cargo lambda invoke --data-file event.json
# 3. inline
cargo lambda invoke --data-ascii '{"name": "Loige"}'
loige.co
cargo lambda build --release --arm64 && cargo lambda deploy
loige.co
cargo lambda deploy --enable-function-url
loige.co
loige.co
Write a Lambda that implements an HTTP API that receives a message via query string parameters and responds with that messages capitalized (in the response body).
ย
Bonus: if no message is provided in the request, return a help message in the response body.
GET https://...?message=Hello
HTTP/1.1 Content-Type: text/plain Content-Length: 5 HELLO
loige.co
async fn function_handler(event: LambdaEvent<EventType>)
-> Result<ResponseType, Error> {
let (event, context) = event.into_parts();
// ...
}
Request
Response
loige.co
async fn function_handler(event: LambdaEvent<EventType>)
-> Result<ResponseType, Error> {
let (event, context) = event.into_parts();
// ...
}
Generic Event Wrapper
Specific Response Type (Success)
loige.co
Specific Event Type
async fn function_handler(event: LambdaEvent<EventType>)
-> Result<ResponseType, Error> {
let (event, context) = event.into_parts();
// ...
}
Execution context
loige.co
Specific Event
(extracted)
use aws_lambda_events::event::s3::S3Event;
async fn function_handler(event: LambdaEvent<S3Event>)
-> Result<(), Error> {
let (event, context) = event.into_parts();
println!("{}", context.request_id);
for record in event.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(())
}
loige.co
async fn function_handler(event: LambdaEvent<S3Event>)
-> Result<(), Error> {
// ...
Ok(())
}
Request
Response
What if we want to use different types? ๐คจ
loige.co
use type definitions in the aws_lambda_events crate
loige.co
loige.co
Jobs
loige.co
# Cargo.toml
[dependencies]
aws_lambda_events = {
version = "0.15.0",
default-features = false,
features = [
"sqs",
]
}
loige.co
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,
})
}
// ...
loige.co
Create custom request and response types
loige.co
loige.co
# Cargo.toml
[dependencies]
serde = "1"
serde_json = "1"
loige.co
// ...
#[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 })
}
// ...
loige.co
Use arbitrary JSON values!
loige.co
// ...
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 }))
}
// ...
loige.co
loige.co
Write a Lambda that processes S3 events. For every object in the event, print the bucket name, the type of operation, and the object name.
S3 Event
MyBucket - ObjectCreated:Put "Happy Face.jpg"
LOGS
MyBucket - ObjectCreated:Put "lolz.gif"
MyBucket - ObjectCreated:Put "secrets/passwords.txt"
loige.co
NO TRIGGER CONFIGURED! ๐
WUT!? ๐ฑ
loige.co
loige.co
loige.co
loige.co
loige.co
# template.yaml
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs22.x
Timeout: 180
Description: "This is a cool serverless project"
Parameters:
ParameterLogicalID:
Description: Information about the parameter
Type: string
Default: somevalue
AllowedValues:
- value1
- value2
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
MESSAGE: "Hello From SAM"
Outputs:
OutputLogicalID:
Description: Information about the value
Value: Value to return
Export:
Name: Name of resource to export
loige.co
SAM Works with Cargo Lambda (beta feature):
Note: Cargo Lambda also works with CDK
(github.com/cargo-lambda/cargo-lambda-cdk)
loige.co
# 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.al2023
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
loige.co
ย
sam build sam local start-api sam deploy
loige.co
sam validate --lint && sam build --beta-features && sam deploy
loige.co
loige.co
loige.co
You should load the configuration and initialize clients in the init phase
loige.co
struct HandlerConfig {
table_arn: String
}
async fn function_handler(
config: &HandlerConfig, // <- now we can receive a reference to the config
_event: LambdaEvent<EventBridgeEvent<Value>>,
) -> Result<(), Error> {
// ...
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let table_arn = env::var("TABLE_ARN")
.expect("TABLE_ARN environment variable is not set");
let config = &HandlerConfig {
table_arn,
};
run(service_fn(move |event| async move {
function_handler(config, event).await
}))
.await
}
loige.co
loige.co
Let's update our previous exercise (process files from S3) and let's use SAM.
Use a SAM template to define the S3 bucket and the Lambda function. Pass the bucket name to the lambda function using an environment variable.
loige.co
loige.co
cargo add aws-config aws-sdk-dynamodb
List of available libraries
You will always need the aws-configย crate (credentials and config management)
You will need to install the specific client library for the services you want to us
loige.co
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));
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.co
Let's update our previous exercise (process files from S3) and let's resize image files.
You have to:
ย
Note: remember to give your Lambda permissions to read and write to the S3 bucket!
Note: Try to create the S3 client on the init phase and pass it as configuration!
loige.co
- How to trigger a lambda on a schedule
- How to make an HTTP request using reqwest (+ Lambda config)
loige.co
loige.co
loige.co
loige.co
loige.co
Thanks to @gbinside, @conzy_m, @eoins, and @micktwomeyย for kindly reviewing this talk!
THANKS!
Grab these slides!
loige.co
By Luciano Mammino
Abstract You're already familiar with AWS Lambda and its potential for scalability and cost savings. Now, imagine harnessing the performance and efficiency of the Rust programming language to take your serverless functions to the next level. This workshop is designed for developers who are comfortable with AWS and Lambda, but want to explore the benefits of using Rust as their serverless runtime. With a focus on practical implementation, we'll cover everything you need to know to write efficient, scalable, and maintainable AWS Lambda functions in Rust. What You'll Learn Why the combination of Rust and AWS Lambda is a perfect match for optimized performance The tooling ecosystem, including the Rust runtime for Lambda and cargo lambda CLI utility How to bootstrap, test locally, compile, deploy, and remotely test your first Lambda function in Rust Best practices for handling different event types (HTTP, SQS, EventBridge, custom) and responses Integration with Infrastructure as Code tools like AWS SAM Dependency management, code organization, and testing strategies for Rust serverless applications Takeaways By the end of this workshop, you'll have a solid understanding of how to leverage Rust's performance benefits in your AWS Lambda functions. Whether you're looking to reduce costs, improve response times, or simplify development workflows, this session will provide you with the
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com