Definire l'handler della funzione Lambda in Rust - AWS Lambda

Definire l'handler della funzione Lambda in Rust

Nota

Il client di runtime Rust è un pacchetto sperimentale. È soggetto a modifiche ed è destinato esclusivamente a scopi di valutazione.

Il gestore di funzioni Lambda è il metodo nel codice della funzione che elabora gli eventi. Quando viene richiamata la funzione, Lambda esegue il metodo del gestore. La funzione viene eseguita fino a quando il gestore non restituisce una risposta, termina o scade.

Questa pagina descrive come lavorare con i gestori di funzioni Lambda in Go, inclusa la configurazione del progetto, le convenzioni di denominazione e le migliori pratiche. Questa pagina include anche un esempio di funzione Go Lambda che raccoglie informazioni su un ordine, produce una ricevuta in un file di testo e inserisce questo file in un bucket Amazon Simple Storage Service (S3). Per informazioni su come distribuire una funzione dopo averla scritta, consulta o.

Configurazione di Go Handler

Quando si lavora con le funzioni Lambda in Rust, il processo prevede la scrittura del codice, la compilazione e la distribuzione degli artefatti compilati in Lambda. Il modo più semplice per configurare un progetto di gestione Lambda in Rust consiste nell'utilizzare il AWS LambdaRuntime for Rust. Nonostante il nome, AWS Lambda Runtime for Rust non è un runtime gestito nello stesso senso in cui lo è in Lambda for Python, Java o Node.js. Invece, il AWS Lambda Runtime for Rust è un crate (lambda_runtime) che supporta la scrittura di funzioni Lambda in Rust e l'interfacciamento AWS Lambda con l'ambiente di esecuzione.

Utilizzare il seguente comando per eseguire il programma di installazione di AWS Lambda.

cargo install cargo-lambda

Dopo aver eseguito con successo l'installazionecargo-lambda, utilizza il comando seguente per inizializzare un nuovo progetto gestore di funzioni Lambda:

cargo lambda new example-rust

Quando esegui questo comando, l'interfaccia a riga di comando (CLI) ti pone un paio di domande sulla tua funzione Lambda:

  • Funzione HTTP: se intendi richiamare la tua funzione tramite API Gateway o un URL di funzione, rispondi . Altrimenti, rispondi No. Nel codice di esempio in questa pagina, invochiamo la nostra funzione con un evento JSON personalizzato, quindi rispondiamo No.

  • Tipo di evento: se intendi utilizzare una forma di evento predefinita per richiamare la funzione, seleziona il tipo di evento previsto corretto. In caso contrario, lasciare vuoto il campo. Nel codice di esempio in questa pagina, invochiamo la nostra funzione con un evento JSON personalizzato, quindi lasciamo vuota questa opzione.

Dopo che il comando è stato eseguito correttamente, accedi alla directory principale del tuo progetto:

cd example-rust

Questo comando genera un file JAR nella directory . generic_handler.rsPuò essere usato per personalizzare un gestore di eventi generico. Il main.rs file contiene la logica principale dell'applicazione. Il Cargo.toml file contiene metadati sul pacchetto ed elenca le sue dipendenze esterne.

Esempio di codice della funzione Lambda

Il seguente esempio di codice della funzione Go Lambda raccoglie le informazioni su un ordine, produce una ricevuta in un file di testo e inserisce questo file in un bucket Amazon S3.

