Definieren von Lambda-Funktionshandlern in Rust - AWS Lambda

Definieren von Lambda-Funktionshandlern in Rust

Anmerkung

Der Rust-Laufzeit-Client ist ein experimentelles Paket. Er kann sich ändern und ist nur zu Evaluierungszwecken gedacht.

Der Lambda-Funktionshandler ist die Methode in Ihrem Funktionscode, die Ereignisse verarbeitet. Wenn Ihre Funktion aufgerufen wird, führt Lambda die Handler-Methode aus. Ihre Funktion wird so lange ausgeführt, bis der Handler eine Antwort zurückgibt, beendet wird oder ein Timeout auftritt.

Auf dieser Seite wird beschrieben, wie Sie mit Lambda-Funktionshandlern in Rust arbeiten, einschließlich der Projektinitialisierung, Benennungskonventionen und Best Practices. Diese Seite enthält auch ein Beispiel für eine Rust-Lambda-Funktion, die Informationen über eine Bestellung aufnimmt, eine Textdatei als Quittung erstellt und diese Datei in einen Amazon Simple Storage Service (S3)-Bucket stellt. Weitere Informationen darüber, wie Sie Ihre Funktion nach dem Schreiben einsetzen können, finden Sie unter Bereitstellen von Lambda-Rust-Funktionen mit ZIP-Dateiarchiven.

Einrichten Ihres Rust-Handler-Projekts

Wenn Sie mit Lambda-Funktionen in Rust arbeiten, umfasst der Prozess das Schreiben Ihres Codes, das Kompilieren und das Bereitstellen der kompilierten Artefakte in Lambda. Der einfachste Weg, ein Lambda-Handler-Projekt in Rust einzurichten, ist die Verwendung von AWS Lambda Runtime for Rust. Trotz des Namens ist AWS Lambda Runtime for Rust keine verwaltete Laufzeit im gleichen Sinne wie Lambda für Python, Java oder Node.js. Stattdessen ist AWS Lambda Runtime for Rust eine Crate (lambda_runtime), die das Schreiben von Lambda-Funktionen in Rust und die Anbindung an die Ausführungsumgebung von AWS Lambda unterstützt.

Verwenden Sie den folgenden Befehl, um AWS Lambda Runtime for Rust zu installieren.

cargo install cargo-lambda

Verwenden Sie nach der erfolgreichen Installation von cargo-lambda den folgenden Befehl, um ein neues Rust-Lambda-Funktions-Handler-Projekt zu initialisieren:

cargo lambda new example-rust

Wenn Sie diesen Befehl ausführen, stellt Ihnen die Befehlszeilenschnittstelle (CLI) einige Fragen zu Ihrer Lambda-Funktion:

  • HTTP-Funktion: Wenn Sie beabsichtigen, Ihre Funktion über API Gateway oder eine Funktions-URL aufzurufen, antworten Sie mit Ja. Andernfalls antworten Sie mit Nein. Im Beispielcode auf dieser Seite rufen wir unsere Funktion mit einem benutzerdefinierten JSON-Ereignis auf und wählen daher Nein.

  • Ereignistyp: Wenn Sie eine vordefinierte Ereignisform verwenden möchten, um Ihre Funktion aufzurufen, wählen Sie den richtigen erwarteten Ereignistyp aus. Andernfalls lassen Sie diese Option leer. Im Beispielcode auf dieser Seite rufen wir unsere Funktion mit einem benutzerdefinierten JSON-Ereignis auf, daher lassen wir diese Option leer.

Nachdem der Befehl erfolgreich ausgeführt wurde, geben Sie das Hauptverzeichnis Ihres Projekts ein:

cd example-rust

Mit diesem Befehl werden eine generic_handler.rs- und eine main.rs-Datei im src-Verzeichnis generiert. generic_handler.rs kann verwendet werden, um einen generischen Ereignis-Handler anzupassen. Die main.rs-Datei enthält Ihre Hauptanwendungslogik. Die Cargo.toml-Datei enthält Metadaten über Ihr Paket und listet dessen externe Abhängigkeiten auf.

Beispiel für Rust-Lambda-Funktionscode

Das folgende Beispiel für einen Rust-Lambda-Funktionscode nimmt Informationen über eine Bestellung auf, erstellt eine Textdateiquittung und platziert diese Datei in einem Amazon-S3-Bucket.

Beispiel main.rs-Lambda-Funktion
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 }

