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)
2023-11-22
👋 I'm Luciano (🇮🇹🍕🍝🤌)
👨💻 Senior Architect @ fourTheorem
📔 Co-Author of Node.js Design Patterns 👉
Let's connect!
✉️ 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
A (relatively) new programming language
Most loved... for 7 years in a row!
Low-level, yet general-purpose
Performant & memory-safe
𝕏 loige
A lovely mascot
Strongly typed with a really good type-system
Takes inspiration from Haskell, C++, OCaml, JavaScript, Ruby
Great (built-in) package manager (Cargo)
Good ecosystem of libraries
Pattern matching
No null, Option & Result types
𝕏 loige
use std::env;
fn main() {
let region = env::var("AWS_REGION");
}
Result<String, VarError>
𝕏 loige
😀 Happy path
🥺 Sad path
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"),
}
}
𝕏 loige
😀 Happy path
🥺 Sad path
use std::env;
fn main() {
let region = env::var("AWS_REGION")
.expect("AWS_REGION environment variable not set");
}
String
𝕏 loige
If you cannot get the value, panic!
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.
𝕏 loige
if you cannot get the value, use a default value!
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
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
Serverless FaaS offering in AWS
Can be triggered by different kinds of events
𝕏 loige
... so again, it's not a silver bullet for all your compute problems! 🔫
𝕏 loige
Cost = Allocated Memory 𝒙 time
𝕏 loige
💰 🏋️♂️ ⏱️
Cost = Allocated Memory 𝒙 time
𝕏 loige
💰 🏋️♂️ ⏱️
You don't explicitly configure it:
CPU scales based on memory
𝕏 loige
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 |
𝕏 loige
𝕏 loige
𝕏 loige
Runtime
Handler (logic)
𝕏 loige
Runtime
Handler (logic)
Poll for events
𝕏 loige
Runtime
Handler (logic)
Poll for events
event (JSON)
𝕏 loige
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
𝕏 loige
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
𝕏 loige
Runtime
Handler (logic)
Poll for events
event (JSON)
execute
response or
error
response (JSON)
or error
𝕏 loige
𝕏 loige
Node.js
Python
Java
.NET
Go
Ruby
Custom
𝕏 loige
Node.js
Python
Java
.NET
Go
Ruby
Custom
RUST?!
𝕏 loige
𝕏 loige
𝕏 loige
𝕏 loige
𝕏 loige
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
}
𝕏 loige
𝕏 loige
𝕏 loige
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(())
}
𝕏 loige
𝕏 loige
async fn function_handler(event: LambdaEvent<S3Event>)
-> Result<(), Error> {
// ...
Ok(())
}
Request
Response
What if we want to use different types? 🤨
𝕏 loige
use type definitions in the aws_lambda_events crate
𝕏 loige
𝕏 loige
Jobs
𝕏 loige
# Cargo.toml
[dependencies]
aws_lambda_events = {
version = "0.10.0",
default-features = false,
features = [
"sqs",
]
}
𝕏 loige
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
Create custom request and response types
𝕏 loige
𝕏 loige
# Cargo.toml
[dependencies]
serde = "1.0.183"
serde_json = "1.0.104"
𝕏 loige
// ...
#[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
𝕏 loige
Use arbitrary JSON values!
𝕏 loige
// ...
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
𝕏 loige
𝕏 loige
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)
}
𝕏 loige
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.
𝕏 loige
cargo lambda build --release && cargo lambda deploy
𝕏 loige
𝕏 loige
NO TRIGGER CONFIGURED! 🙄
WUT!? 😱
𝕏 loige
𝕏 loige
# 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
𝕏 loige
SAM Works with Cargo Lambda (beta feature):
Note: Cargo Lambda also works with CDK
(github.com/cargo-lambda/cargo-lambda-cdk)
𝕏 loige
# 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 # 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
sam build sam local start-api sam deploy
𝕏 loige
𝕏 loige
𝕏 loige
𝕏 loige
𝕏 loige
𝕏 loige
Cover photo by Felipe Portella on Unsplash
Thanks to @gbinside, @conzy_m, @eoins, and @micktwomey for kindly reviewing this talk!
THANKS!
Grab these slides!
𝕏 loige
By Luciano Mammino
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!
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com