Esempio main.rsFunzione Lambda
use aws_sdk_s3::{Client, primitives::ByteStream}; use lambda_runtime::{run, service_fn, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::env; #[derive(Deserialize, Serialize)] struct Order { order_id: String, amount: f64, item: String, } async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let payload = event.payload; // Deserialize the incoming event into Order struct let order: Order = serde_json::from_value(payload)?; let bucket_name = env::var("RECEIPT_BUCKET") .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?; let receipt_content = format!( "OrderID: {}\nAmount: ${:.2}\nItem: {}", order.order_id, order.amount, order.item ); let key = format!("receipts/{}.txt", order.order_id); let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let s3_client = Client::new(&config); upload_receipt_to_s3(&s3_client, &bucket_name, &key, &receipt_content).await?; Ok("Success".to_string()) } async fn upload_receipt_to_s3( client: &Client, bucket_name: &str, key: &str, content: &str, ) -> Result<(), Error> { client .put_object() .bucket(bucket_name) .key(key) .body(ByteStream::from(content.as_bytes().to_vec())) // Fixed conversion .content_type("text/plain") .send() .await?; Ok(()) } #[tokio::main] async fn main() -> Result<(), Error> { run(service_fn(function_handler)).await }

Questo file main.rs contiene le sezioni seguenti:

  • useistruzioni: utilizzale per importare le casse e i metodi Rust richiesti dalla funzione Lambda.

  • #[derive(Deserialize, Serialize)]block: definisce la forma dell'evento di input previsto in questa struttura Go.

  • async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>: questo è il metodo dell'handler principale, che contiene la logica principale dell'applicazione.

  • async fn upload_receipt_to_s3 (...): questo è un metodo helper a cui fa riferimento il metodo dell'handler principale function_handler.

  • #[tokio::main]: Questa è una macro che segna il punto di ingresso di un programma Rust. Imposta anche un runtime Tokio, che consente al main() metodo di utilizzareasync/awaited eseguire in modo asincrono.

  • async fn main() -> Result<(), Error>: La main() funzione è il punto di ingresso del codice. Al suo interno, specifichiamo function_handler come metodo di gestione principale.

Il seguente Cargo.toml file accompagna questa funzione.

[package] name = "example-rust" version = "0.1.0" edition = "2024" [dependencies] aws-config = "1.5.18" aws-sdk-s3 = "1.78.0" lambda_runtime = "0.13.0" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] }

Affinché questa funzione funzioni correttamente, il suo ruolo di esecuzione deve consentire l's3:PutObjectazione. Inoltre, assicuratevi di definire la variabile di RECEIPT_BUCKET ambiente. Dopo una chiamata riuscita, il bucket Amazon S3 dovrebbe contenere un file di ricevuta.

Definizioni di classe valide per i gestori Rust

Nella maggior parte dei casi, le firme dei gestori Lambda definite in Rust avranno il seguente formato:

async fn function_handler(event: LambdaEvent<T>) -> Result<U, Error>

Per questo gestore:

  • Il nome di questo gestore è. function_handler

  • L'input singolare al gestore è evento ed è di tipo. LambdaEvent<T>

    • LambdaEventè un wrapper che proviene dalla cassa. lambda_runtime L'utilizzo di questo wrapper consente di accedere all'oggetto di contesto, che include metadati specifici di Lambda come l'ID della richiesta della chiamata.

    • Tè il tipo di evento deserializzato. Ad esempio, questo può essereserde_json::Value, che consente al gestore di accettare qualsiasi input JSON generico. In alternativa, questo può essere un tipo come ApiGatewayProxyRequest se la funzione si aspettasse un tipo di input specifico e predefinito.

  • Il tipo restituito dal gestore è. Result<U, Error>

    • Uè il tipo di output deserializzato. Udeve implementare il serde::Serialize tratto in modo che Lambda possa convertire il valore restituito in JSON. Ad esempio, U può essere un tipo String semplice o una struttura personalizzata purché implementata. serde_json::Value Serialize Quando il codice raggiunge un'istruzione Ok (U), ciò indica che l'esecuzione è riuscita e la funzione restituisce un valore di tipoU.

    • Quando il codice rileva un errore (ad esempioErr(Error)), la funzione registra l'errore in Amazon CloudWatch e restituisce una risposta di errore di tipo. Error

Nel nostro esempio, la firma del gestore è simile alla seguente:

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>

Altre firme valide del gestore possono presentare quanto segue:

  • Omissione del LambdaEvent wrapper: se si ometteLambdaEvent, si perde l'accesso all'oggetto di contesto Lambda all'interno della funzione. Di seguito è illustrato un evento di esempio di questo tipo:

    async fn handler(event: serde_json::Value) -> Result<String, Error>
  • Utilizzo del tipo di unità come input: per Rust, è possibile utilizzare il tipo di unità per rappresentare un input vuoto. Questo è comunemente usato per funzioni con invocazioni periodiche e programmate. Di seguito è illustrato un evento di esempio di questo tipo:

    async fn handler(_: ()) -> Result<Value, Error>

Convenzioni di denominazione dei gestori

I gestori Lambda in Rust non hanno rigide restrizioni di denominazione. Sebbene sia possibile utilizzare qualsiasi nome per il gestore, i nomi delle funzioni in Rust sono generalmente in. snake_case

Per le applicazioni più piccole, come in questo esempio, puoi utilizzare un singolo main.rs file per contenere tutto il codice. Per progetti più grandi, main.rs deve contenere il punto di ingresso alla funzione, ma è possibile disporre di file aggiuntivi per separare il codice in moduli logici. In uno scenario di esempio potrebbero essere presenti i seguenti utenti:

/example-rust │── src/ │ ├── main.rs # Entry point │ ├── handler.rs # Contains main handler │ ├── services.rs # [Optional] Back-end service calls │ ├── models.rs # [Optional] Data models │── Cargo.toml

Definizione e accesso all'oggetto evento di input

JSON è il formato di input più comune e standard per le funzioni Lambda. In questo esempio, la funzione prevede un input simile a quanto segue:

{ "order_id": "12345", "amount": 199.99, "item": "Wireless Headphones" }

In Rust, puoi definire la forma dell'evento di input previsto in una struttura. In questo esempio, definiamo una struttura per rappresentare unOrder:

#[derive(Deserialize, Serialize)] struct Order { order_id: String, amount: f64, item: String, }

Questa struttura corrisponde alla forma di input prevista. In questo esempio, la #[derive(Deserialize, Serialize)] macro genera automaticamente codice per la serializzazione e la deserializzazione. Ciò significa che possiamo deserializzare il tipo JSON di input generico nella nostra struttura utilizzando il metodo. serde_json::from_value() Ciò è illustrato nelle prime righe dell'handler:

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let payload = event.payload; // Deserialize the incoming event into Order struct let order: Order = serde_json::from_value(payload)?; ... }

È quindi possibile accedere ai campi dell'oggetto. Ad esempio, order.order_id recupera il valore di order_id dall'input originale.

Tipi di eventi di input predefiniti

Nella cassa sono disponibili molti tipi di eventi di input predefiniti. aws_lambda_events Ad esempio, se intendi richiamare la tua funzione con API Gateway, inclusa la seguente importazione:

use aws_lambda_events::event::apigw::ApiGatewayProxyRequest;

Quindi, assicurati che il tuo gestore principale utilizzi la seguente firma:

async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<String, Error> { let body = event.payload.body.unwrap_or_default(); ... }

Fai riferimento alla cassa aws_lambda_events per ulteriori informazioni su altri tipi di eventi di input predefiniti.

Accesso e utilizzo dell'oggetto contestuale Lambda

L'oggetto contesto: contiene informazioni sulla chiamata, sulla funzione e sull'ambiente di esecuzione. In Rust, il wrapper include l'oggetto context. LambdaEvent Ad esempio, è possibile utilizzare l'oggetto context per recuperare l'ID della richiesta della chiamata corrente con il seguente codice:

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let request_id = event.context.request_id; ... }

Per ulteriori informazioni sulla copia di oggetti, consulta la sezione Utilizzo dell'oggetto contestuale Lambda per recuperare le informazioni sulla funzione Rust.

Utilizzo della AWS SDK for Rust versione v2 nell'handler

Spesso, utilizzerai le funzioni Lambda per interagire o aggiornare altre AWS risorse. Il modo più semplice per interfacciarsi con queste risorse è usare la AWS SDK for Rust v2.

Per aggiungere dipendenze SDK alla tua funzione, aggiungile nel tuo file. Cargo.toml Ti consigliamo di aggiungere solo le librerie necessarie per la tua funzione. Nel codice di esempio precedente, abbiamo usatoaws_sdk_s3::Client. Nel Cargo.toml file, puoi aggiungere questa dipendenza aggiungendo la seguente riga nella [dependencies] sezione:

aws-sdk-s3 = "1.78.0"
Nota

Questa potrebbe non essere la versione più recente. Scegli la versione appropriata per la tua applicazione.

Quindi, importa le dipendenze direttamente nel tuo codice:

use aws_sdk_s3::{Client, primitives::ByteStream};

Il codice di esempio quindi inizializza un client Amazon S3 come segue:

let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let s3_client = Client::new(&config);

Dopo aver configurato e inizializzato il client SDK, puoi utilizzarlo per interagire con altri servizi. AWS Il codice di esempio richiama l'PutObjectAPI Amazon S3 nella funzione upload_receipt_to_s3 helper.

Accesso alle variabili d'ambiente

Nel codice dell'handler, puoi fare riferimento a qualsiasi variabile di ambiente utilizzando il metodo env::var. In questo esempio, facciamo riferimento alla variabile di RECEIPT_BUCKET ambiente definita utilizzando la seguente riga di codice:

let bucket_name = env::var("RECEIPT_BUCKET") .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;

Utilizzo dello stato condiviso