Diese main.rs-Datei enthält die folgenden Abschnitte des Codes:

  • use-Anweisungen: Damit können Sie Rust-Crates und -Methoden importieren, die für Ihre Lambda-Funktion erforderlich sind.

  • #[derive(Deserialize, Serialize)]: Definieren Sie die Form des erwarteten Eingabeereignisses in dieser Rust-Struktur.

  • async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>: Dies ist die Haupthandler-Methode, die Ihre Hauptanwendungslogik enthält.

  • async fn upload_receipt_to_s3 (...): Dies ist eine Hilfsmethode, auf die von der Haupt-function_handler-Methode verwiesen wird.

  • #[tokio::main]: Dies ist ein Makro, das den Einstiegspunkt eines Rust-Programms markiert. Außerdem wird eine Tokio-Laufzeit eingerichtet, die es Ihrer main()-Methode ermöglicht, async/await asynchron zu verwenden und auszuführen.

  • async fn main() -> Result<(), Error>: Die main()-Funktion ist der Einstiegspunkt Ihres Codes. Darin spezifizieren wir function_handler als Haupt-Handler-Methode.

Die folgende Cargo.toml-Datei gehört zu dieser Funktion.

[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"] }

Damit diese Funktion ordnungsgemäß funktioniert, muss ihre Ausführungsrolle die s3:PutObject-Aktion zulassen. Stellen Sie außerdem sicher, dass Sie die RECEIPT_BUCKET-Umgebungsvariable definieren. Nach einem erfolgreichen Aufruf sollte der Amazon-S3-Bucket eine Empfangsdatei enthalten.

Gültige Klassendefinitionen für Rust-Handler

In den meisten Fällen haben Lambda-Handler-Signaturen, die Sie in Rust definieren, das folgende Format:

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

Für diesen Handler:

  • Der Name dieses Handlers ist function_handler.

  • Der einzige Eingabewert für den Handler ist „event“ und hat den Typ LambdaEvent<T>.

    • LambdaEvent ist ein Wrapper, die aus der lambda_runtime-Crate stammt. Durch die Verwendung dieses Wrappers erhalten Sie Zugriff auf das Kontextobjekt, das Lambda-spezifische Metadaten wie die Anforderungs-ID des Aufrufs enthält.

    • T ist der deserialisierte Ereignistyp. Dies kann beispielsweise serde_json::Value sein, wodurch der Handler jede generische JSON-Eingabe entgegennehmen kann. Alternativ kann dies ein Typ wie ApiGatewayProxyRequest sein, wenn Ihre Funktion einen bestimmten, vordefinierten Eingabetyp erwartet.

  • Der Rückgabetyp des Handlers ist Result<U, Error>.

    • U ist der deserialisierte Ausgabetyp. U muss das serde::Serialize-Merkmal implementieren, damit Lambda den Rückgabewert in JSON konvertieren kann. Beispielsweise kann U ein einfacher Typ wie String, serde_json::Value oder eine benutzerdefinierte Struktur sein, solange es Serialize implementiert. Wenn Ihr Code eine Ok(U)-Anweisung erreicht, bedeutet dies, dass die Ausführung erfolgreich war, und Ihre Funktion gibt einen Wert vom Typ U zurück.

    • Wenn Ihr Code auf einen Fehler stößt (z. B. Err(Error)), protokolliert Ihre Funktion den Fehler in Amazon CloudWatch und gibt eine Fehlerantwort vom Typ Error zurück.

In unserem Beispiel sieht die Handler-Signatur wie folgt aus:

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

Andere gültige Handler-Signaturen können Folgendes enthalten:

  • Weglassen des LambdaEvent-Wrappers: Wenn Sie LambdaEvent weglassen, verlieren Sie den Zugriff auf das Lambda-Kontextobjekt innerhalb Ihrer Funktion. Es folgt ein Beispiel für diese Art von Signatur:

    async fn handler(event: serde_json::Value) -> Result<String, Error>
  • Verwendung des Einheitentyps als Eingabe: Bei Rust können Sie den Einheitentyp verwenden, um eine leere Eingabe darzustellen. Dies wird häufig für Funktionen mit periodischen, geplanten Aufrufen verwendet. Es folgt ein Beispiel für diese Art von Signatur:

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

Namenskonventionen für Handler

Lambda-Handler in Rust haben keine strengen Benennungsbeschränkungen. Sie können zwar einen beliebigen Namen für Ihren Handler verwenden, Funktionsnamen in Rust werden aber im Allgemeinen in snake_case angegeben.

Für kleinere Anwendungen wie in diesem Beispiel können Sie eine einzige main.rs-Datei verwenden, die Ihren gesamten Code enthält. Bei größeren Projekten sollte main.rs den Einstiegspunkt zu Ihrer Funktion enthalten, aber Sie können zusätzliche Dateien dafür haben, die Ihren Code in logische Module unterteilen. Beispielsweise könnte Ihre Dateistruktur wie folgt aussehen:

/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

Definieren Sie das Eingabeereignisobjekt und greifen Sie darauf zu

JSON ist das gebräuchlichste und standardmäßigste Eingabeformat für Lambda-Funktionen. In diesem Beispiel erwartet die Funktion eine Eingabe ähnlich der folgenden:

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

In Rust können Sie die Form des erwarteten Eingabeereignisses in einer Struktur definieren. In diesem Beispiel definieren wir die folgende Struktur, die einen Order darstellt:

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

Diese Struktur entspricht der erwarteten Eingabeform. In diesem Beispiel generiert das #[derive(Deserialize, Serialize)]-Makro automatisch Code für die Serialisierung und Deserialisierung. Das bedeutet, dass wir den generischen Eingabe-JSON-Typ mithilfe der serde_json::from_value()-Methode in unsere Struktur deserialisieren können. Dies wird in den ersten Zeilen des Handlers veranschaulicht:

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)?; ... }

