N
Neon11mo ago
stormy-gold

Rust crate or example for AWS lambda or Netlify Functions

I'm using Netlify Functions (AWS lambda with Rust) to connect to Neon (postgres) and have it working fine. The only issue I had was to have to config tokio-postgres to use IPv4 due to an error ("Address family not supported by protocol"), but want to use the host instead. I'm sure the IP address could change at any time and will break eventually. (Fixed: Not sure if it was due to moving my lambda region to match the region I'm using for Neon or updating the node version of the lambda build). Questions: - Is there an official rust serverless crate for AWS lambda provided by Neon I should use? - If there isn't a crate, do you have an example of an AWS lambda endpoint using Rust? Note: I plan to post an example once I get pooling working. See Pooled Gist below (recommended)
6 Replies
stormy-gold
stormy-goldOP11mo ago
AWS Lambda example without connection pooler. TODO: User Verification
use std::{env, str::FromStr};

use aws_lambda_events::encodings::Body;
use aws_lambda_events::event::apigw::ApiGatewayProxyResponse;
use http::header::HeaderMap;
use lambda_runtime::{
service_fn,
tracing::{self},
Error, LambdaEvent,
};
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
use serde_json::Value;
use tokio_postgres::Config;

#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();

let handler = service_fn(handler);
lambda_runtime::run(handler).await?;
Ok(())
}
use std::{env, str::FromStr};

use aws_lambda_events::encodings::Body;
use aws_lambda_events::event::apigw::ApiGatewayProxyResponse;
use http::header::HeaderMap;
use lambda_runtime::{
service_fn,
tracing::{self},
Error, LambdaEvent,
};
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
use serde_json::Value;
use tokio_postgres::Config;

#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();

let handler = service_fn(handler);
lambda_runtime::run(handler).await?;
Ok(())
}
async fn handler(_event: LambdaEvent<Value>) -> Result<ApiGatewayProxyResponse, Error> {
// Pull the db url from the environment
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

// Used to store the results of the query
let mut result_json = "".to_string();

// Create a new connection to the database
// Setup the TLS configuration for the connection using native certs
// Using https://crates.io/crates/rustls-platform-verifier
// replaces using rustls-native-certs on its own (recommended)
let config = ClientConfig::with_platform_verifier();
let tls = tokio_postgres_rustls::MakeRustlsConnect::new(config);

// Setup the Config struct using the connection string
let config = Config::from_str(db_url.as_str()).unwrap();
// get the client and connection future
let (client, conn) = config
.connect(tls)
.await
.map_err(|err| format!("Error connecting to PostgreSQL: {}", err))?;

// The connection future performs the actual communication with the database,
// so spawn it off to run on its own.
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("connection error: {}", e);
}
});
async fn handler(_event: LambdaEvent<Value>) -> Result<ApiGatewayProxyResponse, Error> {
// Pull the db url from the environment
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

// Used to store the results of the query
let mut result_json = "".to_string();

// Create a new connection to the database
// Setup the TLS configuration for the connection using native certs
// Using https://crates.io/crates/rustls-platform-verifier
// replaces using rustls-native-certs on its own (recommended)
let config = ClientConfig::with_platform_verifier();
let tls = tokio_postgres_rustls::MakeRustlsConnect::new(config);

// Setup the Config struct using the connection string
let config = Config::from_str(db_url.as_str()).unwrap();
// get the client and connection future
let (client, conn) = config
.connect(tls)
.await
.map_err(|err| format!("Error connecting to PostgreSQL: {}", err))?;

// The connection future performs the actual communication with the database,
// so spawn it off to run on its own.
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("connection error: {}", e);
}
});
// Perform a query to get the posts (Vec<Row>) from the database
for row in client.query("select * from posts", &[]).await? {
let ret: i32 = row.get(0);
let title: &str = row.get(1);
let contents: &str = row.get(2);
let published: bool = row.get(3);

if result_json.len() > 0 {
result_json.push_str(",");
}
result_json.push_str(&format!(
"{{\"id\": {}, \"title\": \"{}\", \"contents\": \"{}\", \"published\": {}}}",
ret, title, contents, published
));
}
let result_json = format!("[{}]", result_json);

// Headers for the lambda response
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json".parse().unwrap());

let resp = ApiGatewayProxyResponse {
status_code: 200,
headers: headers.clone(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text(result_json.into())),
is_base64_encoded: false,
};

Ok(resp)
}
// Perform a query to get the posts (Vec<Row>) from the database
for row in client.query("select * from posts", &[]).await? {
let ret: i32 = row.get(0);
let title: &str = row.get(1);
let contents: &str = row.get(2);
let published: bool = row.get(3);

if result_json.len() > 0 {
result_json.push_str(",");
}
result_json.push_str(&format!(
"{{\"id\": {}, \"title\": \"{}\", \"contents\": \"{}\", \"published\": {}}}",
ret, title, contents, published
));
}
let result_json = format!("[{}]", result_json);

// Headers for the lambda response
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json".parse().unwrap());

let resp = ApiGatewayProxyResponse {
status_code: 200,
headers: headers.clone(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text(result_json.into())),
is_base64_encoded: false,
};

Ok(resp)
}
stormy-gold
stormy-goldOP11mo ago
Here's a Gist because I had to break up code here: https://gist.github.com/talves/92070c8320f7c5c35c3bc640698e9aec
Gist
AWS Lambda in Rust example for Postgres on Neon (any really)
AWS Lambda in Rust example for Postgres on Neon (any really) - Cargo.toml
fair-rose
fair-rose11mo ago
Thank you for sharing the example!
stormy-gold
stormy-goldOP11mo ago
@Mahmoud No problem. I have some more to share, got a pooled connection working also (recommended).
stormy-gold
stormy-goldOP11mo ago
Here is the Gist for the pooled connection: https://gist.github.com/talves/cacd40fe705e4872a6ec0bbeaf3767c3 I used the diesel, diesel-async crates with bb8 pool manager. This lambda function Works with Netlify functions and should work as an AWS lambda function also (not tested yet). Happy New Year!
Gist
AWS Lambda in Rust example for Postgres (any) on Neon using Pooled ...
AWS Lambda in Rust example for Postgres (any) on Neon using Pooled connections. Works on Netlify functions. - Cargo.toml
stormy-gold
stormy-goldOP11mo ago
I'm getting some really fast lambda calls to Neon (about 50-100ms) which was very nice to know. These will slow with data and session verification, but was a really good start for my proof of concept. There are improvements that can be made on using the lambda_runtime for rust, but this is a great start for anyone who wants to use it. Couldn't get axum routing to work for Netlify functions, but should work on AWS lambda if you use lambda_http instead. If using AWS lambda, there are some good examples that helped me get started with these in the aws-lambda-rust-runtime repo. The examples in the repo are a little outdated, because crates in this area are moving fast, but as of this post, my connections using rustls, tokio_postgress and tokio_postgres_rustls is the most current.

Did you find this page helpful?