È possibile dichiarare delle variabili condivise indipendenti dal codice del gestore della funzione Lambda. Queste variabili possono aiutarti a caricare le informazioni sullo stato durante i Fase di init, prima che la funzione riceva eventi. Ad esempio, puoi modificare il codice in questa pagina per utilizzare lo stato condiviso durante l'inizializzazione del client Amazon S3 aggiornando la funzione e la firma main del gestore:

async fn function_handler(client: &Client, event: LambdaEvent<Value>) -> Result<String, Error> { ... upload_receipt_to_s3(client, &bucket_name, &key, &receipt_content).await?; ... } ... #[tokio::main] async fn main() -> Result<(), Error> { let shared_config = aws_config::from_env().load().await; let client = Client::new(&shared_config); let shared_client = &client; lambda_runtime::run(service_fn(move |event: LambdaEvent<Request>| async move { handler(&shared_client, event).await })) .await

Best practice di codice per funzioni Lambda in Rust

Segui le linee guida riportate nell'elenco seguente per utilizzare le best practice di codifica durante la creazione delle funzioni Lambda:

  • Separare il gestore Lambda dalla logica principale. In questo modo è possibile creare una funzione di cui è più semplice eseguire l'unit test.

  • Ridurre la complessità delle dipendenze. Preferire framework più semplici che si caricano velocemente all'avvio del contesto di esecuzione.

  • Ridurre al minimo le dimensioni del pacchetto di implementazione al fine di soddisfare le esigenze di runtime. In questo modo viene ridotta la quantità di tempo necessaria per il download del pacchetto e per la relativa decompressione prima dell'invocazione.

Sfruttare il riutilizzo del contesto di esecuzione per migliorare le prestazioni della funzione. Inizializzare i client SDK e le connessioni al database all'esterno del gestore di funzioni e memorizzare localmente nella cache gli asset statici nella directory /tmp. Le chiamate successive elaborate dalla stessa istanza della funzione possono riutilizzare queste risorse. Ciò consente di risparmiare sui costi riducendo i tempi di esecuzione delle funzioni.

Per evitare potenziali perdite di dati tra le chiamate, non utilizzare il contesto di esecuzione per archiviare dati utente, eventi o altre informazioni con implicazioni di sicurezza. Se la funzione si basa su uno stato mutabile che non può essere archiviato in memoria all'interno del gestore, considerare la possibilità di creare una funzione separata o versioni separate di una funzione per ogni utente.

Utilizzare una direttiva keep-alive per mantenere le connessioni persistenti. Lambda elimina le connessioni inattive nel tempo. Se si tenta di riutilizzare una connessione inattiva quando si richiama una funzione, si verificherà un errore di connessione. Per mantenere la connessione persistente, utilizzare la direttiva keep-alive associata al runtime. Per un esempio, vedere Riutilizzo delle connessioni con Keep-Alive in Node.js.

Utilizzare le variabili di ambiente per passare i parametri operativi alla funzione. Se ad esempio si scrive in un bucket Amazon S3 anziché impostare come hard-coded il nome del bucket in cui si esegue la scrittura, configurare tale nome come una variabile di ambiente.

Evita di usare invocazioni ricorsive nella tua funzione Lambda, in cui la funzione si richiama da sola o avvia un processo che potrebbe richiamare nuovamente la funzione. Ciò potrebbe provocare un volume non desiderato di invocazioni della funzione e un aumento dei costi. Se noti un volume indesiderato di invocazioni, imposta immediatamente la simultaneità riservata della funzione su 0 per interrompere tutte le invocazioni della funzione mentre si aggiorna il codice.

Non utilizzare API non documentate e non pubbliche nel codice della funzione Lambda. Per i tempi di esecuzione gestiti AWS Lambda, Lambda applica periodicamente aggiornamenti di sicurezza e funzionalità alle API interne di Lambda. Questi aggiornamenti API interni potrebbero essere incompatibili con le versioni precedenti, causando conseguenze indesiderate come errori di chiamata se la funzione ha una dipendenza su queste API non pubbliche. Consulta il riferimento all'API per un elenco di API disponibili pubblicamente.

Scrivi un codice idempotente. La scrittura di un codice idempotente per le tue funzioni garantisce che gli eventi duplicati vengano gestiti allo stesso modo. Il tuo codice dovrebbe convalidare correttamente gli eventi e gestire con garbo gli eventi duplicati. Per ulteriori informazioni, consulta Come posso rendere idempotente la mia funzione Lambda?.