Sie können dann auf die Felder des Objekts zugreifen. Zum Beispiel holt order.order_id den Wert von order_id aus der ursprünglichen Eingabe.

Vordefinierte Eingabe-Ereignistypen

In der aws_lambda_events-Crate sind viele vordefinierte Eingabe-Ereignistypen verfügbar. Wenn Sie beispielsweise beabsichtigen, Ihre Funktion mit API Gateway aufzurufen, einschließlich des folgenden Imports:

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

Stellen Sie dann sicher, dass Ihr Haupt-Handler die folgende Signatur verwendet:

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

Weitere Informationen zu anderen vordefinierten Eingabe-Ereignistypen finden Sie in der Crate aws_lambda_events.

Zugreifen auf und Verwenden des Lambda-Kontextobjekts

Das Lambda-Kontextobjekt enthält Informationen über Aufruf, Funktion und Ausführungsumgebung. In Rust enthält der LambdaEvent-Wrapper das Kontextobjekt. Beispielsweise können Sie das Kontextobjekt verwenden, um die Anforderungs-ID des aktuellen Aufrufs mit dem folgenden Code abzurufen:

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

Weitere Informationen über das Kontextobjekt finden Sie unter Verwenden des Lambda-Kontextobjekts zum Abrufen von Rust-Funktionsinformationen.

Verwenden von AWS SDK für Rust in Ihrem Handler

Oft verwenden Sie Lambda-Funktionen, um mit anderen AWS-Ressourcen zu interagieren oder diese zu aktualisieren. Die einfachste Art, eine Schnittstelle zu diesen Ressourcen herzustellen, ist die Verwendung von AWS SDK für Rust.

Um SDK-Abhängigkeiten zu Ihrer Funktion hinzuzufügen, fügen Sie diese in Ihrer Cargo.toml-Datei hinzu. Wir empfehlen, nur die Bibliotheken hinzuzufügen, die Sie für Ihre Funktion benötigen. Im Beispielcode zuvor haben wir aws_sdk_s3::Client verwendet. In der Cargo.toml-Datei können Sie diese Abhängigkeit hinzufügen, indem Sie die folgende Zeile unter dem [dependencies]-Abschnitt hinzufügen:

aws-sdk-s3 = "1.78.0"
Anmerkung

Dies ist möglicherweise nicht die neueste Version. Wählen Sie die passende Version für Ihre Anwendung.

Importieren Sie dann die Abhängigkeiten direkt in Ihren Code:

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

Der Beispielcode initialisiert dann einen Amazon-S3-Client wie folgt:

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

Nachdem Sie Ihren SDK-Client initialisiert haben, können Sie ihn für die Interaktion mit anderen AWS-Diensten verwenden. Der Beispielcode ruft die Amazon-S3-PutObject-API in der Hilfsfunktion upload_receipt_to_s3 auf.

Zugriff auf Umgebungsvariablen

In Ihrem Handler-Code können Sie mithilfe der env::var-Methode auf beliebige Umgebungsvariablen verweisen. In diesem Beispiel verweisen wir mit der folgenden Codezeile auf die definierte RECEIPT_BUCKET-Umgebungsvariable:

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

Geteilten Zustand verwenden

Sie können geteilte Variablen deklarieren, die unabhängig vom Handler-Code Ihrer Lambda-Funktion sind. Diese Variablen können Ihnen helfen, Zustandsinformationen während des Init-Phase zu laden, bevor Ihre Funktion Ereignisse empfängt. Beispielsweise können Sie den Code auf dieser Seite so ändern, dass bei der Initialisierung des Amazon-S3-Clients ein gemeinsamer Status verwendet wird, indem Sie die main-Funktion und die Handler-Signatur aktualisieren:

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

Bewährte Codemethoden für Rust-Lambda-Funktionen

Halten Sie sich an die Richtlinien in der folgenden Liste, um beim Erstellen Ihrer Lambda-Funktionen die besten Codierungspraktiken anzuwenden:

  • Trennen Sie den Lambda-Handler von Ihrer Core-Logik. Auf diese Weise können Sie eine Funktion zur besseren Prüfbarkeit von Einheiten schaffen.

  • Minimieren Sie die Komplexität Ihrer Abhängigkeiten. Ziehen Sie einfachere Frameworks vor, die sich schnell beim Start der Ausführungsumgebung laden lassen.

  • Minimieren Sie die Größe Ihres Bereitstellungspakets auf die für die Laufzeit erforderliche Größe. Dadurch verkürzt sich die Zeit, die für das Herunterladen und Entpacken Ihres Bereitstellungspakets vor dem Aufruf benötigt wird.

Nutzen Sie die Wiederverwendung der Ausführungsumgebung zur Verbesserung Ihrer Funktion. Initialisieren Sie SDK-Clients und Datenbankverbindungen außerhalb des Funktions-Handlers und speichern Sie statische Komponenten lokal im /tmp-Verzeichnis. Nachfolgende Aufrufe, die von derselben Instance Ihrer Funktion verarbeitet werden, können diese Ressourcen wiederverwenden. Dies spart Kosten durch Reduzierung der Funktionslaufzeit.

Um potenzielle Datenlecks über Aufrufe hinweg zu vermeiden, verwenden Sie die Ausführungsumgebung nicht, um Benutzerdaten, Ereignisse oder andere Informationen mit Sicherheitsauswirkungen zu speichern. Wenn Ihre Funktion auf einem veränderbaren Zustand beruht, der nicht im Speicher innerhalb des Handlers gespeichert werden kann, sollten Sie für jeden Benutzer eine separate Funktion oder separate Versionen einer Funktion erstellen.

Verwenden Sie eine Keep-Alive-Direktive, um dauerhafte Verbindungen zu pflegen. Lambda bereinigt Leerlaufverbindungen im Laufe der Zeit. Der Versuch, eine Leerlaufverbindung beim Aufruf einer Funktion wiederzuverwenden, führt zu einem Verbindungsfehler. Um Ihre persistente Verbindung aufrechtzuerhalten, verwenden Sie die Keep-Alive-Direktive, die Ihrer Laufzeit zugeordnet ist. Ein Beispiel finden Sie unter Wiederverwenden von Verbindungen mit Keep-Alive in Node.js.

Verwenden Sie Umgebungsvariablen um Betriebsparameter an Ihre Funktion zu übergeben. Wenn Sie z. B. Daten in einen Amazon-S3-Bucket schreiben, anstatt den Bucket-Namen, in den Sie schreiben, hartzucodieren, konfigurieren Sie den Bucket-Namen als Umgebungsvariable.

Vermeiden Sie rekursive Aufrufe in Ihrer Lambda-Funktion, bei denen die Funktion sich selbst aufruft oder einen Prozess initiiert, der die Funktion erneut aufrufen kann. Dies kann zu unvorhergesehenen Mengen an Funktionsaufrufen führen und höhere Kosten zur Folge haben. Wenn Sie eine unbeabsichtigte Menge von Aufrufen feststellen, legen Sie die reservierte gleichzeitige Ausführung der Funktion auf 0 fest, um sofort alle Aufrufe der Funktion zu drosseln, während Sie den Code aktualisieren.

Verwenden Sie keine nicht dokumentierten, nicht öffentlichen APIs in Ihrem Lambda-Funktionscode. Für AWS Lambda-verwaltete Laufzeiten wendet Lambda regelmäßig Sicherheits- und Funktionsupdates auf Lambdas interne APIs an. Diese internen API-Updates können abwärtskompatibel sein, was zu unbeabsichtigten Konsequenzen wie Aufruffehlern führt, wenn Ihre Funktion von diesen nicht öffentlichen APIs abhängig ist. Eine Liste öffentlich zugänglicher APIs finden Sie in der API-Referenz.

Schreiben Sie idempotenten Code. Das Schreiben idempotenter Code für Ihre Funktionen stellt sicher, dass doppelte Ereignisse auf die gleiche Weise behandelt werden. Ihr Code sollte Ereignisse ordnungsgemäß validieren und doppelte Ereignisse ordnungsgemäß behandeln. Weitere Informationen finden Sie unter Wie mache ich meine Lambda-Funktion idempotent?.